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)
This commit is contained in:
phptits
2026-09-10 21:09:44 +00:00
parent 0d6cd37579
commit b1f17cf637
30 changed files with 60110 additions and 3 deletions
+185
View File
@@ -0,0 +1,185 @@
/**
* # 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");
});
});