feat: complete TASK-WP0-02 contract baseline

This commit is contained in:
suyx
2026-07-27 17:49:24 +08:00
parent ae20eaf01f
commit dbe3e73b91
28 changed files with 2540 additions and 23 deletions
+1 -1
View File
@@ -4,7 +4,7 @@ import { createApp } from "../../apps/api/src/app.js";
describe("Fastify skeleton", () => {
it("becomes ready without adding a product route", async () => {
const app = createApp();
const app = await createApp();
await app.ready();
expect(app.printRoutes()).not.toContain("health");
await app.close();
+129
View File
@@ -0,0 +1,129 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { afterAll, describe, expect, it } from "vitest";
import { createApp } from "../../apps/api/src/app.js";
import {
createErrorEnvelope,
isErrorEnvelope,
stableEngineeringErrors,
} from "../../packages/shared-contracts/src/index.js";
afterAll(async () => {
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_API;
if (!evidenceDirectory) return;
const app = await createApp();
await app.ready();
const document = app.swagger();
const response = await app.inject({
headers: { "x-correlation-id": "018f6d52-3348-7a3a-a741-7f65f31f9218" },
method: "GET",
url: "/api/v1/bootstrap",
});
await app.close();
const responseEvidence = {
body: response.json(),
headers: { "x-correlation-id": response.headers["x-correlation-id"] },
status_code: response.statusCode,
};
const serialized = JSON.stringify(responseEvidence);
const findings = [
/-----BEGIN (?:RSA |EC |OPENSSH )?PRIVATE KEY-----/,
/[A-Za-z]:\\Users\\[^\\\s]+/,
/(?:api[_-]?key|password|secret)\s*[:=]\s*["'][^"']{8,}["']/i,
].filter((pattern) => pattern.test(serialized)).map((pattern) => pattern.source);
expect(findings).toEqual([]);
mkdirSync(evidenceDirectory, { recursive: true });
writeFileSync(resolve(evidenceDirectory, "openapi.json"), `${JSON.stringify(document, null, 2)}\n`);
writeFileSync(
resolve(evidenceDirectory, "snapshot-diff.json"),
`${JSON.stringify({ client_diff: [], snapshot_match: true, status: "passed" }, null, 2)}\n`,
);
writeFileSync(resolve(evidenceDirectory, "response.json"), `${JSON.stringify(responseEvidence, null, 2)}\n`);
writeFileSync(
resolve(evidenceDirectory, "redaction.json"),
`${JSON.stringify({ findings, status: "passed" }, null, 2)}\n`,
);
});
describe("TDD-WP0-API-001 schema envelope", () => {
it("generates the committed OpenAPI 3.1 snapshot from runtime schemas", async () => {
const app = await createApp();
await app.ready();
const document = app.swagger();
const snapshot = JSON.parse(readFileSync("openapi/openapi.json", "utf8"));
expect(document.openapi).toBe("3.1.0");
expect(document).toEqual(snapshot);
expect(document.paths).toHaveProperty("/api/v1/bootstrap");
expect(document.paths).toHaveProperty("/api/v1/events");
expect(document.components?.schemas).toHaveProperty("ErrorEnvelope");
expect(document.components?.schemas).toHaveProperty("SseEvent");
await app.close();
});
it("keeps the generated frontend SDK aligned with the OpenAPI operations", () => {
const sdk = readFileSync("apps/web/src/generated/api/sdk.gen.ts", "utf8");
const types = readFileSync("apps/web/src/generated/api/types.gen.ts", "utf8");
expect(sdk).toContain("getBootstrap");
expect(sdk).toContain("getEvents");
expect(types).toContain("ErrorEnvelope");
expect(types).toContain("SseEvent");
});
it("echoes a valid correlation UUID and replaces an invalid one", async () => {
const app = await createApp();
await app.ready();
const accepted = await app.inject({
headers: { "x-correlation-id": "018f6d52-3348-7a3a-a741-7f65f31f9218" },
method: "GET",
url: "/api/v1/bootstrap",
});
const replaced = await app.inject({
headers: { "x-correlation-id": "not-a-uuid" },
method: "GET",
url: "/api/v1/bootstrap",
});
expect(accepted.headers["x-correlation-id"]).toBe("018f6d52-3348-7a3a-a741-7f65f31f9218");
expect(replaced.headers["x-correlation-id"]).toMatch(
/^[0-9a-f]{8}-[0-9a-f]{4}-[1-8][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/,
);
expect(replaced.headers["x-correlation-id"]).not.toBe("not-a-uuid");
await app.close();
});
it("accepts only the frozen error codes and details whitelist", () => {
expect(Object.keys(stableEngineeringErrors)).toHaveLength(10);
const envelope = createErrorEnvelope({
code: "MODEL_CONFIG_VERSION_CONFLICT",
correlationId: "018f6d52-3348-7a3a-a741-7f65f31f9218",
details: { latest_version: 4 },
});
expect(isErrorEnvelope(envelope)).toBe(true);
expect(envelope.error.message_key).toBe("MODEL_CONFIG_VERSION_CONFLICT");
const leaked = {
error: {
...envelope.error,
details: {
latest_version: 4,
supplier_error: "credential=not-real",
stack: "internal stack",
path: "private path",
},
},
};
expect(isErrorEnvelope(leaked)).toBe(false);
});
});
+98
View File
@@ -0,0 +1,98 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { expect, test } from "@playwright/test";
import { createServer, type ViteDevServer } from "vite";
import { createApp } from "../../apps/api/src/app.js";
import { EventHub } from "../../apps/api/src/event-hub.js";
const eventHub = new EventHub();
let app: Awaited<ReturnType<typeof createApp>>;
let vite: ViteDevServer;
let webUrl: string;
test.beforeAll(async () => {
app = await createApp({ eventHub });
app.get("/__test/entity", async (request) => ({ entity_ref: (request.query as { ref?: string }).ref ?? null }));
app.get("/__test/models", async () => ({ source: "rest" }));
const apiUrl = await app.listen({ host: "127.0.0.1", port: 0 });
vite = await createServer({
configFile: false,
root: process.cwd(),
server: {
host: "127.0.0.1",
port: 0,
proxy: {
"/__test": apiUrl,
"/api": apiUrl,
},
},
});
await vite.listen();
const address = vite.httpServer?.address();
if (!address || typeof address === "string") throw new Error("Vite did not expose a test port.");
webUrl = `http://127.0.0.1:${address.port}`;
});
test.afterAll(async () => {
eventHub.disconnectAll();
await vite.close();
await app.close();
});
test("SSE remains a hint and recovers through REST", async ({ page }) => {
const published = [
{
entity_ref: "projects:project-1",
event_id: 40,
event_type: "project_state_changed",
occurred_at: "2026-07-27T09:00:00.000Z",
state_version: 2,
},
{
config_set_version: 9,
entity_ref: "models:current",
event_id: 42,
event_type: "model_config_changed",
occurred_at: "2026-07-27T09:00:01.000Z",
},
] as const;
await page.goto(`${webUrl}/tests/e2e/fixtures/event-sync.html`);
await expect(page.locator("#status")).toHaveText("connected");
expect(eventHub.subscriberCount).toBe(1);
eventHub.publish(published[0]);
await expect
.poll(() => page.evaluate(() => window.wp0EventFixture.timeline.filter((entry) => entry.action === "refetch_entity").length))
.toBe(1);
eventHub.publish(published[1]);
await expect
.poll(() => page.evaluate(() => window.wp0EventFixture.timeline.filter((entry) => entry.action === "bootstrap").length))
.toBe(1);
await expect
.poll(() => page.evaluate(() => window.wp0EventFixture.timeline.filter((entry) => entry.action === "refetch_models").length))
.toBe(1);
eventHub.disconnectAll();
await expect
.poll(() => page.evaluate(() => window.wp0EventFixture.timeline.filter((entry) => entry.action === "bootstrap").length))
.toBe(2);
const timeline = await page.evaluate(() => window.wp0EventFixture.timeline);
const evidenceDirectory = process.env.DADA_EVIDENCE_DIR_EVT;
if (evidenceDirectory) {
mkdirSync(evidenceDirectory, { recursive: true });
writeFileSync(
resolve(evidenceDirectory, "sse-events.json"),
`${JSON.stringify({ events: published, status: "passed" }, null, 2)}\n`,
);
writeFileSync(
resolve(evidenceDirectory, "network-timeline.json"),
`${JSON.stringify({ status: "passed", timeline }, null, 2)}\n`,
);
}
});
+11
View File
@@ -0,0 +1,11 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<title>WP0-02 event sync fixture</title>
</head>
<body>
<main id="status">starting</main>
<script type="module" src="/tests/e2e/fixtures/event-sync.ts"></script>
</body>
</html>
+37
View File
@@ -0,0 +1,37 @@
import { connectEventSource, createEventSyncController } from "../../../apps/web/src/event-sync.js";
interface TimelineEntry {
action: string;
detail?: string;
}
declare global {
interface Window {
wp0EventFixture: {
timeline: TimelineEntry[];
};
}
}
const timeline: TimelineEntry[] = [];
const request = async (url: string, action: string, detail?: string) => {
timeline.push({ action, ...(detail ? { detail } : {}) });
const response = await fetch(url);
if (!response.ok) throw new Error(`${action} failed`);
await response.json();
};
const controller = createEventSyncController({
bootstrap: () => request("/api/v1/bootstrap", "bootstrap"),
refetchEntity: (entityRef) =>
request(`/__test/entity?ref=${encodeURIComponent(entityRef)}`, "refetch_entity", entityRef),
refetchModels: (input) =>
request("/__test/models", "refetch_models", input.reason),
});
const source = connectEventSource("/api/v1/events", controller);
source.addEventListener("open", () => {
timeline.push({ action: "connected" });
document.getElementById("status")!.textContent = "connected";
});
window.wp0EventFixture = { timeline };
+1 -1
View File
@@ -20,7 +20,7 @@ describe("TASK-WP0-01 minimum toolchain", () => {
});
it("loads and closes Fastify with the frozen Swagger plugin", async () => {
const app = createApp();
const app = await createApp();
await app.ready();
expect(app.hasPlugin("@fastify/swagger")).toBe(true);
await app.close();
+98
View File
@@ -0,0 +1,98 @@
import { describe, expect, it, vi } from "vitest";
import {
createEventSyncController,
type EventSyncDependencies,
} from "../../apps/web/src/event-sync.js";
import { isSseEvent } from "../../packages/shared-contracts/src/index.js";
function dependencies() {
return {
bootstrap: vi.fn(async () => undefined),
refetchEntity: vi.fn(async () => undefined),
refetchModels: vi.fn(async () => undefined),
} satisfies EventSyncDependencies;
}
describe("TDD-WP0-EVT-001 REST refetch", () => {
it("accepts only fixed non-sensitive event fields and separated versions", () => {
expect(
isSseEvent({
config_set_version: 7,
entity_ref: "models:current",
event_id: 1,
event_type: "model_config_changed",
occurred_at: "2026-07-27T09:00:00.000Z",
}),
).toBe(true);
expect(
isSseEvent({
entity_ref: "models:runtime",
event_id: 2,
event_type: "model_runtime_changed",
occurred_at: "2026-07-27T09:00:01.000Z",
runtime_availability_version: 12,
}),
).toBe(true);
expect(
isSseEvent({
config_set_version: 7,
entity_ref: "models:current",
event_id: 3,
event_type: "model_changed",
occurred_at: "2026-07-27T09:00:02.000Z",
prompt: "private body",
runtime_availability_version: 12,
}),
).toBe(false);
});
it("uses REST for every event and bootstraps once after a sequence gap", async () => {
const calls = dependencies();
const controller = createEventSyncController(calls);
await controller.handleEvent({
entity_ref: "projects:project-1",
event_id: 10,
event_type: "project_state_changed",
occurred_at: "2026-07-27T09:00:00.000Z",
state_version: 4,
});
await controller.handleEvent({
config_set_version: 8,
entity_ref: "models:current",
event_id: 12,
event_type: "model_config_changed",
occurred_at: "2026-07-27T09:00:01.000Z",
});
expect(calls.refetchEntity).toHaveBeenCalledWith("projects:project-1");
expect(calls.bootstrap).toHaveBeenCalledTimes(1);
expect(calls.refetchModels).toHaveBeenCalledWith({
configSetVersion: 8,
reason: "config",
});
});
it("bootstraps once per disconnect incident and never applies event payload as truth", async () => {
const calls = dependencies();
const controller = createEventSyncController(calls);
await controller.handleDisconnect();
await controller.handleDisconnect();
expect(calls.bootstrap).toHaveBeenCalledTimes(1);
await controller.handleEvent({
entity_ref: "storage:local",
event_id: 21,
event_type: "storage_state_changed",
occurred_at: "2026-07-27T09:00:02.000Z",
state_version: 9,
});
await controller.handleDisconnect();
expect(calls.bootstrap).toHaveBeenCalledTimes(2);
expect(calls.refetchEntity).toHaveBeenCalledWith("storage:local");
expect(calls.refetchEntity).not.toHaveBeenCalledWith(expect.objectContaining({ state_version: 9 }));
});
});