|
| 1 | +import { describe, expect, it, vi } from "vitest" |
| 2 | + |
| 3 | +import { HostCommandDispatcher } from "../dispatcher.js" |
| 4 | +import { validateHostRoots } from "../roots.js" |
| 5 | +import { VaultSecretStorage, type VaultBackend } from "../security.js" |
| 6 | +import { HostTransport } from "../transport.js" |
| 7 | + |
| 8 | +describe("host security and transport", () => { |
| 9 | + it("requires explicit absolute roots", () => { |
| 10 | + expect(() => |
| 11 | + validateHostRoots({ |
| 12 | + extensionRoot: "relative", |
| 13 | + workspaceRoot: "/workspace", |
| 14 | + storageRoot: "/storage", |
| 15 | + appRoot: "/app", |
| 16 | + }), |
| 17 | + ).toThrow("extensionRoot") |
| 18 | + expect( |
| 19 | + validateHostRoots({ |
| 20 | + extensionRoot: "/extension", |
| 21 | + workspaceRoot: "/workspace", |
| 22 | + storageRoot: "/storage", |
| 23 | + appRoot: "/app", |
| 24 | + }), |
| 25 | + ).toEqual({ |
| 26 | + extensionRoot: "/extension", |
| 27 | + workspaceRoot: "/workspace", |
| 28 | + storageRoot: "/storage", |
| 29 | + appRoot: "/app", |
| 30 | + }) |
| 31 | + }) |
| 32 | + |
| 33 | + it("round trips secrets only through the injected vault", async () => { |
| 34 | + const values = new Map<string, string>() |
| 35 | + const backend: VaultBackend = { |
| 36 | + get: vi.fn(async (key) => values.get(key)), |
| 37 | + store: vi.fn(async (key, value) => void values.set(key, value)), |
| 38 | + delete: vi.fn(async (key) => void values.delete(key)), |
| 39 | + } |
| 40 | + const storage = new VaultSecretStorage(backend) |
| 41 | + const changes: string[] = [] |
| 42 | + storage.onDidChange(({ key }) => changes.push(key)) |
| 43 | + await storage.store("api-key", "secret") |
| 44 | + await expect(storage.get("api-key")).resolves.toBe("secret") |
| 45 | + await storage.delete("api-key") |
| 46 | + expect(changes).toEqual(["api-key", "api-key"]) |
| 47 | + expect(backend.store).toHaveBeenCalledWith("api-key", "secret") |
| 48 | + }) |
| 49 | + |
| 50 | + it("uses one monotonic sequence for ACK and DONE", async () => { |
| 51 | + const sent: unknown[] = [] |
| 52 | + const transport = new HostTransport("host-1", async (message) => void sent.push(message)) |
| 53 | + const api = { |
| 54 | + startHeadlessTask: vi.fn().mockResolvedValue({ taskId: "root", rootTaskId: "root" }), |
| 55 | + } as never |
| 56 | + const dispatcher = new HostCommandDispatcher(api, transport, "/workspace") |
| 57 | + await dispatcher.dispatch({ v: 1, id: "cmd-1", type: "task.start", workspace: "/workspace", prompt: "hello" }) |
| 58 | + expect(sent).toMatchObject([ |
| 59 | + { seq: 1, type: "command.ack", commandId: "cmd-1" }, |
| 60 | + { seq: 2, type: "command.done", commandId: "cmd-1", data: { commandType: "task.start" } }, |
| 61 | + ]) |
| 62 | + }) |
| 63 | + |
| 64 | + it("rejects workspace identity changes after ACK", async () => { |
| 65 | + const sent: unknown[] = [] |
| 66 | + const transport = new HostTransport("host-1", async (message) => void sent.push(message)) |
| 67 | + const dispatcher = new HostCommandDispatcher({} as never, transport, "/workspace") |
| 68 | + await dispatcher.dispatch({ v: 1, id: "cmd-1", type: "task.start", workspace: "/other", prompt: "hello" }) |
| 69 | + expect(sent).toMatchObject([ |
| 70 | + { seq: 1, type: "command.ack" }, |
| 71 | + { seq: 2, type: "command.error", error: { code: "task_failed" } }, |
| 72 | + ]) |
| 73 | + }) |
| 74 | +}) |
0 commit comments