Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
79 changes: 79 additions & 0 deletions test/cli/sandbox-mutations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,47 @@ import { describe, expect, it } from "vitest";

import { runWithEnv, testTimeoutOptions, writeSandboxRegistry } from "./helpers";

function readSandboxPolicies(home: string, sandboxName = "alpha"): string[] {
const registryPath = path.join(home, ".nemoclaw", "sandboxes.json");
const registry = JSON.parse(fs.readFileSync(registryPath, "utf8")) as {
sandboxes?: Record<string, { policies?: unknown }>;
};
const policies = registry.sandboxes?.[sandboxName]?.policies;
return Array.isArray(policies)
? policies.filter((policy): policy is string => typeof policy === "string")
: [];
}

function writePolicyMutationOpenshellStub(home: string): string {
const localBin = path.join(home, "bin");
fs.mkdirSync(localBin, { recursive: true });
const openshell = path.join(localBin, "openshell");
fs.writeFileSync(
openshell,
[
"#!/usr/bin/env bash",
"set -euo pipefail",
'if [ "$1" = "policy" ] && [ "$2" = "get" ]; then',
" cat <<'YAML'",
"version: 1",
"network_policies:",
" github:",
" name: github",
" host: github.com",
"YAML",
" exit 0",
"fi",
'if [ "$1" = "policy" ] && [ "$2" = "set" ]; then',
" exit 0",
"fi",
'printf "unexpected openshell args: %s\\n" "$*" >&2',
"exit 1",
].join("\n"),
{ mode: 0o755 },
);
return openshell;
}

describe("CLI dispatch", () => {
it("connect help uses native oclif usage through the public sandbox route", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-inspection-help-"));
Expand Down Expand Up @@ -68,6 +109,44 @@ describe("CLI dispatch", () => {
expect(snapshots.out).toContain("No snapshots found for 'alpha'.");
});

it("keeps public policy-add/remove built-in mutation routes", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-policy-mutation-"));
writeSandboxRegistry(home);
const openshell = writePolicyMutationOpenshellStub(home);

const add = runWithEnv("alpha policy-add github --yes", {
HOME: home,
NEMOCLAW_OPENSHELL_BIN: openshell,
});
expect(add.code).toBe(0);
expect(add.out).toContain("Applied preset: github");
expect(readSandboxPolicies(home)).toContain("github");

const remove = runWithEnv("alpha policy-remove github -y", {
HOME: home,
NEMOCLAW_OPENSHELL_BIN: openshell,
});
expect(remove.code).toBe(0);
expect(remove.out).toContain("Removed preset: github");
expect(readSandboxPolicies(home)).not.toContain("github");
});

it("keeps public policy-add non-interactive missing-preset failure before mutation", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-policy-noninteractive-"));
writeSandboxRegistry(home);
const openshell = writePolicyMutationOpenshellStub(home);

const result = runWithEnv("alpha policy-add", {
HOME: home,
NEMOCLAW_NON_INTERACTIVE: "1",
NEMOCLAW_OPENSHELL_BIN: openshell,
});

expect(result.code).toBe(1);
expect(result.out).toContain("Non-interactive mode requires a preset name.");
expect(readSandboxPolicies(home)).toEqual([]);
});

it("sandbox channels start rejects a sandbox missing from the registry (#4584)", () => {
const home = fs.mkdtempSync(path.join(os.tmpdir(), "nemoclaw-cli-channels-missing-"));
writeSandboxRegistry(home);
Expand Down
Loading