test(contracts): per-client wire-contract suites + §3 production-guard tests

38 tests: recording provider double answers with the sibling services' real
response shapes; each suite asserts the exact request wire shape (path,
method, tenant/actor/idempotency headers, camelCase vs snake_case body) so a
regression to the pre-rewrite invented shapes fails loudly. Includes the
fail-closed policy cases (missing decision => deny, CL0UD down => typed
error, never implicit allow) and the production factory guard for all four
clients.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Simon Axelsson
2026-08-31 16:37:57 +02:00
co-authored by Claude Fable 5
parent b7038d7675
commit da58176735
7 changed files with 967 additions and 0 deletions
@@ -0,0 +1,170 @@
/**
* CONTRACT test for src/clients/aud0-client.ts against AUD0's REAL wire
* contract (sax3l/aud0 main `src/api/routes.ts` + `src/api/routes/
* openlineage.ts`, verified 2026-08-31):
*
* POST /v1/aud0/events — snake_case evidence event
* POST /v1/aud0/openlineage?tenant_id=&app_id= — OpenLineage RunEvent;
* tenant_id is a QUERY PARAM (route contract), app_id defaults
* "openlineage" server-side when omitted.
*
* /v1/aud0/lineage/events (the pre-rewrite path) does not exist on AUD0 and
* never has — lineage ingest is OpenLineage-only. This test also pins the
* legacy-shape → OpenLineage conversion.
*/
import { describe, it, expect, afterEach } from "vitest";
import {
createAud0Client,
legacyLineageToOpenLineage,
type Aud0Event,
type Aud0LineageEvent,
} from "@/src/clients/aud0-client";
import type { OpenLineageRunEvent } from "@/src/lineage/openlineage";
import { startContractServer, type ContractServer } from "./contract-test-server";
let server: ContractServer | undefined;
afterEach(async () => {
await server?.close();
server = undefined;
});
function event(overrides: Partial<Aud0Event> = {}): Aud0Event {
return {
event_id: "evt_1",
event_type: "0pt.dataset_registered",
tenant_id: "t_test",
project_id: "p_test",
actor_id: "svc_0pt",
app_id: "0pt",
capability_id: "0pt.register_dataset",
resource_type: "dataset",
resource_id: "ds_1",
risk_level: "L2",
created_at: "2026-08-31T12:00:00.000Z",
correlation_id: "corr-1",
purpose: "catalog",
result: "success",
output_refs: ["0pt:dataset:ds_1"],
...overrides,
};
}
describe("Aud0Client wire contract — events", () => {
it("POSTs the snake_case evidence event to /v1/aud0/events with tenant/actor headers", async () => {
server = await startContractServer([
{ method: "POST", path: "/v1/aud0/events", status: 201, respond: (r) => r.body as object },
]);
const client = createAud0Client(server.baseUrl);
await client.ingestEvent(event());
const req = server.requests[0];
expect(req.path).toBe("/v1/aud0/events");
expect(req.headers["x-tenant-id"]).toBe("t_test");
expect(req.headers["x-actor-id"]).toBe("svc_0pt");
const body = req.body as Record<string, unknown>;
// §16 evidence-context fields ride on the wire.
expect(body.event_type).toBe("0pt.dataset_registered");
expect(body.capability_id).toBe("0pt.register_dataset");
expect(body.risk_level).toBe("L2");
expect(body.purpose).toBe("catalog");
expect(body.result).toBe("success");
expect(body.output_refs).toEqual(["0pt:dataset:ds_1"]);
expect(body.correlation_id).toBe("corr-1");
});
it("treats HTTP 200 (deduplicated idempotent replay) as success", async () => {
server = await startContractServer([
{ method: "POST", path: "/v1/aud0/events", status: 200, respond: () => ({ deduplicated: true }) },
]);
const client = createAud0Client(server.baseUrl);
await expect(client.ingestEvent(event())).resolves.toBeTruthy();
});
});
describe("Aud0Client wire contract — OpenLineage lineage ingest", () => {
const runEvent: OpenLineageRunEvent = {
eventType: "COMPLETE",
eventTime: "2026-08-31T12:00:00.000Z",
producer: "https://git.siax.io/sax3l/0pt",
run: { runId: "run-1" },
job: { namespace: "0pt", name: "0pt.dataset.version.v1" },
inputs: [{ namespace: "0pt", name: "source/src_1" }],
outputs: [{ namespace: "0pt", name: "dataset/ds_1" }],
};
it("POSTs the RunEvent to /v1/aud0/openlineage with tenant_id + app_id as QUERY PARAMS", async () => {
server = await startContractServer([
{
method: "POST",
path: "/v1/aud0/openlineage",
status: 201,
respond: () => ({ tenant_id: "t_test", run_id: "run-1", job: "0pt/0pt.dataset.version.v1", event_id: "evt_ol_1" }),
},
]);
const client = createAud0Client(server.baseUrl);
const ack = await client.ingestOpenLineageEvent("t_test", "0pt", runEvent);
const req = server.requests[0];
expect(req.path).toBe("/v1/aud0/openlineage");
expect(req.query).toEqual({ tenant_id: "t_test", app_id: "0pt" });
const body = req.body as Record<string, unknown>;
expect(body.eventType).toBe("COMPLETE");
expect((body.run as { runId: string }).runId).toBe("run-1");
expect((body.job as { namespace: string; name: string })).toEqual({
namespace: "0pt",
name: "0pt.dataset.version.v1",
});
expect(ack.event_id).toBe("evt_ol_1");
});
it("legacy shim: ingestLineageEvent converts to OpenLineage and hits /v1/aud0/openlineage (never /v1/aud0/lineage/events)", async () => {
server = await startContractServer([
{
method: "POST",
path: "/v1/aud0/openlineage",
status: 201,
respond: () => ({ tenant_id: "t_test", run_id: "lin_1", job: "0pt/0pt.write", event_id: "evt_ol_2" }),
},
]);
const client = createAud0Client(server.baseUrl);
const legacy: Aud0LineageEvent = {
lineage_event_id: "lin_1",
tenant_id: "t_test",
project_id: "p_test",
app_id: "0pt",
capability_id: "0pt.write",
source_type: "external_record",
source_id: "idem-1",
target_type: "external_id",
target_id: "ext_1",
lineage_type: "produced",
created_at: "2026-08-31T12:00:00.000Z",
correlation_id: "corr-1",
};
await client.ingestLineageEvent(legacy);
expect(server.requests).toHaveLength(1);
expect(server.requests[0].path).toBe("/v1/aud0/openlineage");
const body = server.requests[0].body as OpenLineageRunEvent;
expect(body.run.runId).toBe("lin_1"); // deterministic → AUD0 dedup key stable
expect(body.inputs?.[0]?.name).toBe("external_record/idem-1");
expect(body.outputs?.[0]?.name).toBe("external_id/ext_1");
});
it("conversion is deterministic (same legacy event → identical RunEvent)", () => {
const legacy: Aud0LineageEvent = {
lineage_event_id: "lin_x",
tenant_id: "t",
project_id: "p",
app_id: "0pt",
capability_id: "cap",
source_type: "a",
source_id: "1",
target_type: "b",
target_id: "2",
lineage_type: "derived_from",
created_at: "2026-08-31T00:00:00.000Z",
};
expect(legacyLineageToOpenLineage(legacy)).toEqual(legacyLineageToOpenLineage({ ...legacy }));
});
});
@@ -0,0 +1,170 @@
/**
* CONTRACT test for src/clients/cl0ud-client.ts against CL0UD's REAL wire
* contract (sax3l/cl0ud main — policy evaluate + capability discovery,
* verified 2026-08-31):
*
* POST /api/policy/evaluate — camelCase body, `{ data, meta }` envelope,
* decision ∈ {allow, deny} + requiresApproval flag.
* GET /api/capabilities — capability-registry discovery.
*
* The pre-rewrite client POSTed snake_case to /v1/cl0ud/policies/evaluate —
* a path that never existed on CL0UD. Policy stays FAIL-CLOSED: missing or
* malformed decision reads as deny.
*/
import { describe, it, expect, afterEach } from "vitest";
import { createCl0udClient, type EvaluatePolicyInput } from "@/src/clients/cl0ud-client";
import { SiaxError } from "@/src/platform/errors";
import { startContractServer, type ContractServer } from "./contract-test-server";
let server: ContractServer | undefined;
afterEach(async () => {
await server?.close();
server = undefined;
});
function input(overrides: Partial<EvaluatePolicyInput> = {}): EvaluatePolicyInput {
return {
tenant_id: "t_test",
project_id: "p_test",
actor_id: "svc_0pt",
capability_id: "0pt.write_external_record",
resource_type: "external_record",
resource_id: "rec_1",
risk_level: "L4",
correlation_id: "corr-1",
...overrides,
};
}
describe("Cl0udClient wire contract — policy evaluate", () => {
it("POSTs the REAL camelCase body to /api/policy/evaluate and maps the {data} envelope", async () => {
server = await startContractServer([
{
method: "POST",
path: "/api/policy/evaluate",
status: 200,
respond: () => ({
data: {
decision: "allow",
requiresApproval: false,
auditRequired: true,
riskLevel: "L4",
decisionId: "pdec_r3al",
reason: "rbac_ok",
},
meta: {},
}),
},
]);
const client = createCl0udClient(server.baseUrl);
const decision = await client.evaluatePolicy(input());
const req = server.requests[0];
expect(req.path).toBe("/api/policy/evaluate");
const body = req.body as Record<string, unknown>;
expect(body.capability).toBe("0pt.write_external_record");
expect(body.tenantId).toBe("t_test");
expect(body.projectId).toBe("p_test");
expect(body.actor).toEqual({ kind: "operator", role: "member" });
expect(body.surface).toBe("api");
expect(body.resource).toEqual({ type: "external_record", id: "rec_1" });
expect(body.correlationId).toBe("corr-1");
// Invented pre-rewrite fields must NOT be on the wire.
expect(body.capability_id).toBeUndefined();
expect(body.tenant_id).toBeUndefined();
expect(body.risk_level).toBeUndefined();
expect(decision).toEqual({
decision: "allow",
decision_id: "pdec_r3al",
reason: "rbac_ok",
deny_code: undefined,
});
});
it("maps allow + requiresApproval=true onto 0pt's approval_required (gates the caller)", async () => {
server = await startContractServer([
{
method: "POST",
path: "/api/policy/evaluate",
status: 200,
respond: () => ({ data: { decision: "allow", requiresApproval: true, decisionId: "pdec_a" }, meta: {} }),
},
]);
const client = createCl0udClient(server.baseUrl);
const decision = await client.evaluatePolicy(input());
expect(decision.decision).toBe("approval_required");
});
it("maps deny with denyCode", async () => {
server = await startContractServer([
{
method: "POST",
path: "/api/policy/evaluate",
status: 200,
respond: () => ({
data: { decision: "deny", requiresApproval: false, decisionId: "pdec_d", denyCode: "rbac_forbidden" },
meta: {},
}),
},
]);
const client = createCl0udClient(server.baseUrl);
const decision = await client.evaluatePolicy(input());
expect(decision.decision).toBe("deny");
expect(decision.deny_code).toBe("rbac_forbidden");
});
it("FAILS CLOSED: 200 with a body missing `decision` is a deny, never an allow", async () => {
server = await startContractServer([
{ method: "POST", path: "/api/policy/evaluate", status: 200, respond: () => ({ data: {}, meta: {} }) },
]);
const client = createCl0udClient(server.baseUrl);
const decision = await client.evaluatePolicy(input());
expect(decision.decision).toBe("deny");
});
it("FAILS CLOSED: CL0UD unreachable → SiaxError, never an implicit allow (§52)", async () => {
server = await startContractServer([
{ method: "POST", path: "/api/policy/evaluate", status: 503, respond: () => ({ error: { code: "DOWN" } }) },
]);
const client = createCl0udClient(server.baseUrl);
try {
await client.evaluatePolicy(input());
expect.unreachable("must throw");
} catch (err) {
expect(err).toBeInstanceOf(SiaxError);
expect((err as SiaxError).code).toBe("DEPENDENCY_UNAVAILABLE");
}
});
});
describe("Cl0udClient wire contract — capability discovery", () => {
it("GETs /api/capabilities with filters and returns the data array", async () => {
server = await startContractServer([
{
method: "GET",
path: "/api/capabilities",
status: 200,
respond: () => ({
data: [
{
name: "0pt.search.query",
version: "1.0.0",
owner: "0pt",
status: "active",
requiredEntitlement: "0pt.search",
surfaces: ["api"],
},
],
meta: { total: 1 },
}),
},
]);
const client = createCl0udClient(server.baseUrl);
const caps = await client.listCapabilities({ owner: "0pt", status: "active" });
expect(server.requests[0].query).toEqual({ owner: "0pt", status: "active" });
expect(caps).toHaveLength(1);
expect(caps[0].name).toBe("0pt.search.query");
expect(caps[0].requiredEntitlement).toBe("0pt.search");
});
});
+86
View File
@@ -0,0 +1,86 @@
/**
* tests/contracts/contract-test-server.ts
* ========================================
* In-process HTTP provider double for the per-client CONTRACT tests
* (Master-DoD §10 "Contract tests finns", §50 Contract level).
*
* NOT a mock of 0pt's clients — a recording provider that answers with
* response bodies shaped EXACTLY like the sibling services' real handlers
* (verified against sax3l/st0re, sax3l/aud0, sax3l/n0d, sax3l/cl0ud main
* this session). Each test asserts the REQUEST the client actually put on
* the wire: path, method, headers (tenant/actor/context propagation,
* idempotency), and body field names. That is the drift alarm: if a client
* regresses to its pre-2026-08-31 invented wire shapes, these tests fail.
*/
import http from "node:http";
import type { AddressInfo } from "node:net";
export interface RecordedRequest {
method: string;
/** Path WITHOUT query string. */
path: string;
/** Raw query params. */
query: Record<string, string>;
headers: http.IncomingHttpHeaders;
body: unknown;
}
export interface ContractRoute {
method: string;
/** Exact path to match (no query). */
path: string;
status: number;
/** Response body factory — receives the recorded request. */
respond: (req: RecordedRequest) => unknown;
}
export interface ContractServer {
baseUrl: string;
requests: RecordedRequest[];
close(): Promise<void>;
}
export async function startContractServer(routes: ContractRoute[]): Promise<ContractServer> {
const requests: RecordedRequest[] = [];
const server = http.createServer((req, res) => {
const chunks: Buffer[] = [];
req.on("data", (c: Buffer) => chunks.push(c));
req.on("end", () => {
const url = new URL(req.url ?? "/", "http://localhost");
const raw = Buffer.concat(chunks).toString("utf8");
let body: unknown;
try {
body = raw ? JSON.parse(raw) : undefined;
} catch {
body = raw;
}
const recorded: RecordedRequest = {
method: req.method ?? "GET",
path: url.pathname,
query: Object.fromEntries(url.searchParams.entries()),
headers: req.headers,
body,
};
requests.push(recorded);
const route = routes.find((r) => r.method === recorded.method && r.path === recorded.path);
if (!route) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ error: { code: "NOT_FOUND", message: `no contract route ${recorded.method} ${recorded.path}` } }));
return;
}
res.writeHead(route.status, { "Content-Type": "application/json" });
res.end(JSON.stringify(route.respond(recorded)));
});
});
await new Promise<void>((resolve) => server.listen(0, "127.0.0.1", resolve));
const { port } = server.address() as AddressInfo;
return {
baseUrl: `http://127.0.0.1:${port}`,
requests,
close: () => new Promise<void>((resolve, reject) => server.close((e) => (e ? reject(e) : resolve()))),
};
}
@@ -0,0 +1,105 @@
/**
* Master-DoD §3/§10: "Production kan ALDRIG tyst falla tillbaka till
* mock/in-memory — fail hard med DEPENDENCY_UNAVAILABLE."
*
* These tests drive every client factory (and the store-mode resolver) in a
* production runtime with the dependency unconfigured and assert the HARD
* throw at construction time — plus the non-production behaviour that keeps
* unit tests hermetic.
*/
import { describe, it, expect } from "vitest";
import { resolveDependencyBaseUrl, resolveStoreMode } from "@/src/platform/dependency-mode";
import { SiaxError } from "@/src/platform/errors";
import { createCl0udClient } from "@/src/clients/cl0ud-client";
import { createSt0reClient } from "@/src/clients/st0re-client";
import { createN0dClient } from "@/src/clients/n0d-client";
import { createAud0Client } from "@/src/clients/aud0-client";
const PROD = { NODE_ENV: "production" } as NodeJS.ProcessEnv;
const DEV = { NODE_ENV: "test" } as NodeJS.ProcessEnv;
describe("resolveDependencyBaseUrl (the §3 kill switch)", () => {
it("production + unconfigured => throws SiaxError DEPENDENCY_UNAVAILABLE", () => {
try {
resolveDependencyBaseUrl("st0re", undefined, PROD);
expect.unreachable("must throw");
} catch (err) {
expect(err).toBeInstanceOf(SiaxError);
expect((err as SiaxError).code).toBe("DEPENDENCY_UNAVAILABLE");
expect((err as SiaxError).source).toBe("st0re");
}
});
it("production + empty string => throws (whitespace is not a configuration)", () => {
expect(() => resolveDependencyBaseUrl("aud0", " ", PROD)).toThrowError(SiaxError);
});
it("non-production + unconfigured => undefined (mock allowed for tests/dev only)", () => {
expect(resolveDependencyBaseUrl("n0d", undefined, DEV)).toBeUndefined();
});
it("configured => normalized URL in any runtime (trailing slashes stripped)", () => {
expect(resolveDependencyBaseUrl("cl0ud", "https://x.example.com///", PROD)).toBe("https://x.example.com");
});
});
describe("resolveStoreMode (§3 for stateful registries)", () => {
it("production + no database => throws DEPENDENCY_UNAVAILABLE", () => {
expect(() => resolveStoreMode("0pt.source_registry", undefined, PROD)).toThrowError(SiaxError);
});
it("non-production + no database => explicit memory mode", () => {
expect(resolveStoreMode("0pt.source_registry", undefined, DEV)).toEqual({ mode: "memory" });
});
it("database configured => postgres mode", () => {
expect(resolveStoreMode("x", "postgres://u@h/db", PROD)).toEqual({ mode: "postgres", url: "postgres://u@h/db" });
});
});
describe("client factories under NODE_ENV=production without a base URL", () => {
// The factories read process.env.NODE_ENV via isProductionRuntime; drive it
// for real, restoring afterwards.
function inProduction<T>(fn: () => T): T {
const prev = process.env.NODE_ENV;
const prevUrls = {
CL0UD_URL: process.env.CL0UD_URL,
ST0RE_URL: process.env.ST0RE_URL,
N0D_URL: process.env.N0D_URL,
AUD0_URL: process.env.AUD0_URL,
};
delete process.env.CL0UD_URL;
delete process.env.ST0RE_URL;
delete process.env.N0D_URL;
delete process.env.AUD0_URL;
(process.env as Record<string, string>).NODE_ENV = "production";
try {
return fn();
} finally {
if (prev === undefined) delete (process.env as Record<string, string | undefined>).NODE_ENV;
else (process.env as Record<string, string>).NODE_ENV = prev;
for (const [k, v] of Object.entries(prevUrls)) {
if (v === undefined) delete process.env[k];
else process.env[k] = v;
}
}
}
it("createCl0udClient() fails hard (policy may NEVER silently allow via local pass-through)", () => {
inProduction(() => expect(() => createCl0udClient()).toThrowError(SiaxError));
});
it("createSt0reClient() fails hard", () => {
inProduction(() => expect(() => createSt0reClient()).toThrowError(SiaxError));
});
it("createN0dClient() fails hard", () => {
inProduction(() => expect(() => createN0dClient()).toThrowError(SiaxError));
});
it("createAud0Client() fails hard", () => {
inProduction(() => expect(() => createAud0Client()).toThrowError(SiaxError));
});
it("all four factories still return local implementations in a test runtime", () => {
expect(createCl0udClient()).toBeTruthy();
expect(createSt0reClient()).toBeTruthy();
expect(createN0dClient()).toBeTruthy();
expect(createAud0Client()).toBeTruthy();
});
});
+153
View File
@@ -0,0 +1,153 @@
/**
* CONTRACT test for src/clients/n0d-client.ts against N0D's REAL wire
* contract (sax3l/n0d main `src/api/schemas.ts` createJobSchema +
* `src/api/mappers.ts` + job-lifecycle routes, verified 2026-08-31):
*
* POST /v1/n0d/jobs (202) — required: job_type, capability_id, tenant_id,
* required_role (enum), resource_requirements {cpu, memory_gb, disk_gb};
* idempotency is BODY-ONLY (idempotency_key); callback_url = CloudEvents.
* GET /v1/n0d/jobs/:id, GET /v1/n0d/jobs/:id/result
*
* The pre-2026-08-31 client sent required_node_roles[]/resource_profile/
* output_artifact_type/risk_level — fields N0D's createJobSchema does not
* accept. This test pins the mapped wire shape.
*/
import { describe, it, expect, afterEach } from "vitest";
import {
createN0dClient,
resolveRequiredRole,
resolveResourceRequirements,
type N0dCreateJobInput,
} from "@/src/clients/n0d-client";
import { SiaxError } from "@/src/platform/errors";
import { startContractServer, type ContractServer } from "./contract-test-server";
let server: ContractServer | undefined;
afterEach(async () => {
await server?.close();
server = undefined;
});
function input(overrides: Partial<N0dCreateJobInput> = {}): N0dCreateJobInput {
return {
tenant_id: "t_test",
project_id: "p_test",
job_type: "0pt.quality.run",
capability_id: "0pt.quality.run.v1",
requested_by_app_id: "0pt",
requested_by_actor_id: "svc_0pt",
input_artifact_ids: ["art_in_1"],
output_artifact_type: "quality_result",
requires_backbone: false,
requires_gpu: false,
required_node_roles: ["compute_general"],
resource_profile: "standard",
risk_level: "L2",
correlation_id: "corr-7",
idempotency_key: "idem-job-1",
...overrides,
};
}
const WIRE_JOB = {
job_id: "job_r3al",
job_type: "0pt.quality.run",
tenant_id: "t_test",
project_id: "p_test",
status: "queued",
capability_id: "0pt.quality.run.v1",
created_at: "2026-08-31T12:00:00.000Z",
};
describe("N0dClient wire contract", () => {
it("POSTs the REAL createJob body to /v1/n0d/jobs and accepts the 202", async () => {
server = await startContractServer([
{ method: "POST", path: "/v1/n0d/jobs", status: 202, respond: () => WIRE_JOB },
]);
const client = createN0dClient(server.baseUrl);
const job = await client.createJob(input());
const req = server.requests[0];
expect(req.method).toBe("POST");
expect(req.path).toBe("/v1/n0d/jobs");
expect(req.headers["x-tenant-id"]).toBe("t_test");
const body = req.body as Record<string, unknown>;
// Required fields per createJobSchema.
expect(body.job_type).toBe("0pt.quality.run");
expect(body.capability_id).toBe("0pt.quality.run.v1");
expect(body.tenant_id).toBe("t_test");
expect(body.required_role).toBe("compute_general");
expect(body.resource_requirements).toEqual({ cpu: 2, memory_gb: 4, gpu: 0, disk_gb: 10 });
// Mapped optionals.
expect(body.app_id).toBe("0pt");
expect(body.requested_by).toBe("svc_0pt");
expect(body.priority).toBe("normal");
expect(body.requires_shared_backbone).toBe(false);
expect(body.input).toEqual({ artifact_ids: ["art_in_1"] });
expect(body.output_policy).toEqual({ write_to_st0re: true, artifact_type: "quality_result" });
expect(body.idempotency_key).toBe("idem-job-1");
expect(body.correlation_id).toBe("corr-7");
// Legacy invented fields must NOT be on the wire.
expect(body.required_node_roles).toBeUndefined();
expect(body.resource_profile).toBeUndefined();
expect(body.output_artifact_type).toBeUndefined();
expect(body.risk_level).toBeUndefined();
expect(body.requested_by_app_id).toBeUndefined();
expect(job.job_id).toBe("job_r3al");
expect(job.status).toBe("queued");
});
it("GETs job + result with tenant header (poll path)", async () => {
server = await startContractServer([
{ method: "GET", path: "/v1/n0d/jobs/job_r3al", status: 200, respond: () => ({ ...WIRE_JOB, status: "completed" }) },
{
method: "GET",
path: "/v1/n0d/jobs/job_r3al/result",
status: 200,
respond: () => ({
job_id: "job_r3al",
outcome: "succeeded",
output_artifacts: [{ st0re_artifact_id: "art_out_1", artifact_type: "quality_result" }],
}),
},
]);
const client = createN0dClient(server.baseUrl);
const job = await client.getJob("job_r3al", "t_test");
expect(job.status).toBe("completed");
const result = await client.getJobResult("job_r3al", "t_test");
expect(result.outcome).toBe("succeeded");
expect(result.output_artifacts?.[0]?.st0re_artifact_id).toBe("art_out_1");
for (const req of server.requests) {
expect(req.headers["x-tenant-id"]).toBe("t_test");
}
});
it("role/resource inference: requires_gpu forces ai_gpu + a GPU profile", () => {
const gpuInput = input({ requires_gpu: true, required_node_roles: [], resource_profile: "standard" });
expect(resolveRequiredRole(gpuInput)).toBe("ai_gpu");
expect(resolveResourceRequirements(gpuInput).gpu).toBeGreaterThan(0);
// Explicit values always win.
const explicit = input({
required_role: "storage",
resource_requirements: { cpu: 1, memory_gb: 1, disk_gb: 1 },
});
expect(resolveRequiredRole(explicit)).toBe("storage");
expect(resolveResourceRequirements(explicit)).toEqual({ cpu: 1, memory_gb: 1, disk_gb: 1 });
});
it("maps a 404 (foreign-tenant probe / unknown job) to SiaxError NOT_FOUND", async () => {
server = await startContractServer([]);
const client = createN0dClient(server.baseUrl);
try {
await client.getJob("job_foreign", "t_other");
expect.unreachable("must throw");
} catch (err) {
expect(err).toBeInstanceOf(SiaxError);
expect((err as SiaxError).code).toBe("NOT_FOUND");
}
});
});
@@ -0,0 +1,133 @@
/**
* Pins 0pt's MIRROR of cl0ud's canonical request-context contract
* (src/platform/siax-context.ts + src/platform/errors.ts) to the canonical
* values from sax3l/cl0ud `packages/contract/src/request-context.ts`.
* If cl0ud evolves the contract, this test is the tripwire that forces the
* mirror (and this pin) to be updated deliberately, never by drift.
*/
import { describe, it, expect } from "vitest";
import {
SIAX_CONTEXT_HEADERS,
toPropagationHeaders,
validateSiaxRequestContext,
} from "@/src/platform/siax-context";
import {
SIAX_ERROR_CODES,
SiaxError,
httpStatusForSiaxErrorCode,
siaxErrorCodeForUpstreamStatus,
} from "@/src/platform/errors";
describe("SIAX context header names (pinned to cl0ud @siax/contract)", () => {
it("uses the canonical wire header names, verbatim", () => {
expect(SIAX_CONTEXT_HEADERS).toEqual({
tenant: "x-tenant-id",
actor: "x-actor-id",
capability: "x-siax-capability",
purpose: "x-siax-purpose",
project: "x-siax-project-id",
correlation: "x-siax-correlation-id",
causation: "x-siax-causation-id",
trace: "x-siax-trace-id",
idempotency: "idempotency-key",
});
});
it("serializes required fields always and optional fields only when present", () => {
const minimal = toPropagationHeaders({
tenantId: "t_x",
actorId: "svc_0pt",
capability: "0pt.search.query.v1",
correlationId: "corr-1",
});
expect(minimal).toEqual({
"x-tenant-id": "t_x",
"x-actor-id": "svc_0pt",
"x-siax-capability": "0pt.search.query.v1",
"x-siax-correlation-id": "corr-1",
});
const full = toPropagationHeaders({
tenantId: "t_x",
projectId: "p_1",
actorId: "svc_0pt",
capability: "0pt.search.query.v1",
purpose: "valuation",
correlationId: "corr-1",
causationId: "cause-1",
traceId: "trace-1",
idempotencyKey: "idem-1",
});
expect(full["x-siax-project-id"]).toBe("p_1");
expect(full["x-siax-purpose"]).toBe("valuation");
expect(full["x-siax-causation-id"]).toBe("cause-1");
expect(full["x-siax-trace-id"]).toBe("trace-1");
expect(full["idempotency-key"]).toBe("idem-1");
});
it("validates required fields (tenantId/actorId/capability/correlationId)", () => {
expect(
validateSiaxRequestContext({
tenantId: "t_x",
actorId: "a",
capability: "c",
correlationId: "x",
})
).toEqual([]);
const problems = validateSiaxRequestContext({ tenantId: " " });
expect(problems.join(" ")).toMatch(/tenantId/);
expect(problems.join(" ")).toMatch(/actorId/);
expect(problems.join(" ")).toMatch(/capability/);
expect(problems.join(" ")).toMatch(/correlationId/);
});
});
describe("Canonical SIAX error vocabulary (pinned to Master-DoD §6 / cl0ud)", () => {
it("carries exactly the 12 canonical codes, in canonical order", () => {
expect([...SIAX_ERROR_CODES]).toEqual([
"UNAUTHENTICATED",
"AUTHORIZATION_DENIED",
"ENTITLEMENT_REQUIRED",
"CAPABILITY_UNAVAILABLE",
"QUOTA_EXCEEDED",
"RATE_LIMITED",
"INVALID_REQUEST",
"NOT_FOUND",
"CONFLICT",
"DEPENDENCY_UNAVAILABLE",
"RETRYABLE_FAILURE",
"PERMANENT_FAILURE",
]);
});
it("maps codes to the canonical HTTP statuses (pinned to cl0ud's mapping)", () => {
expect(httpStatusForSiaxErrorCode("UNAUTHENTICATED")).toBe(401);
expect(httpStatusForSiaxErrorCode("AUTHORIZATION_DENIED")).toBe(403);
expect(httpStatusForSiaxErrorCode("ENTITLEMENT_REQUIRED")).toBe(402);
expect(httpStatusForSiaxErrorCode("QUOTA_EXCEEDED")).toBe(402);
expect(httpStatusForSiaxErrorCode("CAPABILITY_UNAVAILABLE")).toBe(404);
expect(httpStatusForSiaxErrorCode("RATE_LIMITED")).toBe(429);
expect(httpStatusForSiaxErrorCode("INVALID_REQUEST")).toBe(400);
expect(httpStatusForSiaxErrorCode("NOT_FOUND")).toBe(404);
expect(httpStatusForSiaxErrorCode("CONFLICT")).toBe(409);
expect(httpStatusForSiaxErrorCode("DEPENDENCY_UNAVAILABLE")).toBe(503);
expect(httpStatusForSiaxErrorCode("RETRYABLE_FAILURE")).toBe(503);
expect(httpStatusForSiaxErrorCode("PERMANENT_FAILURE")).toBe(500);
});
it("classifies upstream statuses with retryable-vs-permanent explicit (§6)", () => {
expect(siaxErrorCodeForUpstreamStatus(503)).toBe("DEPENDENCY_UNAVAILABLE");
expect(siaxErrorCodeForUpstreamStatus(429)).toBe("RATE_LIMITED");
expect(siaxErrorCodeForUpstreamStatus(500)).toBe("RETRYABLE_FAILURE");
const retryable = new SiaxError("DEPENDENCY_UNAVAILABLE", "st0re", "down");
expect(retryable.retryable).toBe(true);
const permanent = new SiaxError("PERMANENT_FAILURE", "st0re", "broken");
expect(permanent.retryable).toBe(false);
});
it("carries correlation id in the error and its JSON form (§6)", () => {
const err = new SiaxError("NOT_FOUND", "n0d", "job missing", { correlationId: "corr-9" });
expect(err.correlationId).toBe("corr-9");
expect(err.toJSON()).toMatchObject({ error: "NOT_FOUND", source: "n0d", correlation_id: "corr-9" });
});
});
@@ -0,0 +1,150 @@
/**
* CONTRACT test for src/clients/st0re-client.ts against ST0RE's REAL wire
* contract (sax3l/st0re main `src/api/routes/artifacts.ts` +
* `src/api/validation.ts` ArtifactInputSchema, verified 2026-08-31):
*
* POST /v1/st0re/artifacts/register
* headers: X-Tenant-Id (required), X-Actor-Id (required),
* X-Idempotency-Key (optional)
* body: camelCase — tenantId, projectId, appId, artifactType,
* displayName, producedBy, primaryObjectId, schemaVersion,
* retentionPolicy, createdBy, checksumSha256 (64 hex)
* 201 → { artifactId, createdAt, ... } / 200 → idempotent replay
*
* The pre-2026-08-31 client sent snake_case + object_location{} — that shape
* fails ST0RE's Zod validation with a 400. This test is the tripwire.
*/
import { describe, it, expect, afterEach } from "vitest";
import { createSt0reClient, type St0reArtifactInput } from "@/src/clients/st0re-client";
import { SiaxError } from "@/src/platform/errors";
import { startContractServer, type ContractServer } from "./contract-test-server";
let server: ContractServer | undefined;
afterEach(async () => {
await server?.close();
server = undefined;
});
const CHECKSUM = "a".repeat(64);
function input(overrides: Partial<St0reArtifactInput> = {}): St0reArtifactInput {
return {
tenant_id: "t_test",
project_id: "p_test",
artifact_type: "dataset_export",
app_id: "0pt",
produced_by: "0pt.dataset.export",
object_location: { provider: "minio", bucket: "siax-tenants", key: "t_test/exports/x.parquet" },
content_type: "application/vnd.apache.parquet",
size_bytes: 1234,
checksum_sha256: CHECKSUM,
pii_level: "none",
retention_policy: "standard",
data_residency: "eu",
risk_level: "L2",
correlation_id: "corr-42",
idempotency_key: "idem-artifact-1",
...overrides,
};
}
describe("St0reClient wire contract", () => {
it("POSTs the REAL camelCase ArtifactInput to /v1/st0re/artifacts/register with tenant/actor/idempotency headers", async () => {
server = await startContractServer([
{
method: "POST",
path: "/v1/st0re/artifacts/register",
status: 201,
respond: () => ({ artifactId: "art_r3al", tenantId: "t_test", createdAt: 1756640000000 }),
},
]);
const client = createSt0reClient(server.baseUrl);
const artifact = await client.createArtifact(input());
expect(server.requests).toHaveLength(1);
const req = server.requests[0];
expect(req.method).toBe("POST");
expect(req.path).toBe("/v1/st0re/artifacts/register");
// Required ST0RE headers.
expect(req.headers["x-tenant-id"]).toBe("t_test");
expect(req.headers["x-actor-id"]).toBe("0pt.dataset.export");
expect(req.headers["x-idempotency-key"]).toBe("idem-artifact-1");
// REAL body contract: camelCase, required fields present, s3:// URI.
const body = req.body as Record<string, unknown>;
expect(body.tenantId).toBe("t_test");
expect(body.projectId).toBe("p_test");
expect(body.appId).toBe("0pt");
expect(body.artifactType).toBe("dataset_export");
expect(body.displayName).toBe("x.parquet");
expect(body.producedBy).toBe("0pt.dataset.export");
expect(body.primaryObjectId).toBe("s3://siax-tenants/t_test/exports/x.parquet");
expect(body.schemaVersion).toBe("v1");
expect(body.retentionPolicy).toBe("standard");
expect(body.createdBy).toBe("0pt.dataset.export");
expect(body.checksumSha256).toBe(CHECKSUM);
expect(body.idempotencyKey).toBe("idem-artifact-1");
// The invented pre-rewrite fields must NOT be on the wire.
expect(body.object_location).toBeUndefined();
expect(body.tenant_id).toBeUndefined();
// Response mapping.
expect(artifact.artifact_id).toBe("art_r3al");
expect(artifact.created_at).toBe(new Date(1756640000000).toISOString());
});
it("treats HTTP 200 as an idempotent replay success (ST0RE's documented semantics)", async () => {
server = await startContractServer([
{
method: "POST",
path: "/v1/st0re/artifacts/register",
status: 200,
respond: () => ({ artifactId: "art_existing", createdAt: 1756000000000 }),
},
]);
const client = createSt0reClient(server.baseUrl);
const artifact = await client.createArtifact(input());
expect(artifact.artifact_id).toBe("art_existing");
});
it("maps a 400 to SiaxError INVALID_REQUEST without leaking the upstream body", async () => {
server = await startContractServer([
{
method: "POST",
path: "/v1/st0re/artifacts/register",
status: 400,
respond: () => ({ error: { code: "ST0RE_VALIDATION_ERROR", message: "secret-internal-detail" } }),
},
]);
const client = createSt0reClient(server.baseUrl);
try {
await client.createArtifact(input());
expect.unreachable("must throw");
} catch (err) {
expect(err).toBeInstanceOf(SiaxError);
expect((err as SiaxError).code).toBe("INVALID_REQUEST");
expect((err as SiaxError).message).toContain("ST0RE_VALIDATION_ERROR");
expect((err as SiaxError).message).not.toContain("secret-internal-detail");
}
});
it("maps a 503 to a RETRYABLE SiaxError DEPENDENCY_UNAVAILABLE", async () => {
server = await startContractServer([
{
method: "POST",
path: "/v1/st0re/artifacts/register",
status: 503,
respond: () => ({ error: { code: "ST0RE_UNAVAILABLE" } }),
},
]);
const client = createSt0reClient(server.baseUrl);
try {
await client.createArtifact(input());
expect.unreachable("must throw");
} catch (err) {
expect((err as SiaxError).code).toBe("DEPENDENCY_UNAVAILABLE");
expect((err as SiaxError).retryable).toBe(true);
}
});
});