Files
pikasTech-HWLAB/internal/hwpod/spec-registry.test.ts
T
2026-07-21 13:48:31 +02:00

152 lines
9.7 KiB
TypeScript

/*
* SPEC: PJ2026-010102 HWPOD 工具;PJ2026-010103 HWPOD 服务;
* PJ2026-01010305 71FREQ 预装 draft-2026-06-26-71freq-v03-hwpod-preinstall。
* 责任: 验证 runtime spec CRUD、重启持久化与 YAML-first 内置冻结合同。
*/
import { expect, test } from "bun:test";
import { buildHwpodTopologyFromRegistry, createHwpodHttpApp } from "./http.ts";
import { createHwpodSpecRegistry, type HwpodRuntimeSpecRecord, type HwpodRuntimeSpecStore } from "./spec-registry.ts";
test("runtime spec repository persists CRUD and freezes YAML-first built-ins", async () => {
const store = memoryStore();
const builtIn = spec("builtin-pod", "/builtin");
const registry = createHwpodSpecRegistry({ store, builtIns: [{ configRef: "config/hwpod.yaml#spec", sha256: "source-sha", document: builtIn }] });
const initial = await registry.list();
expect(initial.specs).toHaveLength(1);
expect(initial.specs[0]).toMatchObject({ hwpodId: "builtin-pod", authority: "yaml-first-builtin", mutable: false, frozen: true });
const created = await registry.create(spec("runtime-pod", "/runtime-v1"));
expect(created).toMatchObject({ status: "created", hwpodId: "runtime-pod", authority: "runtime", mutable: true, frozen: false, mutation: true });
const restarted = createHwpodSpecRegistry({ store, builtIns: [{ configRef: "config/hwpod.yaml#spec", document: builtIn }] });
expect((await restarted.get("runtime-pod")).document.spec.workspace.path).toBe("/runtime-v1");
expect((await restarted.update("runtime-pod", spec("runtime-pod", "/runtime-v2"))).status).toBe("updated");
expect((await restarted.get("runtime-pod")).document.spec.workspace.path).toBe("/runtime-v2");
expect((await restarted.delete("runtime-pod")).status).toBe("deleted");
await expect(restarted.get("runtime-pod")).rejects.toMatchObject({ code: "hwpod_spec_not_found" });
await expect(restarted.create(spec("builtin-pod", "/shadow"))).rejects.toMatchObject({ code: "hwpod_spec_frozen", details: { mutation: false } });
await expect(restarted.update("builtin-pod", builtIn)).rejects.toMatchObject({ code: "hwpod_spec_frozen" });
await expect(restarted.delete("builtin-pod")).rejects.toMatchObject({ code: "hwpod_spec_frozen" });
expect((await restarted.get("builtin-pod")).document.spec.workspace.path).toBe("/builtin");
});
test("L1 HWPOD HTTP exposes runtime CRUD and typed frozen errors", async () => {
const registry = createHwpodSpecRegistry({ store: memoryStore(), builtIns: [{ configRef: "config/hwpod.yaml#spec", document: spec("builtin-pod", "/builtin") }] });
const temporal = { close: async () => {}, start: async () => ({}), describe: async () => ({}), result: async () => ({}) };
const app = createHwpodHttpApp({ nodeOpsApiUrl: "http://native-hwpod.test", temporal, specRegistry: registry, nodeRegistry: nodeRegistry() });
expect(await request(app, "GET", "/health/ready")).toMatchObject({ status: 200, body: { ok: true, specRegistry: "ready", specCount: 1, nodeRegistry: "ready" } });
const created = await request(app, "POST", "/v1/hwpod/specs", { document: spec("runtime-http", "/http-v1") });
expect(created).toMatchObject({ status: 201, body: { ok: true, status: "created", hwpodId: "runtime-http" } });
expect(await request(app, "GET", "/v1/hwpod/specs/runtime-http")).toMatchObject({ status: 200, body: { document: { spec: { workspace: { path: "/http-v1" } } } } });
expect(await request(app, "PUT", "/v1/hwpod/specs/runtime-http", { document: spec("runtime-http", "/http-v2") })).toMatchObject({ status: 200, body: { status: "updated" } });
expect(await request(app, "DELETE", "/v1/hwpod/specs/runtime-http")).toMatchObject({ status: 200, body: { status: "deleted", mutation: true } });
const frozen = await request(app, "DELETE", "/v1/hwpod/specs/builtin-pod");
expect(frozen).toMatchObject({ status: 409, body: { ok: false, error: { code: "hwpod_spec_frozen", authority: "yaml-first-builtin", mutation: false } } });
expect((await request(app, "GET", "/v1/hwpod/specs/builtin-pod")).body.document.spec.workspace.path).toBe("/builtin");
await app.close();
});
test("L1 HWPOD HTTP owns Python node metadata and download routes", async () => {
const registry = createHwpodSpecRegistry({ store: memoryStore() });
const temporal = { close: async () => {}, start: async () => ({}), describe: async () => ({}), result: async () => ({}) };
const app = createHwpodHttpApp({
nodeOpsApiUrl: "http://native-hwpod.test",
temporal,
specRegistry: registry,
nodeRegistry: nodeRegistry(),
env: { ...process.env, HWPOD_PUBLIC_BASE_URL: "https://lab-dev-hwpod.hwpod.com" },
});
const metadata = await request(app, "GET", "/v1/hwlab-node/update?platform=windows&channel=stable");
expect(metadata).toMatchObject({ status: 200, body: { ok: true, contractVersion: "hwlab-node-update-v1", artifact: { fileName: "hwlab-node.py" } } });
expect(metadata.body.artifact.version).toMatch(/^\d+\.\d+\.\d+$/u);
expect(metadata.body.artifact.sizeBytes).toBeGreaterThan(0);
expect(metadata.body.artifact.sha256).toMatch(/^[a-f0-9]{64}$/u);
expect(metadata.body.artifact.downloadUrl).toBe("https://lab-dev-hwpod.hwpod.com/v1/hwlab-node/download/hwlab-node.py");
const download = await app.fetch(new Request("http://hwpod.test/v1/hwlab-node/download/hwlab-node.py"));
expect(download.status).toBe(200);
expect(download.headers.get("content-type")).toContain("text/x-python");
expect(download.headers.get("x-hwlab-node-sha256")).toBe(metadata.body.artifact.sha256);
expect(await download.text()).toContain(`APP_VERSION = "${metadata.body.artifact.version}"`);
await app.close();
});
test("L0 HWPOD topology is built from the local registry without Cloud API", async () => {
const registry = createHwpodSpecRegistry({
store: memoryStore(),
builtIns: [{ configRef: "config/hwpod.yaml#spec", document: spec("builtin-pod", "/builtin") }],
});
await registry.create(spec("runtime-pod", "/runtime"));
const topology = await buildHwpodTopologyFromRegistry(registry, { resource: "device", q: "runtime-pod" });
expect(topology).toMatchObject({
ok: true,
contractVersion: "hwpod-topology-v1",
resource: "device",
service: { connectedNodeCount: 0, websocketMode: "hwpod-native-api-no-node-registry" },
summary: { deviceCount: 2, onlineNodeCount: 0 },
items: [{ hwpodId: "runtime-pod", nodeId: "node-test", status: "offline", source: { authority: "runtime" } }],
});
expect(topology.items[0].blockers).toContainEqual(expect.objectContaining({ code: "hwpod_node_offline" }));
const temporal = { close: async () => {}, start: async () => ({}), describe: async () => ({}), result: async () => ({}) };
const app = createHwpodHttpApp({ nodeOpsApiUrl: "http://native-hwpod.test", temporal, specRegistry: registry, nodeRegistry: nodeRegistry() });
const response = await request(app, "GET", "/v1/hwpod/topology?resource=node");
expect(response).toMatchObject({ status: 200, body: { ok: true, resource: "node", items: [{ nodeId: "node-test", status: "offline" }] } });
await app.close();
});
test("L1 Web operation compiles a registry spec and submits native node-ops through Temporal", async () => {
const registry = createHwpodSpecRegistry({ store: memoryStore(), builtIns: [{ configRef: "config/hwpod.yaml#spec", document: spec("web-pod", "/workspace") }] });
let submitted: any = null;
const temporal = {
close: async () => {},
start: async (input: unknown) => { submitted = input; return { workflowId: "hwpod-operation-test", workflowRunId: "run-test" }; },
describe: async () => ({}),
result: async () => ({}),
};
const app = createHwpodHttpApp({ nodeOpsApiUrl: "http://127.0.0.1:6681", temporal, specRegistry: registry, nodeRegistry: nodeRegistry() });
const response = await request(app, "POST", "/v1/hwpod/operations", { hwpodId: "web-pod", intent: "workspace.ls", args: { path: "." } });
expect(response).toMatchObject({ status: 202, body: { ok: true, status: "accepted", hwpodId: "web-pod", authority: "hwpod-temporal" } });
expect(submitted).toMatchObject({ runtimeApiUrl: "http://127.0.0.1:6681", plan: { hwpodId: "web-pod", nodeId: "node-test", intent: "workspace.ls", ops: [{ op: "workspace.ls", args: { path: ".", workspacePath: "/workspace" } }] } });
await app.close();
});
async function request(app: any, method: string, pathname: string, body?: unknown) {
const response = await app.fetch(new Request(`http://hwpod.test${pathname}`, { method, headers: { "content-type": "application/json" }, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }));
return { status: response.status, body: await response.json() };
}
function memoryStore(): HwpodRuntimeSpecStore {
const records = new Map<string, HwpodRuntimeSpecRecord>();
return {
list: async () => [...records.values()],
get: async (id) => records.get(id) ?? null,
create: async (record) => records.has(record.hwpodId) ? false : (records.set(record.hwpodId, structuredClone(record)), true),
update: async (id, document, updatedAt) => {
const existing = records.get(id);
if (!existing) return null;
const record = { ...existing, document: structuredClone(document), updatedAt };
records.set(id, record);
return record;
},
delete: async (id) => { const record = records.get(id) ?? null; records.delete(id); return record; },
};
}
function nodeRegistry() {
return {
describe: () => ({ mode: "hwpod-native-api-websocket", connectedCount: 0, nodes: [] }),
lookup: async () => null,
dispatch: async () => ({ ok: true, status: "completed", results: [] }),
};
}
function spec(name: string, workspacePath: string) { return { apiVersion: "hwlab.dev/v0alpha1", kind: "Hwpod", metadata: { name }, spec: { targetDevice: { board: "test" }, workspace: { path: workspacePath }, debugProbe: { type: "test" }, ioProbe: { type: "test" }, nodeBinding: { nodeId: "node-test" } } }; }