Skip to content

Commit 545eb7f

Browse files
authored
Merge pull request #64 from CeceOs92/feat/50-51-52-55-telemetry-workers-exports-contract
feat/telemetry-workers-exports-contract
2 parents de931f1 + d5c8b15 commit 545eb7f

21 files changed

Lines changed: 2006 additions & 56 deletions

File tree

‎README.md‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,10 @@ contributors work in parallel without ever conflicting.
2020
See [docs/ARCHITECTURE.md](./docs/ARCHITECTURE.md) and
2121
[docs/FEATURE_CONTRACT.md](./docs/FEATURE_CONTRACT.md).
2222

23+
Infrastructure docs: [Telemetry](./docs/TELEMETRY.md) ·
24+
[Workers](./docs/WORKERS.md) · [Exports](./docs/EXPORTS.md) ·
25+
[API Contract](./docs/API_CONTRACT.md).
26+
2327
## Tools
2428

2529
| Tool | What it does |
@@ -66,6 +70,9 @@ npm run registry # regenerate the feature registry
6670
npm run new:feature # scaffold a complete feature slice
6771
npm run verify:features # check every slice against the feature contract
6872
npm run verify:issues # check 40+ independent issues and the advanced wave
73+
npm run verify:telemetry # verify >= 5 operations are instrumented
74+
npm run verify:contract # run the API contract drift tests
75+
npm run test:workers # run the background worker suite
6976
npm run issues # preview the next five GrantFox issue payloads
7077
npm run test # unit, hook, component and accessibility tests
7178
npm run test:coverage # unit tests with a coverage summary
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import { describe, expect, it } from "vitest";
2+
import {
3+
TELEMETRY_FIELDS,
4+
measure,
5+
newCorrelationId,
6+
resetTelemetrySink,
7+
setTelemetrySink,
8+
type TelemetryEvent
9+
} from "@/core/telemetry/telemetry";
10+
import { classifyHorizonError } from "@/core/horizon/errors";
11+
import { createWorkerFramework, type JobPayload } from "@/core/workers/queue";
12+
import { ok } from "@/core/result/result";
13+
import {
14+
EXPORT_CURRENT_SCHEMA_VERSION,
15+
exportRecords,
16+
type ExportRecordSource
17+
} from "@/core/export/exporter";
18+
import type { FeatureManifest } from "@/core/registry/types";
19+
import { manifest as paymentQrManifest } from "@/features/payment-qr/manifest";
20+
import {
21+
assertContract,
22+
contractOperation,
23+
validateContract
24+
} from "@/core/contract/contract";
25+
26+
const V = "1.0";
27+
28+
// The five contracts documented in docs/API_CONTRACT.md, kept in lockstep with
29+
// the real modules below. When a module changes a shape used by consumers,
30+
// this file must change too — which is the point of a drift test.
31+
32+
const telemetryContract = contractOperation("telemetry.event", {
33+
version: V,
34+
fields: {
35+
op: { type: "string", required: true },
36+
actorType: { type: "string", required: true },
37+
result: { type: "string", required: true },
38+
latencyMs: { type: "number", required: true },
39+
correlationId: { type: "string", required: true }
40+
}
41+
});
42+
43+
const horizonErrorContract = contractOperation("horizon.error", {
44+
version: V,
45+
fields: {
46+
code: { type: "string", required: true },
47+
detail: { type: "object", required: true },
48+
"detail.status": { type: "number", required: false }
49+
}
50+
});
51+
52+
const workerJobContract = contractOperation("worker.job", {
53+
version: V,
54+
fields: {
55+
id: { type: "string", required: true },
56+
operation: { type: "string", required: true },
57+
attempts: { type: "number", required: true },
58+
status: { type: "string", required: true },
59+
enqueuedAt: { type: "string", required: true },
60+
correlationId: { type: "string", required: true }
61+
}
62+
});
63+
64+
const exportEnvelopeContract = contractOperation("export.envelope", {
65+
version: V,
66+
fields: {
67+
schemaVersion: { type: "string", required: true },
68+
generatedAt: { type: "string", required: true },
69+
expiresAt: { type: "string", required: true },
70+
generator: { type: "string", required: true },
71+
correlationId: { type: "string", required: true },
72+
recordCount: { type: "number", required: true },
73+
totalRecords: { type: "number", required: true },
74+
records: { type: "array", required: true }
75+
}
76+
});
77+
78+
const featureManifestContract = contractOperation("feature.manifest", {
79+
version: V,
80+
fields: {
81+
slug: { type: "string", required: true },
82+
title: { type: "string", required: true },
83+
description: { type: "string", required: true },
84+
category: { type: "string", required: true },
85+
status: { type: "string", required: true }
86+
}
87+
});
88+
89+
async function realTelemetryEvent(): Promise<TelemetryEvent> {
90+
let seen: TelemetryEvent | undefined;
91+
setTelemetrySink({ emit: (event) => (seen = event) });
92+
93+
const correlationId = newCorrelationId();
94+
await measure("horizon.request", { actorType: "client", correlationId }, () => ok(true));
95+
resetTelemetrySink();
96+
97+
if (!seen) throw new Error("no telemetry emitted");
98+
return seen;
99+
}
100+
101+
describe("contract drift tests", () => {
102+
it("locks the telemetry record shape to what the module emits", async () => {
103+
const record = await realTelemetryEvent();
104+
expect(TELEMETRY_FIELDS.every((field) => field in record)).toBe(true);
105+
expect(assertContract(telemetryContract, () => record).ok).toBe(true);
106+
});
107+
108+
it("locks the horizon error classification shape", () => {
109+
const classified = classifyHorizonError({ response: { status: 429 } });
110+
expect(assertContract(horizonErrorContract, () => classified).ok).toBe(true);
111+
});
112+
113+
it("locks the worker job payload shape", () => {
114+
const framework = createWorkerFramework();
115+
const job: JobPayload = framework.enqueue({ operation: "demo.run" });
116+
expect(assertContract(workerJobContract, () => job).ok).toBe(true);
117+
});
118+
119+
it("locks the export envelope shape", () => {
120+
const source: ExportRecordSource = {
121+
recordType: "operation_log",
122+
schemaVersion: EXPORT_CURRENT_SCHEMA_VERSION,
123+
scope: "own",
124+
collect: () => [{ op: "wallet.detect", result: "success" }]
125+
};
126+
const result = exportRecords(
127+
{ schemaVersion: EXPORT_CURRENT_SCHEMA_VERSION, scope: "own", actor: { kind: "user" } },
128+
[source]
129+
);
130+
expect(result.ok).toBe(true);
131+
if (!result.ok) return;
132+
expect(assertContract(exportEnvelopeContract, () => result.value).ok).toBe(true);
133+
});
134+
135+
it("locks the feature manifest shape", () => {
136+
const manifest: FeatureManifest = paymentQrManifest;
137+
expect(assertContract(featureManifestContract, () => manifest).ok).toBe(true);
138+
});
139+
140+
it("detects a drifted response that drops a required field", () => {
141+
const drifted = { detail: {}, status: 200 }; // `code` is required
142+
expect(validateContract(drifted, horizonErrorContract.schema).ok).toBe(false);
143+
expect(assertContract(horizonErrorContract, () => drifted).ok).toBe(false);
144+
});
145+
});

