#!/usr/bin/env npx tsx /** * E2E Test: Permissions Workflow * * This test verifies the permissions workflow using REAL daemons and agents. * It starts an isolated daemon on a random port and runs actual CLI commands. * * Test flow: * 1. Start daemon on random port * 2. Create agent in DEFAULT mode (not bypassPermissions) so it requests permissions * 3. Wait for agent to hit a permission request * 4. Use `permit ls` to list pending permissions * 5. Use `permit allow` to approve the permission * 6. Verify agent continues after approval * 7. Cleanup * * CRITICAL RULES: * - NEVER use port 6767 (user's running daemon) * - Always use claude provider with haiku model for fast, cheap tests * - Clean up resources after test completes */ import assert from "node:assert"; import { createE2ETestContext, type TestDaemonContext } from "../helpers/test-daemon.ts"; interface E2EContext extends TestDaemonContext { paseo: ( args: string[], opts?: { timeout?: number; cwd?: string }, ) => Promise<{ exitCode: number; stdout: string; stderr: string; }>; } let ctx: E2EContext; async function setup(): Promise { console.log("Setting up E2E test context..."); console.log("Starting test daemon on random port (this may take a few seconds)..."); try { ctx = await createE2ETestContext({ timeout: 45000 }); console.log(`Test daemon started on port ${ctx.port}`); console.log(`PASEO_HOME: ${ctx.paseoHome}`); console.log(`Work directory: ${ctx.workDir}`); } catch (err) { console.error("Failed to start test daemon:", err); throw err; } } async function cleanup(): Promise { console.log("\nCleaning up..."); if (ctx) { await ctx.stop(); console.log("Test daemon stopped and temp directories removed"); } } function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } interface PermitResult { exitCode: number; stdout: string; } function findMatchingPermission( result: PermitResult, agentId: string, ): { agentId?: string } | undefined { if (result.exitCode !== 0) return undefined; try { const permissions = JSON.parse(result.stdout.trim()); if (!Array.isArray(permissions) || permissions.length === 0) return undefined; return permissions.find( (p: { agentId?: string }) => p.agentId?.startsWith(agentId.slice(0, 7)) || agentId.startsWith(p.agentId?.slice(0, 7) || ""), ); } catch { return undefined; } } async function test_create_agent_with_permissions(): Promise { console.log("\n--- Test: Create agent in default mode (will request permissions) ---"); // Create agent WITHOUT bypassPermissions - it will need to request permissions // Use a task that is very likely to trigger a tool use (and thus permission request) const result = await ctx.paseo( [ "-q", "agent", "run", "-d", "--provider", "claude", "--model", "claude-3-5-haiku-20241022", "--name", "Permission Test Agent", // This prompt should trigger file read or bash command, requiring permission "List the files in the current directory using ls", ], { timeout: 60000 }, ); console.log("Exit code:", result.exitCode); console.log("Stdout:", result.stdout); if (result.stderr) console.log("Stderr:", result.stderr); assert.strictEqual(result.exitCode, 0, "agent run should succeed"); const agentId = result.stdout.trim(); assert(agentId.length > 0, "Should return agent ID"); assert(agentId.match(/^[a-z0-9-]+$/), `Agent ID should be alphanumeric: ${agentId}`); console.log(`PASS: agent created with ID: ${agentId}`); return agentId; } async function test_wait_for_permission_request(agentId: string): Promise { console.log("\n--- Test: Wait for agent to request permission ---"); // Poll for permission requests with timeout const maxWait = 60000; // 60 seconds max const pollInterval = 1000; // 1 second const deadline = Date.now() + maxWait; async function pollPermission(): Promise { const result = await ctx.paseo(["permit", "ls", "--json"]); const ourPermission = findMatchingPermission(result, agentId); if (ourPermission) { console.log("Permission request detected:", ourPermission); console.log("PASS: Agent requested permission"); return true; } if (Date.now() >= deadline) return false; await sleep(pollInterval); return pollPermission(); } if (await pollPermission()) return; // If we get here, check agent status - it might have already completed const statusResult = await ctx.paseo(["inspect", agentId]); console.log("Agent status:", statusResult.stdout); // It's possible the agent completed without needing permissions (e.g., haiku might not use tools) // In that case, we should skip the permission tests if (statusResult.stdout.includes("idle") || statusResult.stdout.includes("completed")) { console.log( "SKIP: Agent completed without requiring permissions (model chose not to use tools)", ); throw new Error("SKIP_PERMISSION_TEST"); } throw new Error("Timeout waiting for agent to request permission"); } async function test_permit_ls(): Promise<{ agentShortId: string; requestId: string }> { console.log("\n--- Test: List pending permissions with permit ls ---"); const result = await ctx.paseo(["permit", "ls", "--json"]); console.log("Exit code:", result.exitCode); console.log("Stdout:", result.stdout); if (result.stderr) console.log("Stderr:", result.stderr); assert.strictEqual(result.exitCode, 0, "permit ls should succeed"); const permissions = JSON.parse(result.stdout.trim()); assert(Array.isArray(permissions), "Output should be JSON array"); assert(permissions.length > 0, "Should have at least one pending permission"); const permission = permissions[0]; assert(permission.id, "Permission should have id"); assert(permission.agentShortId, "Permission should have agentShortId"); assert(permission.name, "Permission should have tool name"); console.log(`PASS: Found ${permissions.length} pending permission(s)`); console.log(` Request ID: ${permission.id}`); console.log(` Agent: ${permission.agentShortId}`); console.log(` Tool: ${permission.name}`); return { agentShortId: permission.agentShortId, requestId: permission.id, }; } async function test_permit_allow(agentShortId: string, requestId: string): Promise { console.log("\n--- Test: Allow permission with permit allow ---"); const result = await ctx.paseo(["permit", "allow", agentShortId, requestId]); console.log("Exit code:", result.exitCode); console.log("Stdout:", result.stdout); if (result.stderr) console.log("Stderr:", result.stderr); assert.strictEqual(result.exitCode, 0, "permit allow should succeed"); // Check that output shows the permission was allowed const output = result.stdout.toLowerCase(); assert( output.includes("allowed") || output.includes(requestId.slice(0, 8).toLowerCase()), "Should confirm permission was allowed", ); console.log("PASS: Permission allowed successfully"); } async function test_agent_continues(agentId: string): Promise { console.log("\n--- Test: Verify agent continues after permission granted ---"); // Wait for agent to become idle (should continue after permission) const result = await ctx.paseo(["wait", "--timeout", "120s", agentId], { timeout: 130000 }); console.log("Exit code:", result.exitCode); console.log("Stdout:", result.stdout); if (result.stderr) console.log("Stderr:", result.stderr); assert.strictEqual(result.exitCode, 0, "agent wait should succeed after permission granted"); // Verify agent status const inspectResult = await ctx.paseo(["inspect", agentId]); console.log("Final agent status:", inspectResult.stdout); assert( inspectResult.stdout.includes("idle") || inspectResult.stdout.includes("completed"), "Agent should be idle after completing", ); console.log("PASS: Agent completed after permission was granted"); } async function test_agent_delete(agentId: string): Promise { console.log("\n--- Test: Delete agent ---"); const result = await ctx.paseo(["delete", agentId]); console.log("Exit code:", result.exitCode); console.log("Stdout:", result.stdout); if (result.stderr) console.log("Stderr:", result.stderr); assert.strictEqual(result.exitCode, 0, "agent delete should succeed"); console.log("PASS: Agent deleted successfully"); } async function main(): Promise { console.log("=== E2E Test: Permissions Workflow ===\n"); console.log("This test verifies the permission request/allow workflow."); console.log("It creates an agent that will request tool permissions.\n"); let agentId: string | undefined; try { await setup(); // Create agent that will need permissions agentId = await test_create_agent_with_permissions(); try { // Wait for permission request await test_wait_for_permission_request(agentId); // List and verify permissions const { agentShortId, requestId } = await test_permit_ls(); // Allow the permission await test_permit_allow(agentShortId, requestId); // Verify agent continues and completes await test_agent_continues(agentId); console.log("\n=== All permissions E2E tests passed! ==="); } catch (err) { if (err instanceof Error && err.message === "SKIP_PERMISSION_TEST") { console.log("\n=== Permissions test skipped (agent completed without tool use) ==="); console.log("This can happen when the model chooses not to use tools."); // Still considered a pass - the CLI works, just the model behavior varies } else { throw err; } } } catch (err) { console.error("\n=== E2E test FAILED ==="); console.error(err); process.exitCode = 1; } finally { // Always try to delete the agent if it was created if (agentId) { try { await test_agent_delete(agentId); } catch { // Ignore errors during cleanup } } await cleanup(); } } main();