Skip to content
Merged
Show file tree
Hide file tree
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
12 changes: 12 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,18 @@ jobs:
- name: Test Windows sync-port holder identification
run: cd apps/ade-cli && npx vitest run src/services/sync/sharedSyncListener.test.ts

# cli.test.ts carries the win32-gated headless-RPC named-pipe case. It is
# green only once this layer's CLI fixes compose, so the step lives here
# rather than in the foundation layer.
- name: Test Windows CLI contracts
run: cd apps/ade-cli && npx vitest run src/cli.test.ts

# Daemon supervision, restart, and version/role-compatibility. Spawns real
# `ade serve` daemons over the platform transport, so this is the only gate
# that exercises the always-on-brain contract on a named pipe.
- name: Test Windows stdio RPC daemon bridge contracts
run: cd apps/ade-cli && npx vitest run src/stdioRpcDaemon.test.ts

# `trustedWindowsTools` is a security control whose only substantive case
# is win32-gated, so before this step it ran on no runner at all. The
# credential store and the `ade://` deeplink command-injection guard are
Expand Down
123 changes: 72 additions & 51 deletions apps/ade-cli/src/adeRpcServer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2246,31 +2246,37 @@ describe("adeRpcServer", () => {
});

expect(response?.isError).toBeUndefined();
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(
expect.objectContaining({
laneId: "lane-1",
cols: 120,
rows: 36,
tracked: true,
toolType: "claude-orchestrated",
command: claudePath,
args: expect.arrayContaining(["--model", "claude-sonnet-5", "--permission-mode", "default"]),
env: expect.objectContaining({
ADE_DEFAULT_ROLE: "agent",
}),
})
);
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(expect.objectContaining({
laneId: "lane-1",
cols: 120,
rows: 36,
tracked: true,
toolType: "claude-orchestrated",
env: expect.objectContaining({ ADE_DEFAULT_ROLE: "agent" }),
}));
const createCall = (fixture.runtime.ptyService.create as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as {
command?: string;
args?: string[];
startupCommand?: string;
};
// Provider resolution is platform-native on every OS: POSIX goes through
// `command -v`, Windows through `where.exe` (which honours PATHEXT, hence
// the `.cmd` fixture). A resolved provider always becomes a direct
// command/args launch so worker identity rides the process env instead of
// a POSIX-only `VAR=value cmd` prefix.
expect(createCall.command).toBe(claudePath);
expect(createCall.args).toEqual(expect.arrayContaining(["--model", "claude-sonnet-5", "--permission-mode", "default"]));
expect(createCall.startupCommand).toContain("claude --model claude-sonnet-5 --permission-mode default");
// The final arg concatenates ADE_CLI_INLINE_GUIDANCE with the user prompt; assert
// it ends with the user prompt and carries the inline guidance preamble.
const createCall = (fixture.runtime.ptyService.create as ReturnType<typeof vi.fn>).mock.calls[0]?.[0] as { args: string[] };
const finalArg = createCall.args[createCall.args.length - 1];
expect(finalArg).toContain("CLI controls ADE state");
expect(finalArg).toContain("PRs, proof, apps");
expect(finalArg).toContain("clean up started processes");
expect(finalArg).toContain("ade chat note");
expect(finalArg).toContain("ade chat ask");
expect(finalArg).toContain("You cannot settle or unsettle a session");
expect(finalArg.endsWith("Implement API wiring")).toBe(true);
const launchText = createCall.args?.at(-1) ?? createCall.startupCommand ?? "";
expect(launchText).toContain("CLI controls ADE state");
expect(launchText).toContain("PRs, proof, apps");
expect(launchText).toContain("clean up started processes");
expect(launchText).toContain("ade chat note");
expect(launchText).toContain("ade chat ask");
expect(launchText).toContain("You cannot settle or unsettle a session");
expect(launchText).toContain("Implement API wiring");
expect(response.structuredContent.startupCommand).toContain("claude");
expect(response.structuredContent.startupCommand).toContain("--model");
expect(response.structuredContent.startupCommand).toContain("--permission-mode");
Expand All @@ -2297,12 +2303,12 @@ describe("adeRpcServer", () => {

expect(response?.isError).toBeUndefined();
const createCall = fixture.runtime.ptyService.create.mock.calls[0]?.[0] as { args?: string[]; startupCommand?: string };
expect(createCall.args).toEqual(expect.arrayContaining(["--sandbox", "workspace-write", "--ask-for-approval", "on-request"]));
const finalArg = createCall.args?.at(-1) ?? "";
expect(finalArg).toContain("ade chat note");
expect(finalArg).toContain("ade chat ask");
expect(finalArg).toContain("You cannot settle or unsettle a session");
expect(createCall.args).not.toContain("--full-auto");
const launchText = createCall.args?.join(" ") ?? createCall.startupCommand ?? "";
expect(launchText).toContain("--sandbox workspace-write --ask-for-approval on-request");
expect(launchText).toContain("ade chat note");
expect(launchText).toContain("ade chat ask");
expect(launchText).toContain("You cannot settle or unsettle a session");
expect(launchText).not.toContain("--full-auto");
expect(createCall.startupCommand).toContain("--sandbox workspace-write --ask-for-approval on-request");
expect(createCall.startupCommand).not.toContain("--full-auto");
});
Expand Down Expand Up @@ -2520,22 +2526,30 @@ describe("adeRpcServer", () => {
});

expect(response?.isError).toBeUndefined();
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(
expect.objectContaining({
laneId: "lane-1",
title: "Shell",
toolType: "shell",
command: "/bin/zsh",
args: ["-f"],
env: { ZDOTDIR: "/var/empty" },
}),
);
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(expect.objectContaining(
process.platform === "win32"
? {
laneId: "lane-1",
title: "Shell",
toolType: "shell",
command: "powershell.exe",
args: ["-NoLogo", "-NoProfile"],
}
: {
laneId: "lane-1",
title: "Shell",
toolType: "shell",
command: "/bin/zsh",
args: ["-f"],
env: { ZDOTDIR: "/var/empty" },
},
));
});

