import { describe, expect, it } from "vitest"; // 炼境分发给 DSH 的会话级插件(src-tauri/resources/dsh/):用假的 DSH ctx 验证挂载边界与「永不抛错」约束。 // 项目只在 Windows 上运行,路径断言按 Windows 语义书写。 type Roots = Record; interface MountOptions { transport: string; serverName: string; url: string; headers: Record; } interface FakeAgent { session: { header: { cwd: string; origin?: string } }; ctx: { plugin: (client: unknown, options: MountOptions) => { dispose: () => void } }; } interface ToolView { visible: Set; restrictableNames: Set; } type Handler = (event: { agent: FakeAgent }) => unknown; interface PluginModule { name: string; findProjectId: (roots: Roots | undefined, cwd: unknown) => string | undefined; readAuthorization: (shim: string, projectId: string, timeoutMs: number) => Promise; apply: (ctx: unknown, config: unknown) => Promise; } const pluginUrl = new URL("../../src-tauri/resources/dsh/lianjing-session-mcp.v1.mjs", import.meta.url).href; const loadPlugin = async (): Promise => (await import(/* @vite-ignore */ pluginUrl)) as PluginModule; const fixturePath = (name: string) => decodeURIComponent(new URL(`./__fixtures__/${name}`, import.meta.url).pathname) .replace(/^\/([A-Za-z]:)/, "$1") .replace(/\//g, "\\"); const roots: Roots = { "e:\\code-project": null, "e:\\code-project\\demo": "p 1" }; const config = { port: 27190, shim: "Z:\\lianjing-test-missing\\lianjing-mcp-header.cmd", roots }; const fakeClient = { Config: (options: MountOptions) => options, apply: () => undefined }; function fakeDsh( options: { profile?: string; importFails?: boolean; agentsApi?: boolean; toolView?: (agent: FakeAgent) => ToolView } = {}, ) { const handlers = new Map(); const ctx = { get: (key: string) => (key === "profileContext" && options.profile ? { name: options.profile } : undefined), agents: options.agentsApi === false ? undefined : { list: (): FakeAgent[] => [] }, loader: { import: async () => { if (options.importFails) throw new Error("module not found"); return fakeClient; }, }, tools: options.toolView ? { view: options.toolView } : undefined, on: (event: string, handler: Handler) => handlers.set(event, [...(handlers.get(event) ?? []), handler]), effect: () => undefined, }; const emit = async (event: string, agent: FakeAgent) => { for (const handler of handlers.get(event) ?? []) await handler({ agent }); }; return { ctx, handlers, emit }; } function fakeAgent( cwd: string, mounts: MountOptions[], options: { disposed?: string[]; throws?: boolean; origin?: string } = {}, ): FakeAgent { return { session: { header: { cwd, origin: options.origin } }, ctx: { plugin: (_client, mountOptions) => { if (options.throws) throw new Error("serverName already in use"); mounts.push(mountOptions); return { dispose: () => options.disposed?.push(mountOptions.url) }; }, }, }; } describe("DSH 会话级插件", () => { it("按会话工作目录逐级向上匹配,null 停止哨兵与未登记目录都不命中", async () => { const plugin = await loadPlugin(); expect(plugin.name).toBe("lianjing-session-mcp"); expect(plugin.findProjectId(roots, "E:\\code-project\\demo\\apps\\web")).toBe("p 1"); expect(plugin.findProjectId(roots, "E:\\Code-Project\\Demo\\")).toBe("p 1"); expect(plugin.findProjectId(roots, "E:\\code-project\\other")).toBeUndefined(); expect(plugin.findProjectId(roots, "D:\\elsewhere")).toBeUndefined(); expect(plugin.findProjectId(roots, "")).toBeUndefined(); expect(plugin.findProjectId(undefined, "E:\\code-project\\demo")).toBeUndefined(); // 分层:控制面(null)位于已登记项目之下时,命中即停,不越过它继承上级项目 const layered: Roots = { "e:\\top": "p-top", "e:\\top\\cp": null }; expect(plugin.findProjectId(layered, "E:\\top\\cp\\other")).toBeUndefined(); expect(plugin.findProjectId(layered, "E:\\top\\cp")).toBeUndefined(); expect(plugin.findProjectId(layered, "E:\\top\\x")).toBe("p-top"); }); it("只给命中项目的会话挂项目专用地址;取不到认证时仍挂载但不带认证头", async () => { const plugin = await loadPlugin(); const dsh = fakeDsh({ profile: "desktop" }); await plugin.apply(dsh.ctx, config); const mounts: MountOptions[] = []; await dsh.emit("agent/created", fakeAgent("E:\\code-project\\demo\\src", mounts)); await dsh.emit("agent/created", fakeAgent("E:\\code-project\\other", mounts)); expect(mounts).toHaveLength(1); expect(mounts[0].serverName).toBe("lian-jing"); expect(mounts[0].transport).toBe("streamable-http"); expect(mounts[0].url).toBe("http://127.0.0.1:27190/mcp/projects/p%201"); expect(mounts[0].headers).toEqual({}); }); it("取认证失败或超时都返回 undefined,且受超时上限约束", async () => { const plugin = await loadPlugin(); await expect(plugin.readAuthorization(fixturePath("shim-fail.cmd"), "p1", 3000)).resolves.toBeUndefined(); const started = Date.now(); await expect(plugin.readAuthorization(fixturePath("shim-hang.cmd"), "p1", 300)).resolves.toBeUndefined(); expect(Date.now() - started).toBeLessThan(3000); }); it("子代理:带工具限制或无法判断时不挂载,未受限时照常挂载", async () => { const plugin = await loadPlugin(); const allTools = new Set(["job_output", "mcp__lian-jing__list_all_projects"]); const restricted: ToolView = { visible: new Set(["job_output"]), restrictableNames: allTools }; const unrestricted: ToolView = { visible: new Set(allTools), restrictableNames: allTools }; const cwd = "E:\\code-project\\demo"; for (const [view, expected] of [ [restricted, 0], [unrestricted, 1], ] as const) { const dsh = fakeDsh({ profile: "desktop", toolView: () => view }); await plugin.apply(dsh.ctx, config); const mounts: MountOptions[] = []; await dsh.emit("agent/created", fakeAgent(cwd, mounts, { origin: "subagent" })); expect(mounts).toHaveLength(expected); } const noToolsApi = fakeDsh({ profile: "desktop" }); await plugin.apply(noToolsApi.ctx, config); const mounts: MountOptions[] = []; await noToolsApi.emit("agent/created", fakeAgent(cwd, mounts, { origin: "subagent" })); await noToolsApi.emit("agent/created", fakeAgent(cwd, mounts)); expect(mounts).toHaveLength(1); }); it("会话销毁时释放挂载", async () => { const plugin = await loadPlugin(); const dsh = fakeDsh({ profile: "web" }); await plugin.apply(dsh.ctx, config); const mounts: MountOptions[] = []; const disposed: string[] = []; const agent = fakeAgent("E:\\code-project\\demo", mounts, { disposed }); await dsh.emit("agent/created", agent); await dsh.emit("agent/disposed", agent); expect(disposed).toEqual(["http://127.0.0.1:27190/mcp/projects/p%201"]); }); it("headless 交给全局行处理,插件不注册任何监听", async () => { const plugin = await loadPlugin(); const dsh = fakeDsh({ profile: "headless" }); await plugin.apply(dsh.ctx, config); expect(dsh.handlers.size).toBe(0); }); it("DSH 接口缺失或 mcp-client 加载失败时静默降级,不抛错", async () => { const plugin = await loadPlugin(); for (const options of [{ importFails: true }, { agentsApi: false }]) { const dsh = fakeDsh({ profile: "desktop", ...options }); await expect(plugin.apply(dsh.ctx, config)).resolves.toBeUndefined(); expect(dsh.handlers.has("agent/created")).toBe(false); } }); it("挂载异常不外抛:agent/created 是串行事件,监听器失败会让会话创建失败", async () => { const plugin = await loadPlugin(); const dsh = fakeDsh({ profile: "desktop" }); await plugin.apply(dsh.ctx, config); const mounts: MountOptions[] = []; await expect( dsh.emit("agent/created", fakeAgent("E:\\code-project\\demo", mounts, { throws: true })), ).resolves.toBeUndefined(); }); });