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+ } ) ;
0 commit comments