Files
paseo/packages/server/src/server/daemon-e2e/permissions-codex.e2e.test.ts
T
Mohamed Boudra d964415eba feat(server): improve task CLI with steering, execution order, and planner guidance
- Fix steering notes to check scope task instead of leaf task
- Fix NOT_DONE replan to use scope for bird's eye view
- Pass reason to planner prompt ("Why you were called" section)
- Add execution-order.ts with reusable computeExecutionOrder and buildSortedChildrenMap
- Make 'tree' an alias for 'plan' command
- Add comprehensive help text for run command and acceptance criteria
- Add planner guidance for propagating requirements to subtasks
2026-01-17 11:46:38 +07:00

379 lines
13 KiB
TypeScript

import { describe, test, expect, beforeEach, afterEach } from "vitest";
import { mkdtempSync, writeFileSync, existsSync, rmSync, mkdirSync, readFileSync, readdirSync } from "fs";
import { tmpdir } from "os";
import path from "path";
import {
createDaemonTestContext,
type DaemonTestContext,
} from "../test-utils/index.js";
import type { AgentTimelineItem } from "../agent/agent-sdk-types.js";
import type { AgentSnapshotPayload, SessionOutboundMessage } from "../messages.js";
function tmpCwd(): string {
return mkdtempSync(path.join(tmpdir(), "daemon-e2e-"));
}
// Use gpt-5.1-codex-mini with low reasoning effort for faster test execution
const CODEX_TEST_MODEL = "gpt-5.1-codex-mini";
const CODEX_TEST_REASONING_EFFORT = "low";
describe("daemon E2E", () => {
let ctx: DaemonTestContext;
beforeEach(async () => {
ctx = await createDaemonTestContext();
});
afterEach(async () => {
await ctx.cleanup();
}, 60000);
describe("permission flow: Codex", () => {
test(
"approves permission and executes command",
async () => {
const cwd = tmpCwd();
const filePath = path.join(cwd, "permission.txt");
// Create Codex agent with on-request approval policy
const agent = await ctx.client.createAgent({
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
cwd,
title: "Codex Permission Test",
modeId: "auto",
});
expect(agent.id).toBeTruthy();
expect(agent.status).toBe("idle");
// Clear message queue before sending prompt
ctx.client.clearMessageQueue();
// Send a prompt that requires permission
const prompt = [
"Request approval to run the command `printf \"ok\" > permission.txt`.",
"After approval, run it and reply DONE.",
].join(" ");
await ctx.client.sendMessage(agent.id, prompt);
// Wait for permission request
const permission = await ctx.client.waitForPermission(agent.id, 60000);
expect(permission).not.toBeNull();
expect(permission.id).toBeTruthy();
expect(permission.kind).toBe("tool");
// Approve the permission
await ctx.client.respondToPermission(agent.id, permission.id, {
behavior: "allow",
});
// Wait for agent to complete
const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000);
expect(finalState.status).toBe("idle");
// Verify the file was created
expect(existsSync(filePath)).toBe(true);
// Verify permission_resolved event was received
const queue = ctx.client.getMessageQueue();
const hasPermissionResolved = queue.some((m) => {
if (m.type === "agent_stream" && m.payload.agentId === agent.id) {
return (
m.payload.event.type === "permission_resolved" &&
m.payload.event.requestId === permission.id &&
m.payload.event.resolution.behavior === "allow"
);
}
return false;
});
expect(hasPermissionResolved).toBe(true);
rmSync(cwd, { recursive: true, force: true });
},
180000
);
test(
"denies permission and prevents execution",
async () => {
const cwd = tmpCwd();
const filePath = path.join(cwd, "permission.txt");
// Create Codex agent with on-request approval policy
const agent = await ctx.client.createAgent({
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
cwd,
title: "Codex Permission Deny Test",
modeId: "auto",
});
expect(agent.id).toBeTruthy();
// Clear message queue before sending prompt
ctx.client.clearMessageQueue();
// Send a prompt that requires permission
const prompt = [
"Request approval to run the command `printf \"ok\" > permission.txt`.",
"If approval is denied, acknowledge and stop.",
].join(" ");
await ctx.client.sendMessage(agent.id, prompt);
// Wait for permission request
const permission = await ctx.client.waitForPermission(agent.id, 60000);
expect(permission).not.toBeNull();
expect(permission.id).toBeTruthy();
// Deny the permission
await ctx.client.respondToPermission(agent.id, permission.id, {
behavior: "deny",
message: "Not allowed.",
});
// Wait for agent to complete
const finalState = await ctx.client.waitForAgentIdle(agent.id, 120000);
expect(finalState.status).toBe("idle");
// Verify the file was NOT created
expect(existsSync(filePath)).toBe(false);
// Verify permission_resolved event was received with deny
const queue = ctx.client.getMessageQueue();
const hasPermissionDenied = queue.some((m) => {
if (m.type === "agent_stream" && m.payload.agentId === agent.id) {
return (
m.payload.event.type === "permission_resolved" &&
m.payload.event.requestId === permission.id &&
m.payload.event.resolution.behavior === "deny"
);
}
return false;
});
expect(hasPermissionDenied).toBe(true);
rmSync(cwd, { recursive: true, force: true });
},
180000
);
// TODO: Fix this test - there's a race condition causing agent not found errors
test.skip(
"Codex agent can complete a new turn after interrupt",
async () => {
const cwd = tmpCwd();
// Create Codex agent with full-access (no permissions needed)
const agent = await ctx.client.createAgent({
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
cwd,
title: "Codex Interrupt Test",
modeId: "full-access",
});
expect(agent.id).toBeTruthy();
expect(agent.currentModeId).toBe("full-access");
// Send first message to start the agent
ctx.client.clearMessageQueue();
await ctx.client.sendMessage(agent.id, "List the files in the current directory.");
// Wait for agent to start running
await ctx.client.waitForAgentState(
agent.id,
(snapshot) => snapshot.status === "running",
10000
);
// Cancel while running
await ctx.client.cancelAgent(agent.id);
// Wait for agent to become idle after cancellation
// Don't use waitForAgentIdle because it requires seeing "running" first,
// but we already saw it above. Just wait for "idle" or "error".
await ctx.client.waitForAgentState(
agent.id,
(snapshot) =>
snapshot.status === "idle" || snapshot.status === "error",
30000
);
// Now send another message - this should work
ctx.client.clearMessageQueue();
await ctx.client.sendMessage(
agent.id,
"Say 'hello from interrupt test' and nothing else."
);
// Wait for this to complete
await ctx.client.waitForAgentIdle(agent.id, 60000);
// Verify we got an assistant message in the queue
const queue = ctx.client.getMessageQueue();
const hasAssistantMessage = queue.some(
(m) =>
m.type === "agent_stream" &&
m.agentId === agent.id &&
m.event?.type === "timeline" &&
m.event?.item?.type === "assistant_message"
);
expect(hasAssistantMessage).toBe(true);
rmSync(cwd, { recursive: true, force: true });
},
120000
);
test(
"aborting Codex actually stops execution (sleep + write test)",
async () => {
const cwd = tmpCwd();
const filePath = path.join(cwd, "abort-test-file.txt");
// Create Codex agent with full-access (no permissions needed)
const agent = await ctx.client.createAgent({
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
cwd,
title: "Codex Abort Stop Test",
modeId: "full-access",
});
expect(agent.id).toBeTruthy();
// Ask Codex to sleep 15 seconds then write a file
ctx.client.clearMessageQueue();
await ctx.client.sendMessage(
agent.id,
"Run this bash command: sleep 15 && echo 'abort-test-completed' > abort-test-file.txt"
);
// Wait 3 seconds for the command to start
await new Promise((r) => setTimeout(r, 3000));
// Cancel/interrupt the agent
await ctx.client.cancelAgent(agent.id);
// Wait 10 seconds - if abort works, file should NOT be written
// (sleep would have completed at 15s if not interrupted)
await new Promise((r) => setTimeout(r, 10000));
// Assert the file was NOT created (proving Codex actually stopped)
const fileExists = existsSync(filePath);
expect(fileExists).toBe(false);
rmSync(cwd, { recursive: true, force: true });
},
30000 // 30 second timeout
);
// TODO: Fix this test - there's a race condition causing timeout errors
test.skip(
"switching from auto to full-access mode allows writes without permission",
async () => {
const cwd = tmpCwd();
const filePath = path.join(cwd, "mode-switch-test.txt");
// Step 1: Create Codex agent with "auto" mode (requires permission for writes)
const agent = await ctx.client.createAgent({
provider: "codex", model: CODEX_TEST_MODEL, reasoningEffort: CODEX_TEST_REASONING_EFFORT,
cwd,
title: "Codex Mode Switch Permission Test",
modeId: "auto",
});
expect(agent.id).toBeTruthy();
expect(agent.currentModeId).toBe("auto");
// Step 2: Ask agent to write a file - this should trigger permission request
// Note: We DON'T tell the agent to "stop" if denied - this keeps the conversation
// alive and tests the real scenario where mode switch must work mid-conversation.
ctx.client.clearMessageQueue();
const writePrompt =
"Write a file called mode-switch-test.txt with the content 'first'";
await ctx.client.sendMessage(agent.id, writePrompt);
// Step 3: Wait for permission request
const permission = await ctx.client.waitForPermission(agent.id, 60000);
expect(permission).not.toBeNull();
expect(permission.id).toBeTruthy();
// Step 4: Deny the permission
await ctx.client.respondToPermission(agent.id, permission.id, {
behavior: "deny",
message: "Permission denied for test.",
});
// Wait for agent to complete after denial
await ctx.client.waitForAgentIdle(agent.id, 120000);
// Verify file was NOT created after denial
expect(existsSync(filePath)).toBe(false);
// Step 5: Switch to "full-access" mode
ctx.client.clearMessageQueue();
const modeStartPosition = ctx.client.getMessageQueue().length;
await ctx.client.setAgentMode(agent.id, "full-access");
// Wait for mode change to be reflected in agent_state
await new Promise<void>((resolve, reject) => {
const timeout = setTimeout(() => {
reject(new Error("Timeout waiting for full-access mode change"));
}, 15000);
const checkForModeChange = (): void => {
const queue = ctx.client.getMessageQueue();
for (let i = modeStartPosition; i < queue.length; i++) {
const msg = queue[i];
if (
msg.type === "agent_state" &&
msg.payload.id === agent.id &&
msg.payload.currentModeId === "full-access"
) {
clearTimeout(timeout);
clearInterval(interval);
resolve();
return;
}
}
};
const interval = setInterval(checkForModeChange, 50);
});
// Step 6: Ask agent to write file again - should succeed WITHOUT permission request
// In full-access mode, the agent should just execute without asking.
ctx.client.clearMessageQueue();
const writePrompt2 =
"Write a file called mode-switch-test.txt with the content 'success'";
await ctx.client.sendMessage(agent.id, writePrompt2);
// Wait for agent to complete
await ctx.client.waitForAgentIdle(agent.id, 120000);
// Step 7: Verify file was created (mode switch worked)
expect(existsSync(filePath)).toBe(true);
const content = readFileSync(filePath, "utf-8");
expect(content).toBe("success");
// Verify no permission was requested in this second attempt
const queue = ctx.client.getMessageQueue();
const hasPermissionRequest = queue.some(
(m) =>
m.type === "agent_permission_request" &&
m.agentId === agent.id
);
expect(hasPermissionRequest).toBe(false);
rmSync(cwd, { recursive: true, force: true });
},
240000
);
});
});