‎core/contract/contract.ts‎

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
1+
/**
2+
* Public API contract documentation support and drift detection.
3+
*
4+
* Utix exposes no external HTTP API — it is a read-only browser toolkit — so
5+
* the "public API" is the stable shape of the operations integration points
6+
* call and consumers rely on: error classifications, telemetry records, worker
7+
* jobs, export envelopes and feature manifests.
8+
*
9+
* Each operation gets a locked `ContractSchema`. Drift tests capture the real
10+
* output of the operation and assert it still satisfies the schema, so an
11+
* incompatible change to a response shape fails CI instead of silently
12+
* breaking a downstream consumer.
13+
*/
14+
15+
import { err, ok, type Result } from "@/core/result/result";
16+
import { measureSync } from "@/core/telemetry/telemetry";
17+
18+
export type ContractErrorCode = "contract_drift" | "missing_required_field" | "wrong_type";
19+
20+
export type ContractFieldType = "string" | "number" | "boolean" | "object" | "array";
21+
22+
export interface ContractField {
23+
type: ContractFieldType;
24+
required?: boolean;
25+
/** Dot-nested path when the value lives inside an object/array. */
26+
path?: string;
27+
}
28+
29+
export interface ContractSchema {
30+
/** Stable contract version; bumps on any breaking change. */
31+
version: string;
32+
fields: Record<string, ContractField>;
33+
}
34+
35+
export interface ContractOperation {
36+
name: string;
37+
schema: ContractSchema;
38+
}
39+
40+
function fieldValue(root: unknown, path: string | undefined): unknown {
41+
if (!path) return root;
42+
let cursor: unknown = root;
43+
for (const segment of path.split(".")) {
44+
if (cursor === null || cursor === undefined) return undefined;
45+
if (Array.isArray(cursor) && /^\d+$/.test(segment)) cursor = cursor[Number(segment)];
46+
else if (typeof cursor === "object") cursor = (cursor as Record<string, unknown>)[segment];
47+
else return undefined;
48+
}
49+
return cursor;
50+
}
51+
52+
function matchesType(value: unknown, type: ContractFieldType): boolean {
53+
switch (type) {
54+
case "string":
55+
return typeof value === "string";
56+
case "number":
57+
return typeof value === "number" && Number.isFinite(value);
58+
case "boolean":
59+
return typeof value === "boolean";
60+
case "object":
61+
return typeof value === "object" && value !== null && !Array.isArray(value);
62+
case "array":
63+
return Array.isArray(value);
64+
}
65+
}
66+
67+
/**
68+
* Validates an actual value against a locked schema. Returns
69+
* `contract_drift` (first field that no longer matches) or `ok`.
70+
*/
71+
export function validateContract(
72+
actual: unknown,
73+
schema: ContractSchema
74+
): Result<true, ContractErrorCode> {
75+
if (typeof actual !== "object" || actual === null) {
76+
return err("contract_drift");
77+
}
78+
79+
for (const [name, field] of Object.entries(schema.fields)) {
80+
if (!field.required) continue;
81+
const value = fieldValue(actual, field.path ?? name);
82+
if (value === undefined) return err("missing_required_field");
83+
if (!matchesType(value, field.type)) return err("wrong_type");
84+
}
85+
86+
return ok(true);
87+
}
88+
89+
/**
90+
* Registers an operation contract against the documented schema. Used by the
91+
* drift tests and by `docs/API_CONTRACT.md` examples.
92+
*/
93+
export function contractOperation(name: string, schema: ContractSchema): ContractOperation {
94+
return { name, schema };
95+
}
96+
97+
/**
98+
* Runs `capture` (the real implementation) and checks the result against the
99+
* locked schema, emitting `contract.validate` telemetry in the process.
100+
*/
101+
export function assertContract(
102+
operation: ContractOperation,
103+
capture: () => unknown
104+
): Result<true, ContractErrorCode> {
105+
return measureSync(
106+
"contract.validate",
107+
{ actorType: "system", payload: { contract: operation.name } },
108+
() => {
109+
const actual = capture();
110+
const validated = validateContract(actual, operation.schema);
111+
return validated.ok ? ok(true) : err("contract_drift");
112+
}
113+
);
114+
}
115+
116+
/** Convenience for error-shaped results: `{ code, detail }`. */
117+
export function errorSchema(version: string): ContractSchema {
118+
return {
119+
version,
120+
fields: {
121+
code: { type: "string", required: true },
122+
detail: { type: "object", required: false }
123+
}
124+
};
125+
}

0 commit comments

Comments
 (0)