--bare disables OAuth/keychain auth and requires ANTHROPIC_API_KEY, breaking Claude Pro/Max subscriptions. CLAUDE.md isolation is now achieved structurally: workspaces live under ~/.disclaw/workspaces/ (outside the repo) so walk-up never reaches DisClaw's CLAUDE.md. Update integration test to assert --bare is absent and verify correct args (-p, --output-format json). Fix sanitize-env tests to reflect CI=true removal (was breaking OAuth headless-mode detection). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
149 lines
4.5 KiB
TypeScript
149 lines
4.5 KiB
TypeScript
import { describe, it, expect, vi, beforeAll, afterAll, beforeEach, afterEach } from "vitest";
|
|
import * as fs from "fs";
|
|
import * as os from "os";
|
|
import * as path from "path";
|
|
|
|
/**
|
|
* Integration test: verifies that Claude CLI invocations use the correct args.
|
|
*
|
|
* CLAUDE.md isolation is achieved structurally (workspaces live outside the
|
|
* repo under ~/.disclaw/workspaces/) — NOT via --bare or --append-system-prompt-file.
|
|
* --bare was removed because it disables OAuth/keychain auth, breaking Claude
|
|
* Pro/Max subscriptions.
|
|
*
|
|
* We mock cross-spawn so no real process is launched, and capture the exact
|
|
* args array that runner.ts would pass to the CLI.
|
|
*/
|
|
|
|
// Capture spawned args via a mock before any module imports runner
|
|
let capturedArgs: string[] = [];
|
|
|
|
vi.mock("cross-spawn", () => {
|
|
// Minimal EventEmitter-like child mock that immediately closes with code 0
|
|
function makeMockChild(args: string[]) {
|
|
capturedArgs = args;
|
|
const listeners: Record<string, Array<(...a: unknown[]) => void>> = {};
|
|
|
|
const stdioStream = {
|
|
setEncoding: () => {},
|
|
on: (event: string, cb: (...a: unknown[]) => void) => {
|
|
if (!listeners[event]) listeners[event] = [];
|
|
listeners[event].push(cb);
|
|
// emit 'data' with a JSON payload that looks like our fake-claude output
|
|
if (event === "data") {
|
|
Promise.resolve().then(() =>
|
|
cb(
|
|
JSON.stringify({
|
|
args,
|
|
hasBare: args.includes("--bare"),
|
|
systemPromptFile:
|
|
args.indexOf("--append-system-prompt-file") >= 0
|
|
? args[args.indexOf("--append-system-prompt-file") + 1]
|
|
: null,
|
|
})
|
|
)
|
|
);
|
|
}
|
|
},
|
|
};
|
|
|
|
const child = {
|
|
stdout: { ...stdioStream },
|
|
stderr: {
|
|
setEncoding: () => {},
|
|
on: () => {},
|
|
},
|
|
stdin: { end: () => {} },
|
|
on: (event: string, cb: (...a: unknown[]) => void) => {
|
|
if (!listeners[event]) listeners[event] = [];
|
|
listeners[event].push(cb);
|
|
if (event === "close") {
|
|
// Fire 'close' after stdout data has been emitted
|
|
Promise.resolve()
|
|
.then(() => Promise.resolve())
|
|
.then(() => cb(0));
|
|
}
|
|
},
|
|
kill: () => {},
|
|
};
|
|
|
|
return child;
|
|
}
|
|
|
|
return {
|
|
default: (_cmd: string, args: string[]) => makeMockChild(args),
|
|
};
|
|
});
|
|
|
|
// Import after mock registration
|
|
import { runAgent } from "../../src/agent/runner";
|
|
import { _resetResolveClaudeCache } from "../../src/runtime/resolve-claude";
|
|
|
|
describe("no-parent-claude-md-leak", () => {
|
|
let workspaceDir: string;
|
|
const originalEnv = { ...process.env };
|
|
|
|
beforeAll(() => {
|
|
// Create a minimal temp workspace with agent.yaml and CLAUDE.md
|
|
workspaceDir = fs.mkdtempSync(path.join(os.tmpdir(), "disclaw-test-"));
|
|
|
|
fs.writeFileSync(
|
|
path.join(workspaceDir, "agent.yaml"),
|
|
`name: test-agent\ndisplay_name: Test Agent\nrole: Testing\nchannel_id: "123456789"\n`,
|
|
"utf-8"
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(workspaceDir, "CLAUDE.md"),
|
|
"# Test Agent\nYou are a test agent.\n",
|
|
"utf-8"
|
|
);
|
|
});
|
|
|
|
beforeEach(() => {
|
|
capturedArgs = [];
|
|
// Reset the resolve-claude cache and set a dummy CLAUDE_PATH
|
|
_resetResolveClaudeCache();
|
|
process.env.CLAUDE_PATH = "/fake/claude";
|
|
});
|
|
|
|
afterEach(() => {
|
|
process.env = { ...originalEnv };
|
|
_resetResolveClaudeCache();
|
|
});
|
|
|
|
afterAll(() => {
|
|
try {
|
|
fs.rmSync(workspaceDir, { recursive: true, force: true });
|
|
} catch {
|
|
// Best-effort cleanup
|
|
}
|
|
});
|
|
|
|
it("does NOT pass --bare flag (--bare disables OAuth and breaks Claude Pro/Max)", async () => {
|
|
await runAgent({
|
|
workspacePath: workspaceDir,
|
|
channelName: "test-channel",
|
|
userMessage: "hello",
|
|
conversationHistory: [],
|
|
});
|
|
|
|
expect(capturedArgs).not.toContain("--bare");
|
|
});
|
|
|
|
it("passes -p and --output-format json; does NOT pass --append-system-prompt-file", async () => {
|
|
await runAgent({
|
|
workspacePath: workspaceDir,
|
|
channelName: "test-channel",
|
|
userMessage: "hello",
|
|
conversationHistory: [],
|
|
});
|
|
|
|
// Required args for non-interactive JSON output
|
|
expect(capturedArgs).toContain("-p");
|
|
expect(capturedArgs).toContain("--output-format");
|
|
expect(capturedArgs).toContain("json");
|
|
|
|
// CLAUDE.md is auto-discovered via cwd (workspacePath), no flag needed
|
|
expect(capturedArgs).not.toContain("--append-system-prompt-file");
|
|
});
|
|
});
|