Files
UHD_Media_Tool/apps/server/test/modules.test.ts
T
rootandClaude Opus 5.5 9858b8494d feat(server): ServerModule skeleton with shared ServerContext, worker runs engine + outbox relay (B6)
modules/{auth-settings,sources-identity,acquisition-organize,agent} placeholders listed in
SERVER_MODULES; ServerModule registers routes (under /api/v1, authenticated), tasks,
outbox routes, capabilities and background loops. bootstrap() builds ServerContext (db,
config, logger, events, engine, outbox writer, secrets placeholder, settings reader,
capability registry), fills unregistered tasks with stubs and rejects duplicate task or
outbox ownership. worker starts the OpenWorkflow worker, OutboxRelay and module start().
--role migrate also migrates the engine schema; api/worker connect without migrating.
tasks: TaskRegistry.has, migrateEngineSchema.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-09-27 19:30:55 +08:00

99 lines
3.9 KiB
TypeScript

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]);
});
});