Files
devos/tests/v2-action-runtime.test.ts
T
phptits b1f17cf637 v2 platform foundation (docs 01/02/03): Action Runtime + Registry + unified API
- v2 foundation: ActionResult/DevError/ActorContext/ResourceRef/Execution types
- ActionRegistry + ResourceRegistry (namespaced types, FAIL FAST on duplicates)
- ActionRuntime pipeline: validate -> authorize -> middleware -> handler ->
  output validation -> audit -> event -> execution persisted (devos_action_executions)
- Legacy bridge: v1 services registered as devops.*/core.* resources & actions (47 actions, 9 resources)
- Unified Action API: POST /api/v2/actions/:actionId, GET discovery (actions/resources)
- 10 v2 unit tests (registry fail-fast, validation, authz, failure audit, events)
2026-09-10 21:09:44 +00:00

186 lines
7.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* # 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<string, unknown>[] = [];
const events: { eventType: string; payload: Record<string, unknown> }[] = [];
const executions: ActionExecutionRecord[] = [];
const ports: ActionRuntimePorts = {
checkPermission: async () => opts.allowed ?? true,
audit: async (r) => {
audits.push(r as unknown as Record<string, unknown>);
},
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<never, never>["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<never, never>;
}
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<typeof makeRuntime>;
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<never, never>,
"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");
});
});