summaryrefslogtreecommitdiffhomepage
path: root/packages/api/tests
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-05-19 23:20:41 +0900
committerAdam Malczewski <[email protected]>2026-05-19 23:20:41 +0900
commita38d5b1279db6f9de5228c173019fc2ac08daec3 (patch)
tree32c3a535d0b74872ef952b4a44d4d5ba2ec9d638 /packages/api/tests
parent0ae805b28b5160b8d9fb43635fa172961f6550cc (diff)
downloaddispatch-a38d5b1279db6f9de5228c173019fc2ac08daec3.tar.gz
dispatch-a38d5b1279db6f9de5228c173019fc2ac08daec3.zip
feat: Phase 2 — shell permissions, tree-sitter analysis, permission UI
Permission engine: - Rule-based engine: wildcard matching, last-match-wins, reject cascade - PermissionService with pending/approved state, PermissionChecker interface - dispatch.yaml config loader with per-permission pattern rules Shell tool: - run_shell tool with child_process spawn, timeout, streaming output - Tree-sitter static analysis (web-tree-sitter + tree-sitter-bash WASM) - BashArity command normalization for 'always allow' patterns - FILE_COMMANDS set: rm, cp, mv, mkdir, ls, find, grep, cat, etc. Agent loop refactored: - Removed maxSteps, manual step loop with tool execution - Permission checks on shell commands (external_directory only) - Permission checks on file tools outside workspace boundary - Symlink bypass fix (realpathSync), .. false positive fix - Shell output streaming via Promise.race + setImmediate polling API layer: - PermissionManager wraps PermissionService, broadcasts via WebSocket - WebSocket handles permission-reply messages from frontend - Config loaded from dispatch.yaml, converted to ruleset Frontend: - Permission prompt modal (native dialog, focus trap, ARIA) - Always-allow confirmation flow with pattern preview - Shell output display (live streaming + final parsed result) - Permission log panel (fixed bottom-right overlay) - Exit code badge (green 0, red non-zero) 134 tests, typecheck clean on all 3 packages
Diffstat (limited to 'packages/api/tests')
-rw-r--r--packages/api/tests/agent-manager.test.ts23
-rw-r--r--packages/api/tests/routes.test.ts23
2 files changed, 46 insertions, 0 deletions
diff --git a/packages/api/tests/agent-manager.test.ts b/packages/api/tests/agent-manager.test.ts
index 56cb818..2111b0e 100644
--- a/packages/api/tests/agent-manager.test.ts
+++ b/packages/api/tests/agent-manager.test.ts
@@ -18,6 +18,15 @@ vi.mock("@dispatch/core", () => ({
yield { type: "status", status: "idle" } as const;
}
},
+ PermissionService: class MockPermissionService {
+ ask(_request: unknown, _rulesets: unknown[]) {
+ return Promise.resolve("once");
+ }
+ reply(_id: string, _reply: unknown) {}
+ getPending() {
+ return [];
+ }
+ },
createReadFileTool(_wd: string): ToolDefinition {
return {
name: "read_file",
@@ -42,6 +51,20 @@ vi.mock("@dispatch/core", () => ({
execute: async () => ["file1.ts"],
};
},
+ createRunShellTool(_wd: string): ToolDefinition {
+ return {
+ name: "run_shell",
+ description: "run shell command",
+ parameters: { _type: "z.ZodObject", shape: {} } as unknown as ToolDefinition["parameters"],
+ execute: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
+ };
+ },
+ loadConfig(_dir: string) {
+ return { permissions: {} };
+ },
+ configToRuleset(_config: unknown) {
+ return [];
+ },
}));
// Import after mock is defined (Vitest hoists vi.mock automatically)
diff --git a/packages/api/tests/routes.test.ts b/packages/api/tests/routes.test.ts
index 9f852ee..87ff436 100644
--- a/packages/api/tests/routes.test.ts
+++ b/packages/api/tests/routes.test.ts
@@ -19,6 +19,15 @@ vi.mock("@dispatch/core", () => ({
yield { type: "status", status: "idle" } as const;
}
},
+ PermissionService: class MockPermissionService {
+ ask(_request: unknown, _rulesets: unknown[]) {
+ return Promise.resolve("once");
+ }
+ reply(_id: string, _reply: unknown) {}
+ getPending() {
+ return [];
+ }
+ },
createReadFileTool(_wd: string): ToolDefinition {
return {
name: "read_file",
@@ -43,6 +52,20 @@ vi.mock("@dispatch/core", () => ({
execute: async () => ["file1.ts"],
};
},
+ createRunShellTool(_wd: string): ToolDefinition {
+ return {
+ name: "run_shell",
+ description: "run shell command",
+ parameters: { _type: "z.ZodObject", shape: {} } as unknown as ToolDefinition["parameters"],
+ execute: async () => ({ stdout: "", stderr: "", exitCode: 0 }),
+ };
+ },
+ loadConfig(_dir: string) {
+ return { permissions: {} };
+ },
+ configToRuleset(_config: unknown) {
+ return [];
+ },
}));
const { app } = await import("../src/app.js");