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
View File
@@ -9,6 +9,7 @@
"typecheck": "tsc --noEmit -p tsconfig.json" "typecheck": "tsc --noEmit -p tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@dada/shared-contracts": "workspace:*",
"@fastify/swagger": "9.8.1", "@fastify/swagger": "9.8.1",
"@sinclair/typebox": "0.34.52", "@sinclair/typebox": "0.34.52",
"better-sqlite3": "13.0.1", "better-sqlite3": "13.0.1",
+126 -3
View File
@@ -1,17 +1,140 @@
import { randomUUID } from "node:crypto";
import {
BootstrapResponseSchema,
CorrelationIdSchema,
ErrorDetailsSchema,
ErrorEnvelopeSchema,
GenerationErrorCategorySchema,
ModelConfigSseEventSchema,
ModelRuntimeSseEventSchema,
SseEventSchema,
StableEngineeringErrorCodeSchema,
StateSseEventSchema,
isCorrelationId,
type BootstrapResponse,
} from "@dada/shared-contracts";
import swagger from "@fastify/swagger"; import swagger from "@fastify/swagger";
import Fastify from "fastify"; import Fastify from "fastify";
export function createApp() { import { EventHub } from "./event-hub.js";
const app = Fastify({ logger: false });
void app.register(swagger, { const defaultBootstrap: BootstrapResponse = {
app_version: "0.0.0",
dependencies: [],
model_summary: {
config_set_version: null,
configured_default_model_id: null,
recommended_model_id: null,
runtime_availability_version: null,
},
public_features: [],
};
export interface CreateAppOptions {
bootstrap?: () => BootstrapResponse | Promise<BootstrapResponse>;
eventHub?: EventHub;
}
function requestCorrelationId(headers: Record<string, string | string[] | undefined>) {
const header = headers["x-correlation-id"];
const candidate = Array.isArray(header) ? header[0] : header;
return isCorrelationId(candidate) ? candidate : randomUUID();
}
export async function createApp(options: CreateAppOptions = {}) {
const eventHub = options.eventHub ?? new EventHub();
const bootstrap = options.bootstrap ?? (() => defaultBootstrap);
const app = Fastify({
genReqId: (request) => requestCorrelationId(request.headers),
logger: false,
});
await app.register(swagger, {
openapi: { openapi: {
info: { info: {
title: "Dada P0-A", title: "Dada P0-A",
version: "0.0.0", version: "0.0.0",
}, },
openapi: "3.1.0",
},
refResolver: {
buildLocalReference: (schema, _baseUri, _fragment, index) =>
typeof schema.$id === "string" ? schema.$id : `schema-${index}`,
}, },
}); });
for (const schema of [
CorrelationIdSchema,
GenerationErrorCategorySchema,
StableEngineeringErrorCodeSchema,
ErrorDetailsSchema,
ErrorEnvelopeSchema,
BootstrapResponseSchema,
StateSseEventSchema,
ModelConfigSseEventSchema,
ModelRuntimeSseEventSchema,
SseEventSchema,
]) {
app.addSchema(schema);
}
app.addHook("onRequest", (request, reply, done) => {
reply.header("X-Correlation-Id", request.id);
done();
});
app.get(
"/api/v1/bootstrap",
{
schema: {
operationId: "getBootstrap",
response: {
200: BootstrapResponseSchema,
},
tags: ["Bootstrap"],
},
},
async () => bootstrap(),
);
app.get(
"/api/v1/events",
{
schema: {
operationId: "getEvents",
response: {
200: {
content: {
"text/event-stream": {
schema: SseEventSchema,
},
},
description: "Non-sensitive state change hints. REST remains authoritative.",
},
},
tags: ["State events"],
},
},
(request, reply) => {
reply.hijack();
reply.raw.setHeader("Cache-Control", "no-store");
reply.raw.setHeader("Connection", "keep-alive");
reply.raw.setHeader("Content-Type", "text/event-stream; charset=utf-8");
reply.raw.setHeader("X-Correlation-Id", request.id);
reply.raw.writeHead(200);
reply.raw.write(": connected\n\n");
const unsubscribe = eventHub.connect(
(event) => {
reply.raw.write(`id: ${event.event_id}\n`);
reply.raw.write(`data: ${JSON.stringify(event)}\n\n`);
},
() => reply.raw.end(),
);
request.raw.once("close", unsubscribe);
},
);
return app; return app;
} }
+31
View File
@@ -0,0 +1,31 @@
import { isSseEvent, type SseEvent } from "@dada/shared-contracts";
interface Connection {
close: () => void;
send: (event: SseEvent) => void;
}
export class EventHub {
readonly #connections = new Set<Connection>();
get subscriberCount() {
return this.#connections.size;
}
connect(send: Connection["send"], close: Connection["close"]) {
const connection = { close, send };
this.#connections.add(connection);
return () => this.#connections.delete(connection);
}
publish(event: SseEvent) {
if (!isSseEvent(event)) throw new Error("SSE event does not match the frozen non-sensitive schema.");
for (const connection of this.#connections) connection.send(event);
}
disconnectAll() {
const connections = [...this.#connections];
this.#connections.clear();
for (const connection of connections) connection.close();
}
}
+1 -1
View File
@@ -1,6 +1,6 @@
import { createApp } from "./app.js"; import { createApp } from "./app.js";
const app = createApp(); const app = await createApp();
await app.listen({ await app.listen({
host: "127.0.0.1", host: "127.0.0.1",
+1
View File
@@ -8,6 +8,7 @@
"typecheck": "tsc --noEmit -p tsconfig.json" "typecheck": "tsc --noEmit -p tsconfig.json"
}, },
"dependencies": { "dependencies": {
"@dada/shared-contracts": "workspace:*",
"@vibrant/core": "4.0.4", "@vibrant/core": "4.0.4",
"@vibrant/quantizer-mmcq": "4.0.4", "@vibrant/quantizer-mmcq": "4.0.4",
"fabric": "7.4.0", "fabric": "7.4.0",
+74
View File
@@ -0,0 +1,74 @@
import { isSseEvent, type SseEvent } from "@dada/shared-contracts";
export interface EventSyncDependencies {
bootstrap: () => Promise<unknown>;
refetchEntity: (entityRef: string) => Promise<unknown>;
refetchModels: (input: {
configSetVersion?: number;
reason: "config" | "runtime";
runtimeAvailabilityVersion?: number;
}) => Promise<unknown>;
}
export interface EventSyncController {
handleDisconnect: () => Promise<void>;
handleEvent: (event: unknown) => Promise<void>;
}
async function refetchEventTruth(dependencies: EventSyncDependencies, event: SseEvent) {
if ("config_set_version" in event) {
await dependencies.refetchModels({
configSetVersion: event.config_set_version,
reason: "config",
});
return;
}
if ("runtime_availability_version" in event) {
await dependencies.refetchModels({
reason: "runtime",
runtimeAvailabilityVersion: event.runtime_availability_version,
});
return;
}
await dependencies.refetchEntity(event.entity_ref);
}
export function createEventSyncController(dependencies: EventSyncDependencies): EventSyncController {
let lastEventId: number | undefined;
let recoveryInProgress = false;
async function recoverOnce() {
if (recoveryInProgress) return;
recoveryInProgress = true;
await dependencies.bootstrap();
}
return {
async handleDisconnect() {
await recoverOnce();
},
async handleEvent(event: unknown) {
if (!isSseEvent(event)) throw new Error("SSE event failed the non-sensitive schema.");
if (lastEventId !== undefined && event.event_id !== lastEventId + 1) {
await recoverOnce();
}
lastEventId = event.event_id;
await refetchEventTruth(dependencies, event);
recoveryInProgress = false;
},
};
}
export function connectEventSource(url: string, controller: EventSyncController) {
const source = new EventSource(url, { withCredentials: true });
source.onmessage = (message) => {
void controller.handleEvent(JSON.parse(message.data) as SseEvent);
};
source.onerror = () => {
void controller.handleDisconnect();
};
return source;
}
+46
View File
@@ -0,0 +1,46 @@
// Generated from openapi/openapi.json. Do not edit by hand.
export interface ClientOptions { baseUrl?: string; fetch?: typeof globalThis.fetch; headers?: HeadersInit; }
export async function getBootstrap(options: ClientOptions = {}): Promise<{
"app_version": string;
"dependencies": Array<{
"service_id": string;
"status": "available" | "paused" | "degraded" | "unavailable";
}>;
"model_summary": {
"config_set_version": number | null;
"configured_default_model_id": string | null;
"recommended_model_id": string | null;
"runtime_availability_version": number | null;
};
"public_features": Array<{
"feature_id": string;
"status": "enabled" | "disabled" | "paused";
}>;
}> {
const request = options.fetch ?? globalThis.fetch;
const response = await request(`${options.baseUrl ?? ""}/api/v1/bootstrap`, { method: "GET", headers: options.headers ?? {} });
if (!response.ok) throw new Error(`HTTP ${response.status}`);
return response.json() as Promise<{
"app_version": string;
"dependencies": Array<{
"service_id": string;
"status": "available" | "paused" | "degraded" | "unavailable";
}>;
"model_summary": {
"config_set_version": number | null;
"configured_default_model_id": string | null;
"recommended_model_id": string | null;
"runtime_availability_version": number | null;
};
"public_features": Array<{
"feature_id": string;
"status": "enabled" | "disabled" | "paused";
}>;
}>;
}
export function getEvents(options: Pick<ClientOptions, "baseUrl"> = {}): string {
return `${options.baseUrl ?? ""}/api/v1/events`;
}
+99
View File
@@ -0,0 +1,99 @@
// Generated from openapi/openapi.json. Do not edit by hand.
export type BootstrapResponse = {
"app_version": string;
"dependencies": Array<{
"service_id": string;
"status": "available" | "paused" | "degraded" | "unavailable";
}>;
"model_summary": {
"config_set_version": number | null;
"configured_default_model_id": string | null;
"recommended_model_id": string | null;
"runtime_availability_version": number | null;
};
"public_features": Array<{
"feature_id": string;
"status": "enabled" | "disabled" | "paused";
}>;
};
export type CorrelationId = string;
export type ErrorDetails = {
"capacity_status"?: "normal" | "warning" | "critical" | "full" | "unavailable";
"current_task_ref"?: string;
"field_errors"?: Array<{
"field": string;
"message_key": string;
}>;
"latest_version"?: number | string;
"remaining_bytes"?: number;
};
export type ErrorEnvelope = {
"error": {
"code": "BROWSER_UNSUPPORTED" | "MODEL_CONFIG_VERSION_CONFLICT" | "MODEL_DEFAULT_REPLACEMENT_REQUIRED" | "MODEL_DEFAULT_REPLACEMENT_INVALID" | "MODEL_RECOMMENDATION_PRIORITY_INVALID" | "MODEL_RECOMMENDATION_PRIORITY_CONFLICT" | "PRIVATE_CONTENT_NOTICE_ACK_REQUIRED" | "ASSET_HISTORY_REFERENCE_CONFLICT" | "ASSET_CLEANUP_CANDIDATE_STALE" | "STORAGE_CAPACITY_EXCEEDED";
"correlation_id": string;
"details": {
"capacity_status"?: "normal" | "warning" | "critical" | "full" | "unavailable";
"current_task_ref"?: string;
"field_errors"?: Array<{
"field": string;
"message_key": string;
}>;
"latest_version"?: number | string;
"remaining_bytes"?: number;
};
"error_category"?: "upstream_timeout" | "upstream_failed" | "safety_rejected" | "model_disabled" | "gateway_balance_insufficient" | "gateway_contract_invalid" | "reference_invalid" | "unknown_retryable" | "unknown_non_retryable";
"message_key": string;
};
};
export type GenerationErrorCategory = "upstream_timeout" | "upstream_failed" | "safety_rejected" | "model_disabled" | "gateway_balance_insufficient" | "gateway_contract_invalid" | "reference_invalid" | "unknown_retryable" | "unknown_non_retryable";
export type ModelConfigSseEvent = {
"config_set_version": number;
"entity_ref": string;
"event_id": number;
"event_type": "model_config_changed";
"occurred_at": string;
};
export type ModelRuntimeSseEvent = {
"entity_ref": string;
"event_id": number;
"event_type": "model_runtime_changed";
"occurred_at": string;
"runtime_availability_version": number;
};
export type SseEvent = {
"entity_ref": string;
"event_id": number;
"event_type": string;
"occurred_at": string;
"state_version": number;
} | {
"config_set_version": number;
"entity_ref": string;
"event_id": number;
"event_type": "model_config_changed";
"occurred_at": string;
} | {
"entity_ref": string;
"event_id": number;
"event_type": "model_runtime_changed";
"occurred_at": string;
"runtime_availability_version": number;
};
export type StableEngineeringErrorCode = "BROWSER_UNSUPPORTED" | "MODEL_CONFIG_VERSION_CONFLICT" | "MODEL_DEFAULT_REPLACEMENT_REQUIRED" | "MODEL_DEFAULT_REPLACEMENT_INVALID" | "MODEL_RECOMMENDATION_PRIORITY_INVALID" | "MODEL_RECOMMENDATION_PRIORITY_CONFLICT" | "PRIVATE_CONTENT_NOTICE_ACK_REQUIRED" | "ASSET_HISTORY_REFERENCE_CONFLICT" | "ASSET_CLEANUP_CANDIDATE_STALE" | "STORAGE_CAPACITY_EXCEEDED";
export type StateSseEvent = {
"entity_ref": string;
"event_id": number;
"event_type": string;
"occurred_at": string;
"state_version": number;
};
+1146
View File
File diff suppressed because it is too large Load Diff
+8 -5
View File
@@ -10,19 +10,22 @@
"build": "pnpm -r --if-present build && dotnet build supervisor/Dada.Supervisor/Dada.Supervisor.csproj --configuration Release", "build": "pnpm -r --if-present build && dotnet build supervisor/Dada.Supervisor/Dada.Supervisor.csproj --configuration Release",
"typecheck": "pnpm -r --if-present typecheck", "typecheck": "pnpm -r --if-present typecheck",
"test:unit:contract": "node --test tests/toolchain/frozen-toolchain.test.mjs", "test:unit:contract": "node --test tests/toolchain/frozen-toolchain.test.mjs",
"test:unit": "pnpm run test:unit:contract && vitest run tests/unit", "test:unit": "pnpm --filter @dada/shared-contracts build && pnpm run test:unit:contract && vitest run tests/unit",
"test:integration": "vitest run tests/integration", "test:integration": "vitest run tests/integration",
"test:api": "vitest run tests/api", "test:api": "pnpm check:openapi && vitest run tests/api",
"test:worker": "pnpm --filter @dada/worker build && node scripts/worker-smoke.mjs", "test:worker": "pnpm --filter @dada/worker build && node scripts/worker-smoke.mjs",
"test:e2e": "node scripts/validate-layer-scope.mjs E2E", "test:e2e": "pnpm check:openapi && playwright test tests/e2e/event-sync.spec.ts --config playwright.config.ts",
"test:visual": "node scripts/validate-layer-scope.mjs VISUAL", "test:visual": "node scripts/validate-layer-scope.mjs VISUAL",
"test:performance": "node scripts/validate-layer-scope.mjs PERFORMANCE", "test:performance": "node scripts/validate-layer-scope.mjs PERFORMANCE",
"test:security": "node scripts/verify-frozen-dependencies.mjs && node scripts/redaction-scan.mjs", "test:security": "node scripts/verify-frozen-dependencies.mjs && node scripts/redaction-scan.mjs",
"test:package": "pnpm run typecheck && pnpm --filter @dada/web build && pnpm --filter @dada/api build && pnpm --filter @dada/worker build && node scripts/package-smoke.mjs", "test:package": "pnpm run typecheck && pnpm --filter @dada/shared-contracts build && pnpm --filter @dada/web build && pnpm --filter @dada/api build && pnpm --filter @dada/worker build && node scripts/package-smoke.mjs",
"generate:openapi": "node scripts/generate-openapi.mjs",
"check:openapi": "node scripts/check-openapi.mjs",
"validate:tdd-trace": "node scripts/validate-tdd-trace.mjs", "validate:tdd-trace": "node scripts/validate-tdd-trace.mjs",
"validate:external": "node scripts/validate-external.mjs", "validate:external": "node scripts/validate-external.mjs",
"test:all": "pnpm test:unit && pnpm test:integration && pnpm test:api && pnpm test:worker && pnpm test:e2e && pnpm test:visual && pnpm test:performance && pnpm test:security && pnpm test:package && pnpm validate:tdd-trace", "test:all": "pnpm test:unit && pnpm test:integration && pnpm test:api && pnpm test:worker && pnpm test:e2e && pnpm test:visual && pnpm test:performance && pnpm test:security && pnpm test:package && pnpm validate:tdd-trace",
"test:wp0-01": "node scripts/run-wp0-01-validation.mjs" "test:wp0-01": "node scripts/run-wp0-01-validation.mjs",
"test:wp0-02": "node scripts/run-wp0-02-validation.mjs"
}, },
"devDependencies": { "devDependencies": {
"@playwright/test": "1.62.0", "@playwright/test": "1.62.0",
+128
View File
@@ -0,0 +1,128 @@
import { Type, type Static } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
const uuidPattern = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-8][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$";
export const stableEngineeringErrors = {
BROWSER_UNSUPPORTED: { httpStatus: 426, messageKey: "browser.unsupported" },
MODEL_CONFIG_VERSION_CONFLICT: { httpStatus: 412, messageKey: "MODEL_CONFIG_VERSION_CONFLICT" },
MODEL_DEFAULT_REPLACEMENT_REQUIRED: { httpStatus: 409, messageKey: "MODEL_DEFAULT_REPLACEMENT_REQUIRED" },
MODEL_DEFAULT_REPLACEMENT_INVALID: { httpStatus: 409, messageKey: "MODEL_DEFAULT_REPLACEMENT_INVALID" },
MODEL_RECOMMENDATION_PRIORITY_INVALID: { httpStatus: 400, messageKey: "MODEL_RECOMMENDATION_PRIORITY_INVALID" },
MODEL_RECOMMENDATION_PRIORITY_CONFLICT: { httpStatus: 409, messageKey: "MODEL_RECOMMENDATION_PRIORITY_CONFLICT" },
PRIVATE_CONTENT_NOTICE_ACK_REQUIRED: { httpStatus: 428, messageKey: "PRIVATE_CONTENT_NOTICE_ACK_REQUIRED" },
ASSET_HISTORY_REFERENCE_CONFLICT: { httpStatus: 409, messageKey: "ASSET_HISTORY_REFERENCE_CONFLICT" },
ASSET_CLEANUP_CANDIDATE_STALE: { httpStatus: 409, messageKey: "ASSET_CLEANUP_CANDIDATE_STALE" },
STORAGE_CAPACITY_EXCEEDED: { httpStatus: 507, messageKey: "STORAGE_CAPACITY_EXCEEDED" },
} as const;
export type StableEngineeringErrorCode = keyof typeof stableEngineeringErrors;
export const CorrelationIdSchema = Type.String({ pattern: uuidPattern, $id: "CorrelationId" });
export const GenerationErrorCategorySchema = Type.Union(
[
Type.Literal("upstream_timeout"),
Type.Literal("upstream_failed"),
Type.Literal("safety_rejected"),
Type.Literal("model_disabled"),
Type.Literal("gateway_balance_insufficient"),
Type.Literal("gateway_contract_invalid"),
Type.Literal("reference_invalid"),
Type.Literal("unknown_retryable"),
Type.Literal("unknown_non_retryable"),
],
{ $id: "GenerationErrorCategory" },
);
export const StableEngineeringErrorCodeSchema = Type.Union(
[
Type.Literal("BROWSER_UNSUPPORTED"),
Type.Literal("MODEL_CONFIG_VERSION_CONFLICT"),
Type.Literal("MODEL_DEFAULT_REPLACEMENT_REQUIRED"),
Type.Literal("MODEL_DEFAULT_REPLACEMENT_INVALID"),
Type.Literal("MODEL_RECOMMENDATION_PRIORITY_INVALID"),
Type.Literal("MODEL_RECOMMENDATION_PRIORITY_CONFLICT"),
Type.Literal("PRIVATE_CONTENT_NOTICE_ACK_REQUIRED"),
Type.Literal("ASSET_HISTORY_REFERENCE_CONFLICT"),
Type.Literal("ASSET_CLEANUP_CANDIDATE_STALE"),
Type.Literal("STORAGE_CAPACITY_EXCEEDED"),
],
{ $id: "StableEngineeringErrorCode" },
);
export const ErrorDetailsSchema = Type.Object(
{
capacity_status: Type.Optional(
Type.Union([
Type.Literal("normal"),
Type.Literal("warning"),
Type.Literal("critical"),
Type.Literal("full"),
Type.Literal("unavailable"),
]),
),
current_task_ref: Type.Optional(Type.String({ maxLength: 160, pattern: "^[a-z][a-z0-9_]*:[A-Za-z0-9_-]+$" })),
field_errors: Type.Optional(
Type.Array(
Type.Object(
{
field: Type.String({ maxLength: 120, pattern: "^[A-Za-z0-9_.\\[\\]-]+$" }),
message_key: Type.String({ maxLength: 120, pattern: "^[A-Za-z0-9_.-]+$" }),
},
{ additionalProperties: false },
),
{ maxItems: 32 },
),
),
latest_version: Type.Optional(Type.Union([Type.Integer({ minimum: 0 }), Type.String({ maxLength: 120 })])),
remaining_bytes: Type.Optional(Type.Integer({ minimum: 0 })),
},
{ additionalProperties: false, $id: "ErrorDetails" },
);
export const ErrorEnvelopeSchema = Type.Object(
{
error: Type.Object(
{
code: StableEngineeringErrorCodeSchema,
message_key: Type.String({ maxLength: 120, pattern: "^[A-Za-z0-9_.-]+$" }),
correlation_id: CorrelationIdSchema,
error_category: Type.Optional(GenerationErrorCategorySchema),
details: ErrorDetailsSchema,
},
{ additionalProperties: false },
),
},
{ additionalProperties: false, $id: "ErrorEnvelope" },
);
export type ErrorDetails = Static<typeof ErrorDetailsSchema>;
export type ErrorEnvelope = Static<typeof ErrorEnvelopeSchema>;
export type GenerationErrorCategory = Static<typeof GenerationErrorCategorySchema>;
export function isCorrelationId(value: unknown): value is string {
return Value.Check(CorrelationIdSchema, value);
}
export function isErrorEnvelope(value: unknown): value is ErrorEnvelope {
if (!Value.Check(ErrorEnvelopeSchema, value)) return false;
const definition = stableEngineeringErrors[value.error.code];
return definition.messageKey === value.error.message_key;
}
export function createErrorEnvelope(input: {
code: StableEngineeringErrorCode;
correlationId: string;
details?: ErrorDetails;
errorCategory?: GenerationErrorCategory;
}): ErrorEnvelope {
const definition = stableEngineeringErrors[input.code];
const envelope: ErrorEnvelope = {
error: {
code: input.code,
correlation_id: input.correlationId,
details: input.details ?? {},
message_key: definition.messageKey,
...(input.errorCategory ? { error_category: input.errorCategory } : {}),
},
};
if (!isErrorEnvelope(envelope)) throw new Error("Error envelope does not match the frozen schema.");
return envelope;
}
@@ -0,0 +1,44 @@
import { Type, type Static } from "@sinclair/typebox";
const ModelIdSchema = Type.String({ maxLength: 80, pattern: "^[a-z0-9][a-z0-9.-]+$" });
export const BootstrapResponseSchema = Type.Object(
{
app_version: Type.String({ maxLength: 40, pattern: "^[0-9]+\\.[0-9]+\\.[0-9]+(?:[-+][A-Za-z0-9.-]+)?$" }),
dependencies: Type.Array(
Type.Object(
{
service_id: Type.String({ maxLength: 60, pattern: "^[a-z][a-z0-9_-]+$" }),
status: Type.Union([
Type.Literal("available"),
Type.Literal("paused"),
Type.Literal("degraded"),
Type.Literal("unavailable"),
]),
},
{ additionalProperties: false },
),
),
model_summary: Type.Object(
{
config_set_version: Type.Union([Type.Integer({ minimum: 0 }), Type.Null()]),
configured_default_model_id: Type.Union([ModelIdSchema, Type.Null()]),
recommended_model_id: Type.Union([ModelIdSchema, Type.Null()]),
runtime_availability_version: Type.Union([Type.Integer({ minimum: 0 }), Type.Null()]),
},
{ additionalProperties: false },
),
public_features: Type.Array(
Type.Object(
{
feature_id: Type.String({ maxLength: 80, pattern: "^[a-z][a-z0-9_-]+$" }),
status: Type.Union([Type.Literal("enabled"), Type.Literal("disabled"), Type.Literal("paused")]),
},
{ additionalProperties: false },
),
),
},
{ additionalProperties: false, $id: "BootstrapResponse" },
);
export type BootstrapResponse = Static<typeof BootstrapResponseSchema>;
+43
View File
@@ -0,0 +1,43 @@
import { Type, type Static } from "@sinclair/typebox";
import { Value } from "@sinclair/typebox/value";
const eventBase = {
entity_ref: Type.String({ maxLength: 160, pattern: "^[a-z][a-z0-9_]*:[A-Za-z0-9_-]+$" }),
event_id: Type.Integer({ minimum: 0 }),
occurred_at: Type.String({ pattern: "^[0-9]{4}-[0-9]{2}-[0-9]{2}T[0-9]{2}:[0-9]{2}:[0-9]{2}(?:\\.[0-9]{3})?Z$" }),
};
export const StateSseEventSchema = Type.Object(
{
...eventBase,
event_type: Type.String({ maxLength: 80, pattern: "^(?!model_config_changed$|model_runtime_changed$)[a-z][a-z0-9_]+$" }),
state_version: Type.Integer({ minimum: 0 }),
},
{ additionalProperties: false, $id: "StateSseEvent" },
);
export const ModelConfigSseEventSchema = Type.Object(
{
...eventBase,
config_set_version: Type.Integer({ minimum: 0 }),
event_type: Type.Literal("model_config_changed"),
},
{ additionalProperties: false, $id: "ModelConfigSseEvent" },
);
export const ModelRuntimeSseEventSchema = Type.Object(
{
...eventBase,
event_type: Type.Literal("model_runtime_changed"),
runtime_availability_version: Type.Integer({ minimum: 0 }),
},
{ additionalProperties: false, $id: "ModelRuntimeSseEvent" },
);
export const SseEventSchema = Type.Union(
[StateSseEventSchema, ModelConfigSseEventSchema, ModelRuntimeSseEventSchema],
{ $id: "SseEvent" },
);
export type SseEvent = Static<typeof SseEventSchema>;
export function isSseEvent(value: unknown): value is SseEvent {
return Value.Check(SseEventSchema, value);
}
+3
View File
@@ -1 +1,4 @@
export { Type } from "@sinclair/typebox"; export { Type } from "@sinclair/typebox";
export * from "./api.js";
export * from "./bootstrap.js";
export * from "./events.js";
+18
View File
@@ -0,0 +1,18 @@
import { defineConfig } from "@playwright/test";
export default defineConfig({
forbidOnly: true,
fullyParallel: false,
outputDir: process.env.DADA_PLAYWRIGHT_OUTPUT_DIR ?? "test-results/wp0-02",
reporter: "line",
retries: 0,
testDir: "./tests/e2e",
timeout: 30_000,
use: {
channel: "msedge",
headless: true,
trace: "on",
viewport: { height: 720, width: 1280 },
},
workers: 1,
});
+26 -12
View File
@@ -28,13 +28,16 @@ importers:
version: 7.0.2 version: 7.0.2
vite: vite:
specifier: 8.1.5 specifier: 8.1.5
version: 8.1.5(@types/node@24.13.3)(yaml@2.9.0) version: 8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)
vitest: vitest:
specifier: 4.1.10 specifier: 4.1.10
version: 4.1.10(@types/node@24.13.3)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(yaml@2.9.0)) version: 4.1.10(@types/node@24.13.3)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))
apps/api: apps/api:
dependencies: dependencies:
'@dada/shared-contracts':
specifier: workspace:*
version: link:../../packages/shared-contracts
'@fastify/swagger': '@fastify/swagger':
specifier: 9.8.1 specifier: 9.8.1
version: 9.8.1 version: 9.8.1
@@ -63,6 +66,9 @@ importers:
apps/web: apps/web:
dependencies: dependencies:
'@dada/shared-contracts':
specifier: workspace:*
version: link:../../packages/shared-contracts
'@vibrant/core': '@vibrant/core':
specifier: 4.0.4 specifier: 4.0.4
version: 4.0.4 version: 4.0.4
@@ -87,13 +93,13 @@ importers:
version: 19.2.3(@types/react@19.2.17) version: 19.2.3(@types/react@19.2.17)
'@vitejs/plugin-react': '@vitejs/plugin-react':
specifier: 6.0.4 specifier: 6.0.4
version: 6.0.4(vite@8.1.5(@types/node@24.13.3)(yaml@2.9.0)) version: 6.0.4(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))
typescript: typescript:
specifier: 7.0.2 specifier: 7.0.2
version: 7.0.2 version: 7.0.2
vite: vite:
specifier: 8.1.5 specifier: 8.1.5
version: 8.1.5(@types/node@24.13.3)(yaml@2.9.0) version: 8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)
apps/worker: apps/worker:
dependencies: dependencies:
@@ -827,6 +833,10 @@ packages:
is-potential-custom-element-name@1.0.1: is-potential-custom-element-name@1.0.1:
resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==}
jiti@2.7.0:
resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==}
hasBin: true
jsdom@26.1.0: jsdom@26.1.0:
resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==}
engines: {node: '>=18'} engines: {node: '>=18'}
@@ -1635,10 +1645,10 @@ snapshots:
dependencies: dependencies:
'@vibrant/types': 4.0.4 '@vibrant/types': 4.0.4
'@vitejs/plugin-react@6.0.4(vite@8.1.5(@types/node@24.13.3)(yaml@2.9.0))': '@vitejs/plugin-react@6.0.4(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))':
dependencies: dependencies:
'@rolldown/pluginutils': 1.0.1 '@rolldown/pluginutils': 1.0.1
vite: 8.1.5(@types/node@24.13.3)(yaml@2.9.0) vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)
'@vitest/expect@4.1.10': '@vitest/expect@4.1.10':
dependencies: dependencies:
@@ -1649,13 +1659,13 @@ snapshots:
chai: 6.2.2 chai: 6.2.2
tinyrainbow: 3.1.0 tinyrainbow: 3.1.0
'@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(yaml@2.9.0))': '@vitest/mocker@4.1.10(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))':
dependencies: dependencies:
'@vitest/spy': 4.1.10 '@vitest/spy': 4.1.10
estree-walker: 3.0.3 estree-walker: 3.0.3
magic-string: 0.30.21 magic-string: 0.30.21
optionalDependencies: optionalDependencies:
vite: 8.1.5(@types/node@24.13.3)(yaml@2.9.0) vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)
'@vitest/pretty-format@4.1.10': '@vitest/pretty-format@4.1.10':
dependencies: dependencies:
@@ -1914,6 +1924,9 @@ snapshots:
is-potential-custom-element-name@1.0.1: is-potential-custom-element-name@1.0.1:
optional: true optional: true
jiti@2.7.0:
optional: true
jsdom@26.1.0(canvas@3.2.3): jsdom@26.1.0(canvas@3.2.3):
dependencies: dependencies:
cssstyle: 4.6.0 cssstyle: 4.6.0
@@ -2342,7 +2355,7 @@ snapshots:
util-deprecate@1.0.2: util-deprecate@1.0.2:
optional: true optional: true
vite@8.1.5(@types/node@24.13.3)(yaml@2.9.0): vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0):
dependencies: dependencies:
lightningcss: 1.33.0 lightningcss: 1.33.0
picomatch: 4.0.5 picomatch: 4.0.5
@@ -2352,12 +2365,13 @@ snapshots:
optionalDependencies: optionalDependencies:
'@types/node': 24.13.3 '@types/node': 24.13.3
fsevents: 2.3.3 fsevents: 2.3.3
jiti: 2.7.0
yaml: 2.9.0 yaml: 2.9.0
vitest@4.1.10(@types/node@24.13.3)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(yaml@2.9.0)): vitest@4.1.10(@types/node@24.13.3)(jsdom@26.1.0(canvas@3.2.3))(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)):
dependencies: dependencies:
'@vitest/expect': 4.1.10 '@vitest/expect': 4.1.10
'@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(yaml@2.9.0)) '@vitest/mocker': 4.1.10(vite@8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0))
'@vitest/pretty-format': 4.1.10 '@vitest/pretty-format': 4.1.10
'@vitest/runner': 4.1.10 '@vitest/runner': 4.1.10
'@vitest/snapshot': 4.1.10 '@vitest/snapshot': 4.1.10
@@ -2374,7 +2388,7 @@ snapshots:
tinyexec: 1.2.4 tinyexec: 1.2.4
tinyglobby: 0.2.17 tinyglobby: 0.2.17
tinyrainbow: 3.1.0 tinyrainbow: 3.1.0
vite: 8.1.5(@types/node@24.13.3)(yaml@2.9.0) vite: 8.1.5(@types/node@24.13.3)(jiti@2.7.0)(yaml@2.9.0)
why-is-node-running: 2.3.0 why-is-node-running: 2.3.0
optionalDependencies: optionalDependencies:
'@types/node': 24.13.3 '@types/node': 24.13.3
+31
View File
@@ -0,0 +1,31 @@
import { readFileSync, rmSync } from "node:fs";
import { sep, resolve } from "node:path";
import { generateClient } from "./lib/generate-client.mjs";
import {
buildApiContracts,
compareTrees,
createOpenApiDocument,
formattedJson,
} from "./lib/openapi.mjs";
buildApiContracts();
const document = await createOpenApiDocument();
const expectedDocument = readFileSync("openapi/openapi.json", "utf8");
const currentDocument = formattedJson(document);
const buildRoot = resolve(".build");
const generatedRoot = resolve(buildRoot, "openapi-client-check");
if (!generatedRoot.startsWith(`${buildRoot}${sep}`)) throw new Error("Generated check path escaped .build.");
rmSync(generatedRoot, { force: true, recursive: true });
await generateClient(resolve("openapi/openapi.json"), generatedRoot);
const clientDiff = compareTrees(resolve("apps/web/src/generated/api"), generatedRoot);
const result = {
client_diff: clientDiff,
openapi: document.openapi,
snapshot_match: currentDocument === expectedDocument,
status: currentDocument === expectedDocument && clientDiff.length === 0 ? "passed" : "failed",
};
console.log(JSON.stringify(result, null, 2));
if (result.status !== "passed") process.exit(1);
+13
View File
@@ -0,0 +1,13 @@
import { mkdirSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
import { generateClient } from "./lib/generate-client.mjs";
import { buildApiContracts, createOpenApiDocument, formattedJson } from "./lib/openapi.mjs";
buildApiContracts();
const document = await createOpenApiDocument();
const snapshotPath = resolve("openapi/openapi.json");
mkdirSync(resolve("openapi"), { recursive: true });
writeFileSync(snapshotPath, formattedJson(document));
await generateClient(snapshotPath, resolve("apps/web/src/generated/api"));
console.log(JSON.stringify({ client: "generated", openapi: document.openapi, snapshot: "openapi/openapi.json" }));
+89
View File
@@ -0,0 +1,89 @@
import { mkdirSync, readFileSync, writeFileSync } from "node:fs";
import { resolve } from "node:path";
function identifier(value) {
return value.replaceAll(/[^A-Za-z0-9_$]/g, "_");
}
function schemaType(schema) {
if (!schema || typeof schema !== "object") return "unknown";
if (schema.$ref) return identifier(schema.$ref.split("/").at(-1));
if (Object.hasOwn(schema, "const")) return JSON.stringify(schema.const);
if (schema.enum) return schema.enum.map((value) => JSON.stringify(value)).join(" | ");
if (schema.anyOf) return schema.anyOf.map(schemaType).join(" | ");
if (schema.oneOf) return schema.oneOf.map(schemaType).join(" | ");
if (Array.isArray(schema.type)) {
return schema.type.map((type) => schemaType({ ...schema, type })).join(" | ");
}
if (schema.type === "array") return `Array<${schemaType(schema.items)}>`;
if (schema.type === "boolean") return "boolean";
if (schema.type === "integer" || schema.type === "number") return "number";
if (schema.type === "null") return "null";
if (schema.type === "string") return "string";
if (schema.type === "object" || schema.properties) {
const required = new Set(schema.required ?? []);
const fields = Object.entries(schema.properties ?? {}).map(
([name, child]) => ` ${JSON.stringify(name)}${required.has(name) ? "" : "?"}: ${schemaType(child)};`,
);
return fields.length ? `{\n${fields.join("\n")}\n}` : "Record<string, never>";
}
return "unknown";
}
function operationResult(operation) {
const response = operation.responses?.["200"];
if (!response) return "unknown";
if (response.content) {
const media = response.content["application/json"] ?? response.content["text/event-stream"];
if (media?.schema) return schemaType(media.schema);
}
return response.schema ? schemaType(response.schema) : "unknown";
}
function operationMediaType(operation) {
const content = operation.responses?.["200"]?.content;
if (!content) return undefined;
return Object.keys(content)[0];
}
function operations(document) {
const result = [];
for (const [path, pathItem] of Object.entries(document.paths ?? {})) {
for (const method of ["get", "post", "put", "patch", "delete"]) {
const operation = pathItem[method];
if (operation?.operationId) result.push({ method, operation, path });
}
}
return result.sort((left, right) => left.operation.operationId.localeCompare(right.operation.operationId));
}
export function generateClient(input, output) {
const document = JSON.parse(readFileSync(input, "utf8"));
const schemas = Object.entries(document.components?.schemas ?? {})
.map(([key, schema]) => [schema.title ?? key, schema])
.sort(([left], [right]) => left.localeCompare(right));
const types = [
"// Generated from openapi/openapi.json. Do not edit by hand.",
...schemas.map(([name, schema]) => `export type ${identifier(name)} = ${schemaType(schema)};`),
].join("\n\n");
const operationList = operations(document);
const importedTypes = [...new Set(operationList.map(({ operation }) => operationResult(operation)).filter((type) => /^[A-Za-z_$][A-Za-z0-9_$]*$/.test(type)))];
const sdk = [
"// Generated from openapi/openapi.json. Do not edit by hand.",
importedTypes.length ? `import type { ${importedTypes.join(", ")} } from "./types.gen.js";` : "",
"export interface ClientOptions { baseUrl?: string; fetch?: typeof globalThis.fetch; headers?: HeadersInit; }",
...operationList.map(({ method, operation, path }) => {
const resultType = operationResult(operation);
if (operationMediaType(operation) === "text/event-stream") {
return `export function ${identifier(operation.operationId)}(options: Pick<ClientOptions, "baseUrl"> = {}): string {\n return \`${"${options.baseUrl ?? \"\"}"}${path}\`;\n}`;
}
return `export async function ${identifier(operation.operationId)}(options: ClientOptions = {}): Promise<${resultType}> {\n const request = options.fetch ?? globalThis.fetch;\n const response = await request(\`${"${options.baseUrl ?? \"\"}"}${path}\`, { method: "${method.toUpperCase()}", headers: options.headers ?? {} });\n if (!response.ok) throw new Error(\`HTTP ${"${response.status}"}\`);\n return response.json() as Promise<${resultType}>;\n}`;
}),
"",
].filter(Boolean).join("\n\n");
mkdirSync(output, { recursive: true });
writeFileSync(resolve(output, "sdk.gen.ts"), `${sdk}\n`);
writeFileSync(resolve(output, "types.gen.ts"), `${types}\n`);
}
+69
View File
@@ -0,0 +1,69 @@
import { execFileSync } from "node:child_process";
import { readFileSync, readdirSync, statSync } from "node:fs";
import { relative, resolve } from "node:path";
function runPnpm(args) {
if (process.platform === "win32") {
execFileSync(process.env.ComSpec ?? "cmd.exe", ["/d", "/s", "/c", `pnpm ${args.join(" ")}`], {
stdio: "inherit",
});
return;
}
execFileSync("pnpm", args, { stdio: "inherit" });
}
export function buildApiContracts() {
runPnpm(["--filter", "@dada/shared-contracts", "build"]);
runPnpm(["--filter", "@dada/api", "build"]);
}
export async function createOpenApiDocument() {
const moduleUrl = new URL(`../../apps/api/dist/app.js?build=${Date.now()}`, import.meta.url);
const { createApp } = await import(moduleUrl.href);
const app = await createApp();
await app.ready();
const document = app.swagger();
await app.close();
return sortObject(document);
}
export function formattedJson(value) {
return `${JSON.stringify(sortObject(value), null, 2)}\n`;
}
export function readTree(root) {
const files = new Map();
function visit(path) {
for (const name of readdirSync(path)) {
const child = resolve(path, name);
if (statSync(child).isDirectory()) visit(child);
else files.set(relative(root, child).replaceAll("\\", "/"), readFileSync(child, "utf8"));
}
}
visit(root);
return files;
}
export function compareTrees(expectedRoot, actualRoot) {
const expected = readTree(expectedRoot);
const actual = readTree(actualRoot);
const names = new Set([...expected.keys(), ...actual.keys()]);
return [...names]
.sort()
.filter((name) => expected.get(name) !== actual.get(name))
.map((name) => ({
actual: actual.has(name),
expected: expected.has(name),
file: name,
}));
}
function sortObject(value) {
if (Array.isArray(value)) return value.map(sortObject);
if (!value || typeof value !== "object") return value;
return Object.fromEntries(
Object.entries(value)
.sort(([left], [right]) => left.localeCompare(right))
.map(([key, child]) => [key, sortObject(child)]),
);
}
+168
View File
@@ -0,0 +1,168 @@
import { createHash } from "node:crypto";
import { spawnSync } from "node:child_process";
import {
copyFileSync,
existsSync,
mkdirSync,
readFileSync,
readdirSync,
statSync,
writeFileSync,
} from "node:fs";
import { resolve } from "node:path";
const runId = process.env.DADA_TDD_RUN_ID ?? `wp0-02-green-${new Date().toISOString().replace(/[^0-9]/g, "")}`;
const runDirectory = resolve("artifacts", "tdd", runId);
const apiCaseId = "TDD-WP0-API-001-schema-envelope";
const eventCaseId = "TDD-WP0-EVT-001-rest-refetch";
const apiDirectory = resolve(runDirectory, "cases", apiCaseId);
const eventDirectory = resolve(runDirectory, "cases", eventCaseId);
const playwrightDirectory = resolve(eventDirectory, "playwright");
if (existsSync(runDirectory)) throw new Error(`Evidence run already exists: ${runId}`);
mkdirSync(apiDirectory, { recursive: true });
mkdirSync(eventDirectory, { recursive: true });
const commandDefinitions = [
{ command: "pnpm test:unit", args: ["test:unit"] },
{ command: "pnpm test:api", args: ["test:api"] },
{ command: "pnpm test:e2e", args: ["test:e2e"] },
{ command: "pnpm validate:tdd-trace", args: ["validate:tdd-trace"] },
];
const startedAt = new Date().toISOString();
const commands = [];
for (const definition of commandDefinitions) {
const commandStartedAt = new Date().toISOString();
const executable = process.platform === "win32" ? (process.env.ComSpec ?? "cmd.exe") : "pnpm";
const args = process.platform === "win32"
? ["/d", "/s", "/c", `pnpm ${definition.args.join(" ")}`]
: definition.args;
const execution = spawnSync(executable, args, {
env: {
...process.env,
DADA_EVIDENCE_DIR_API: apiDirectory,
DADA_EVIDENCE_DIR_EVT: eventDirectory,
DADA_PLAYWRIGHT_OUTPUT_DIR: playwrightDirectory,
},
stdio: "inherit",
});
commands.push({
command: definition.command,
exit_code: execution.status ?? 1,
finished_at: new Date().toISOString(),
started_at: commandStartedAt,
});
}
function findFile(root, target) {
if (!existsSync(root)) return undefined;
for (const name of readdirSync(root)) {
const child = resolve(root, name);
if (statSync(child).isDirectory()) {
const nested = findFile(child, target);
if (nested) return nested;
} else if (name === target) return child;
}
return undefined;
}
const trace = findFile(playwrightDirectory, "trace.zip");
if (trace) copyFileSync(trace, resolve(eventDirectory, "trace.zip"));
const commandEvidence = {
commands,
phase: "green",
run_id: runId,
schema_version: "1.0",
};
writeFileSync(resolve(apiDirectory, "commands.json"), `${JSON.stringify(commandEvidence, null, 2)}\n`);
writeFileSync(resolve(eventDirectory, "commands.json"), `${JSON.stringify(commandEvidence, null, 2)}\n`);
const manifestBytes = readFileSync("tasks.manifest.json");
const manifest = {
path: "tasks.manifest.json",
sha256: createHash("sha256").update(manifestBytes).digest("hex").toUpperCase(),
};
const commit = spawnSync("git", ["rev-parse", "--short", "HEAD"], { encoding: "utf8" }).stdout.trim();
const worktreeDirty = spawnSync("git", ["status", "--porcelain"], { encoding: "utf8" }).stdout.trim().length > 0;
const environment = {
arch: process.arch,
node: process.version.slice(1),
os: process.platform,
};
const cases = [
{
acceptance_criteria: [],
directory: apiDirectory,
evidence_refs: ["openapi.json", "snapshot-diff.json", "response.json", "redaction.json"],
green_assertions: [
"OpenAPI 3.1 snapshot and runtime schemas agree",
"generated client matches the OpenAPI operations",
"responses include a valid correlation ID",
"error details accept only whitelisted non-sensitive fields",
],
layer: ["UNIT", "API"],
parent_family: "TDD-WP0-API-001",
requirements: ["NFR-05", "DevelopmentPlan 6.1", "DevelopmentPlan 6.6"],
test_id: apiCaseId,
},
{
acceptance_criteria: ["AC-20", "AC-30"],
directory: eventDirectory,
evidence_refs: ["sse-events.json", "network-timeline.json", "trace.zip"],
green_assertions: [
"events contain only fixed non-sensitive fields",
"each event causes a REST refetch",
"disconnects and event ID gaps cause bootstrap recovery",
"configuration and runtime availability versions remain separate",
],
layer: ["API", "E2E"],
parent_family: "TDD-WP0-EVT-001",
requirements: ["PROJECT-04", "GEN-14", "NFR-05", "DevelopmentPlan 6.2"],
test_id: eventCaseId,
},
];
const commandsPassed = commands.every((command) => command.exit_code === 0);
const results = cases.map((testCase) => {
const missingEvidence = testCase.evidence_refs.filter((file) => !existsSync(resolve(testCase.directory, file)));
const passed = commandsPassed && missingEvidence.length === 0;
const result = {
acceptance_criteria: testCase.acceptance_criteria,
automation: ["automated"],
commit,
environment,
evidence_refs: testCase.evidence_refs,
finished_at: new Date().toISOString(),
fixture_ids: [],
green_assertions: testCase.green_assertions,
layer: testCase.layer,
manifest,
missing_evidence: missingEvidence,
parent_family: testCase.parent_family,
phase: "green",
release_gate: ["work_package:WP-0", "release:P0-A"],
requirements: testCase.requirements,
run_id: runId,
schema_version: "1.0",
started_at: startedAt,
status: passed ? "passed" : "failed",
task_id: "TASK-WP0-02",
test_id: testCase.test_id,
work_package: "WP-0",
worktree_under_test: worktreeDirty ? "uncommitted implementation" : "clean committed implementation",
};
writeFileSync(resolve(testCase.directory, "result.json"), `${JSON.stringify(result, null, 2)}\n`);
return result;
});
const summary = {
cases: results.map(({ missing_evidence, status, test_id }) => ({ missing_evidence, status, test_id })),
run_id: runId,
status: results.every((result) => result.status === "passed") ? "passed" : "failed",
};
writeFileSync(resolve(runDirectory, "evidence.json"), `${JSON.stringify(summary, null, 2)}\n`);
console.log(JSON.stringify(summary, null, 2));
if (summary.status !== "passed") process.exit(1);
+1 -1
View File
@@ -4,7 +4,7 @@ import { createApp } from "../../apps/api/src/app.js";
describe("Fastify skeleton", () => { describe("Fastify skeleton", () => {
it("becomes ready without adding a product route", async () => { it("becomes ready without adding a product route", async () => {
const app = createApp(); const app = await createApp();
await app.ready(); await app.ready();
expect(app.printRoutes()).not.toContain("health"); expect(app.printRoutes()).not.toContain("health");
await app.close(); 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 () => { it("loads and closes Fastify with the frozen Swagger plugin", async () => {
const app = createApp(); const app = await createApp();
await app.ready(); await app.ready();
expect(app.hasPlugin("@fastify/swagger")).toBe(true); expect(app.hasPlugin("@fastify/swagger")).toBe(true);
await app.close(); 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 }));
});
});