/** * # DevOS v2 Action Runtime 单元测试(docs 03 §98) * 覆盖:注册 FAIL FAST / 输入验证 / 授权 / 执行成功 / 失败审计 / Discovery。 */ import { describe, test, expect, beforeEach } from "bun:test"; import { z } from "zod"; import { ActionRegistry, defineAction, type ActionDefinition } from "../apps/api/src/v2/registry/action-registry"; import { ResourceRegistry } from "../apps/api/src/v2/registry/resource-registry"; import { ActionRuntime, type ActionRuntimePorts } from "../apps/api/src/v2/runtime/action-runtime"; import { actionRegistry as globalRegistry } from "../apps/api/src/v2/registry/action-registry"; import type { ActorContext, ActionExecutionRecord } from "../apps/api/src/v2/foundation/types"; /** # 测试用 runtime 工厂(采集端口调用) **/ function makeRuntime(opts: { allowed?: boolean } = {}) { globalRegistry.clear(); const registry = globalRegistry; const executed: string[] = []; const audits: Record[] = []; const events: { eventType: string; payload: Record }[] = []; const executions: ActionExecutionRecord[] = []; const ports: ActionRuntimePorts = { checkPermission: async () => opts.allowed ?? true, audit: async (r) => { audits.push(r as unknown as Record); }, publishEvent: async (e) => { events.push({ eventType: e.eventType, payload: e.payload }); }, saveExecution: async (r) => { executions.push(r); }, }; const runtime = new ActionRuntime(ports); const actor: ActorContext = { type: "user", id: "user_1", organizationId: "ws_1" }; return { registry, runtime, actor, executed, audits, events, executions, ports }; } /** # 示例动作:devops.website.scan(对齐 docs 03 §76) **/ function scanAction(impl?: ActionDefinition["execute"]) { return defineAction({ id: "devops.website.scan", resource: "devops.website", name: "Scan website", description: "Scan a website for issues", mode: "async", permission: "devops.website.scan", inputSchema: z.object({ websiteId: z.string(), depth: z.number().int().min(1).max(10).default(3) }), outputSchema: z.object({ executionId: z.string(), pagesScanned: z.number() }), execute: impl ?? (async () => ({ executionId: "exec_x", pagesScanned: 42 })), }) as unknown as ActionDefinition; } const actor: ActorContext = { type: "user", id: "user_1", organizationId: "ws_1" }; describe("v2 ActionRegistry", () => { test("重复注册 FAIL FAST", () => { const registry = new ActionRegistry(); const action = scanAction(); registry.register(action, "test"); expect(() => registry.register(action, "test")).toThrow(/duplicate action id/); }); test("非命名空间 id 拒绝注册", () => { const registry = new ActionRegistry(); const bad = scanAction(); (bad as { id: string }).id = "scan"; expect(() => registry.register(bad, "test")).toThrow(/namespaced/); }); test("list 返回已注册动作", () => { const registry = new ActionRegistry(); registry.register(scanAction(), "test"); expect(registry.list().map((a) => a.id)).toContain("devops.website.scan"); }); }); describe("v2 ResourceRegistry", () => { test("注册 + 发现", () => { const registry = new ResourceRegistry(); registry.register({ type: "devops.website", label: "Website", actionIds: ["devops.website.scan"], ownership: { organization: true }, }); expect(registry.has("devops.website")).toBe(true); expect(registry.get("devops.website")?.actionIds).toContain("devops.website.scan"); }); test("重复类型 FAIL FAST", () => { const registry = new ResourceRegistry(); const def = { type: "devops.website", label: "W", actionIds: [] }; registry.register(def as never); expect(() => registry.register(def as never)).toThrow(/duplicate resource type/); }); }); describe("v2 ActionRuntime", () => { let env: ReturnType; beforeEach(() => { env = makeRuntime({ allowed: true }); env.registry.register(scanAction(), "test"); }); test("执行成功:验证 → 授权 → handler → execution 落库 → 审计 → 事件", async () => { const result = await env.runtime.execute( "devops.website.scan", { websiteId: "website_123" }, actor, { source: "user", organizationId: "ws_1" }, ); expect(result.success).toBe(true); expect(result.data).toEqual({ executionId: "exec_x", pagesScanned: 42 }); expect(result.executionId.startsWith("exec_")).toBe(true); // depth 默认值 3 由 zod schema 应用 expect(env.executions).toHaveLength(1); expect(env.executions[0].status).toBe("completed"); expect(env.executions[0].resourceType).toBe("devops.website"); // 审计 expect(env.audits).toHaveLength(1); expect(env.audits[0].action).toBe("devops.website.scan"); // 事件 started + completed expect(env.events.map((e) => e.eventType)).toEqual(["action.started", "action.completed"]); }); test("输入验证失败:ACTION 不执行、execution failed、不抛异常", async () => { const result = await env.runtime.execute( "devops.website.scan", { websiteId: "website_123", depth: 99 }, actor, { organizationId: "ws_1" }, ); expect(result.success).toBe(false); expect(result.error?.code).toBe("VALIDATION_FAILED"); expect(env.executions[0].status).toBe("failed"); expect(env.audits[0].status).toBe("FAILURE"); expect(env.events.some((e) => e.eventType === "action.failed")).toBe(true); }); test("授权失败:ACTION_FORBIDDEN,handler 未执行", async () => { const denied = makeRuntime({ allowed: false }); denied.registry.register(scanAction(), "test"); const result = await denied.runtime.execute( "devops.website.scan", { websiteId: "w1" }, actor, { organizationId: "ws_1" }, ); expect(result.success).toBe(false); expect(result.error?.code).toBe("ACTION_FORBIDDEN"); }); test("未知 Action:ACTION_NOT_FOUND", async () => { const result = await env.runtime.execute("no.such.action", {}, actor, {}); expect(result.success).toBe(false); expect(result.error?.code).toBe("ACTION_NOT_FOUND"); }); test("handler 抛错:映射 ACTION_FAILED + 失败审计", async () => { const registry = globalRegistry; registry.clear(); registry.register( defineAction({ id: "devops.website.scan", resource: "devops.website", name: "scan", description: "scan", inputSchema: z.object({ websiteId: z.string() }), execute: async () => { throw new Error("boom"); }, }) as unknown as ActionDefinition, "test", ); const result = await env.runtime.execute("devops.website.scan", { websiteId: "w1" }, actor, { organizationId: "ws_1", }); console.log("DEBUG registry ids:", env.registry.list().map((a) => a.id), "| result:", JSON.stringify(result).slice(0, 200)); expect(result.success).toBe(false); expect(result.error?.message).toBe("boom"); expect(env.executions[0].error?.message).toBe("boom"); }); });