import { createRoute, z } from "@hono/zod-openapi"; import { apiResponse, CAPABILITY_IDS, capabilitySpec } from "@umt/contracts"; import { defineTask, TASK_NAMES } from "@umt/tasks"; import { describe, expect, it } from "vitest"; import { buildTaskRegistry, collectOutboxRoutes, DeferredTaskEngine } from "../src/context.ts"; import { createApp } from "../src/http/app.ts"; import { requireScope } from "../src/http/auth.ts"; import { EventBus } from "../src/http/sse.ts"; import { SERVER_MODULES } from "../src/modules/index.ts"; import type { ServerContext, ServerModule } from "../src/modules/types.ts"; import { DEV_KEY, json, memoryLogger } from "./helpers.ts"; // register* 阶段模块只保存 ctx 引用,这里不需要真实依赖 const fakeCtx = { role: "all" } as unknown as ServerContext; const pingRoute = createRoute({ method: "get", path: "/test-ping", tags: ["test"], responses: { 200: { description: "ok", content: { "application/json": { schema: apiResponse(z.object({ who: z.string() })) } } }, }, }); const pingModule: ServerModule = { name: "test-ping", registerRoutes(app) { app.use("/test-ping", requireScope("admin")); app.openapi(pingRoute, (c) => c.json({ ok: true as const, data: { who: c.get("principal").name } }, 200)); }, registerTasks(registry) { registry.register( defineTask({ name: "ingestFeed", version: 1, inputSchema: z.object({ feedSourceId: z.string() }), handler: async () => "real", }), ); }, outboxRoutes: () => ({ "release.resolved": () => ({ task: "evaluateAcquisition", input: {} }) }), }; describe("ServerModule 骨架", () => { it("清单里是四个 W2 模块,名字唯一", () => { expect(SERVER_MODULES.map((m) => m.name)).toEqual([ "auth-settings", "sources-identity", "acquisition-organize", "agent", ]); }); it("模块路由挂在 /api/v1 下,经过鉴权与 scope 检查,并进入 OpenAPI 文档", async () => { const { logger } = memoryLogger(); const app = createApp({ config: { NODE_ENV: "development", DEV_ADMIN_KEY: DEV_KEY }, logger, bus: new EventBus(5), role: "all", version: "0.0.0-test", modules: [pingModule], ctx: fakeCtx, }); expect((await app.request("/api/v1/test-ping")).status).toBe(401); const ok = await app.request("/api/v1/test-ping", { headers: { authorization: `Bearer ${DEV_KEY}` } }); expect(ok.status).toBe(200); expect((await json(ok)).data.who).toBe("dev-admin"); const doc = await json(await app.request("/api/openapi.json")); expect(doc.paths["/api/v1/test-ping"]).toBeDefined(); // 未知路径仍是 JSON 404 expect((await app.request("/api/v1/nope", { headers: { authorization: `Bearer ${DEV_KEY}` } })).status).toBe(404); }); it("任务注册表:模块任务优先,其余 §3.1-8 任务由空壳兜底;重复注册报错", async () => { const reg = buildTaskRegistry([...SERVER_MODULES, pingModule], fakeCtx); expect(reg.names()).toEqual([...TASK_NAMES].sort()); const ingest = reg.latest("ingestFeed"); expect(await ingest.handler({ input: {} } as never)).toBe("real"); expect(() => buildTaskRegistry([pingModule, { ...pingModule, name: "dup" }], fakeCtx)).toThrow(/重复注册/); }); it("outbox 路由合并;同一事件被两个模块认领报错", () => { expect(Object.keys(collectOutboxRoutes([...SERVER_MODULES, pingModule], fakeCtx))).toEqual(["release.resolved"]); expect(() => collectOutboxRoutes([pingModule, { ...pingModule, name: "dup" }], fakeCtx)).toThrow(/重复认领/); }); it("引擎就绪前调用给出明确错误", async () => { const e = new DeferredTaskEngine(); expect(e.ready).toBe(false); expect(() => e.get("x")).toThrow(/尚未就绪/); await e.stop(); }); it("能力规格取自 contracts,可按模块注册", () => { expect(CAPABILITY_IDS.map((id) => capabilitySpec(id).id)).toEqual([...CAPABILITY_IDS]); }); });