it("starts Codex spawn_agent with current default permission flags", async () => {
const fixture = createRuntime();
const binDir = fs.mkdtempSync(path.join(os.tmpdir(), "ade-cli-spawn-bin-"));
createFakePathExecutable(binDir, "codex");
const codexPath = createFakePathExecutable(binDir, "codex");
const handler = createAdeRpcRequestHandler({ runtime: fixture.runtime, serverVersion: "test" });

const response = await withEnv({ PATH: `${binDir}${path.delimiter}${process.env.PATH ?? ""}`, SHELL: "/bin/sh" }, async () => {
Expand All @@ -2548,13 +2562,14 @@ describe("adeRpcServer", () => {
});

expect(response?.isError).toBeUndefined();
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(
expect.objectContaining({
command: expect.stringMatching(/codex$/),
args: expect.arrayContaining(["--sandbox", "workspace-write", "--ask-for-approval", "on-request"]),
startupCommand: expect.stringContaining("codex --sandbox workspace-write --ask-for-approval on-request"),
}),
);
const createCall = fixture.runtime.ptyService.create.mock.calls[0]?.[0] as {
command?: string;
args?: string[];
startupCommand?: string;
};
expect(createCall.startupCommand).toContain("codex --sandbox workspace-write --ask-for-approval on-request");
expect(createCall.command).toBe(codexPath);
expect(createCall.args).toEqual(expect.arrayContaining(["--sandbox", "workspace-write", "--ask-for-approval", "on-request"]));
expect(response.structuredContent.startupCommand).not.toContain("--full-auto");
});

Expand Down Expand Up @@ -2793,18 +2808,24 @@ describe("adeRpcServer", () => {

expect(response?.isError).toBeUndefined();
expect(response.structuredContent.startupCommand).toContain("claude");
expect(response.structuredContent.startupCommand).toContain("ADE_RUN_ID=run-1");
expect(response.structuredContent.startupCommand).toContain("ADE_ATTEMPT_ID=attempt-workspace-roots");
if (process.platform === "win32") {
expect(response.structuredContent.startupCommand).not.toContain("ADE_RUN_ID=run-1");
expect(response.structuredContent.startupCommand).not.toContain("ADE_ATTEMPT_ID=attempt-workspace-roots");
} else {
expect(response.structuredContent.startupCommand).toContain("ADE_RUN_ID=run-1");
expect(response.structuredContent.startupCommand).toContain("ADE_ATTEMPT_ID=attempt-workspace-roots");
}
expect(fixture.runtime.ptyService.create).toHaveBeenCalledWith(
expect.objectContaining({
command: claudePath,
env: expect.objectContaining({
ADE_RUN_ID: "run-1",
ADE_ATTEMPT_ID: "attempt-workspace-roots",
ADE_DEFAULT_ROLE: "agent",
}),
})
);
const createCall = fixture.runtime.ptyService.create.mock.calls[0]?.[0] as { command?: string };
expect(createCall.command).toBe(claudePath);
});

it("keeps spawn_agent on shell startup when the provider executable cannot be resolved", async () => {
Expand Down Expand Up @@ -2946,7 +2967,7 @@ describe("adeRpcServer", () => {
expect(response.structuredContent.startupCommand).toContain("CLI controls ADE state");
const contextPath = response.structuredContent.contextRef?.path as string | null;
expect(contextPath).toBeTruthy();
expect(contextPath?.includes("/.ade/cache/orchestrator/agent-context/run-123/")).toBe(true);
expect(contextPath?.replace(/\\/g, "/")).toContain("/.ade/cache/orchestrator/agent-context/run-123/");
if (!contextPath) {
throw new Error("Expected context manifest path");
}
Expand Down
2 changes: 2 additions & 0 deletions apps/ade-cli/src/adeRpcServer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,7 @@ function resolveExecutableOnPath(command: string, env: NodeJS.ProcessEnv = proce
encoding: "utf8",
env,
stdio: ["ignore", "pipe", "ignore"],
windowsHide: true,
});
if (result.status !== 0 || typeof result.stdout !== "string") return null;
const first = result.stdout
Expand Down Expand Up @@ -3507,6 +3508,7 @@ async function runTool(args: {
const result = spawnSync(command, commandArgs, {
cwd: runtime.projectRoot,
encoding: "utf8",
windowsHide: true,
env: {
...process.env,
...(options?.env ?? {}),
Expand Down
Loading
Loading