@openwop/openwop-conformance 1.152.0 → 1.154.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +17 -0
- package/README.md +9 -0
- package/dist/cli.js +7 -2
- package/dist/lib/requirement-ids.js +100 -0
- package/dist/lib/scenario-disposition.js +22 -0
- package/package.json +33 -2
- package/requirement-aliases.json +4 -0
- package/requirements.json +23554 -0
- package/schemas/CORPUS-STAMP.json +99 -3
- package/src/cli.ts +7 -5
- package/src/global-setup.ts +13 -0
- package/src/lib/corpus-stamp.ts +125 -0
- package/src/lib/requirement-ids.ts +111 -0
- package/src/lib/scenario-disposition.ts +22 -0
- package/src/setup.ts +84 -5
- package/src/lib/capabilities-auth-subject-link.test.ts +0 -103
- package/src/lib/fork-availability.test.ts +0 -69
- package/src/lib/global-setup.test.ts +0 -76
- package/src/lib/grpc-framing.test.ts +0 -96
- package/src/lib/oidc-issuer.test.ts +0 -328
- package/src/lib/otel-collector-grpc.test.ts +0 -191
- package/src/lib/otel-collector.test.ts +0 -303
- package/src/lib/otlp-protobuf.test.ts +0 -461
- package/src/lib/polling.test.ts +0 -80
- package/src/lib/requirement-ledger.test.ts +0 -75
- package/src/lib/risk-disposition.test.ts +0 -91
- package/src/lib/saml-idp.test.ts +0 -127
- package/src/lib/spec-coherence-registry.test.ts +0 -155
- package/src/lib/webhook-receiver.test.ts +0 -144
- package/src/scenarios/artifact-schema-compile-bounded.test.ts +0 -126
- package/src/scenarios/artifact-type-legacy-ids.test.ts +0 -124
- package/src/scenarios/capability-example-root-layout.test.ts +0 -272
- package/src/scenarios/certification-floor-enforcement.test.ts +0 -204
- package/src/scenarios/chain-subchain-unsupported-refused.test.ts +0 -70
- package/src/scenarios/compensation-profile.test.ts +0 -340
- package/src/scenarios/core-manifest-and-extension-registry.test.ts +0 -250
- package/src/scenarios/discovery-canonical-family-no-shadow.test.ts +0 -219
- package/src/scenarios/edge-condition-truthy-falsy.test.ts +0 -108
- package/src/scenarios/effect-identity-composition.test.ts +0 -129
- package/src/scenarios/effect-identity-cross-scope.test.ts +0 -82
- package/src/scenarios/error-envelope-canonical-shape.test.ts +0 -64
- package/src/scenarios/form-content-packs.test.ts +0 -415
- package/src/scenarios/multi-region-effect-vocabulary.test.ts +0 -175
- package/src/scenarios/normative-example-extraction.test.ts +0 -242
- package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +0 -309
- package/src/scenarios/pack-manifest-extensions.test.ts +0 -203
- package/src/scenarios/protocol-version-grammar.test.ts +0 -119
- package/src/scenarios/registry-declarative-kinds.test.ts +0 -121
- package/src/scenarios/rfc-0147-self-audit.test.ts +0 -104
- package/src/scenarios/rfc-lifecycle-coherence.test.ts +0 -215
- package/src/scenarios/semantic-digest-v2.test.ts +0 -128
- package/src/scenarios/spec-corpus-validity.test.ts +0 -1727
- package/src/scenarios/spec-section-citations.test.ts +0 -132
- package/src/scenarios/tool-result-trust-monotone.test.ts +0 -168
- package/src/scenarios/versioned-composition-profiles.test.ts +0 -201
- package/src/scenarios/workflow-chain-internal-flag.test.ts +0 -84
- package/src/scenarios/workload-identity-profile.test.ts +0 -184
|
@@ -1,461 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Server-free unit tests for the OTLP protobuf decoder.
|
|
3
|
-
*
|
|
4
|
-
* The decoder is real protobuf wire-format parsing. Track 11 follow-up
|
|
5
|
-
* landed it to unlock the broader class of OTLP-only hosts that emit
|
|
6
|
-
* application/x-protobuf rather than application/json. A regression in
|
|
7
|
-
* varint decoding, fixed64 endianness, or AnyValue oneof handling would
|
|
8
|
-
* cause silent telemetry loss — the host emits, the collector accepts,
|
|
9
|
-
* but the spans/metrics never reach the assertion path.
|
|
10
|
-
*
|
|
11
|
-
* Strategy: an in-file `PbWriter` encoder synthesizes minimal OTLP
|
|
12
|
-
* payloads; we round-trip them through `decodeExportTraceServiceRequest`
|
|
13
|
-
* / `decodeExportMetricsServiceRequest` and assert the output shape.
|
|
14
|
-
* The writer is test-only — production code only ever decodes.
|
|
15
|
-
*
|
|
16
|
-
* @see conformance/src/lib/otlp-protobuf.ts
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
import { describe, it, expect } from 'vitest';
|
|
20
|
-
import {
|
|
21
|
-
PbReader,
|
|
22
|
-
decodeExportTraceServiceRequest,
|
|
23
|
-
decodeExportMetricsServiceRequest,
|
|
24
|
-
} from './otlp-protobuf.js';
|
|
25
|
-
|
|
26
|
-
// ─── Test-only protobuf encoder ────────────────────────────────────────────
|
|
27
|
-
|
|
28
|
-
const WIRE_VARINT = 0;
|
|
29
|
-
const WIRE_I64 = 1;
|
|
30
|
-
const WIRE_LEN = 2;
|
|
31
|
-
|
|
32
|
-
class PbWriter {
|
|
33
|
-
private chunks: number[] = [];
|
|
34
|
-
|
|
35
|
-
bytes(): Uint8Array {
|
|
36
|
-
return new Uint8Array(this.chunks);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
writeVarint(v: bigint | number): void {
|
|
40
|
-
let x = typeof v === 'bigint' ? v : BigInt(v);
|
|
41
|
-
while (x >= 0x80n) {
|
|
42
|
-
this.chunks.push(Number(x & 0x7fn) | 0x80);
|
|
43
|
-
x >>= 7n;
|
|
44
|
-
}
|
|
45
|
-
this.chunks.push(Number(x & 0x7fn));
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
writeTag(fieldNumber: number, wireType: number): void {
|
|
49
|
-
this.writeVarint((fieldNumber << 3) | wireType);
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
writeString(fieldNumber: number, value: string): void {
|
|
53
|
-
this.writeTag(fieldNumber, WIRE_LEN);
|
|
54
|
-
const enc = new TextEncoder().encode(value);
|
|
55
|
-
this.writeVarint(enc.length);
|
|
56
|
-
for (const b of enc) this.chunks.push(b);
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
writeBytes(fieldNumber: number, value: Uint8Array): void {
|
|
60
|
-
this.writeTag(fieldNumber, WIRE_LEN);
|
|
61
|
-
this.writeVarint(value.length);
|
|
62
|
-
for (const b of value) this.chunks.push(b);
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
writeBytesHex(fieldNumber: number, hex: string): void {
|
|
66
|
-
const bytes = new Uint8Array(hex.length / 2);
|
|
67
|
-
for (let i = 0; i < bytes.length; i++) {
|
|
68
|
-
bytes[i] = Number.parseInt(hex.slice(i * 2, i * 2 + 2), 16);
|
|
69
|
-
}
|
|
70
|
-
this.writeBytes(fieldNumber, bytes);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
writeVarintField(fieldNumber: number, v: bigint | number): void {
|
|
74
|
-
this.writeTag(fieldNumber, WIRE_VARINT);
|
|
75
|
-
this.writeVarint(v);
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
writeFixed64Uint(fieldNumber: number, v: bigint): void {
|
|
79
|
-
this.writeTag(fieldNumber, WIRE_I64);
|
|
80
|
-
const buf = new ArrayBuffer(8);
|
|
81
|
-
const view = new DataView(buf);
|
|
82
|
-
view.setBigUint64(0, v, true);
|
|
83
|
-
for (let i = 0; i < 8; i++) this.chunks.push(view.getUint8(i));
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
writeFixed64Int(fieldNumber: number, v: bigint): void {
|
|
87
|
-
this.writeTag(fieldNumber, WIRE_I64);
|
|
88
|
-
const buf = new ArrayBuffer(8);
|
|
89
|
-
const view = new DataView(buf);
|
|
90
|
-
view.setBigInt64(0, v, true);
|
|
91
|
-
for (let i = 0; i < 8; i++) this.chunks.push(view.getUint8(i));
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
writeDouble(fieldNumber: number, v: number): void {
|
|
95
|
-
this.writeTag(fieldNumber, WIRE_I64);
|
|
96
|
-
const buf = new ArrayBuffer(8);
|
|
97
|
-
const view = new DataView(buf);
|
|
98
|
-
view.setFloat64(0, v, true);
|
|
99
|
-
for (let i = 0; i < 8; i++) this.chunks.push(view.getUint8(i));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
writeMessage(fieldNumber: number, body: Uint8Array): void {
|
|
103
|
-
this.writeTag(fieldNumber, WIRE_LEN);
|
|
104
|
-
this.writeVarint(body.length);
|
|
105
|
-
for (const b of body) this.chunks.push(b);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// Helpers for building OTLP message shapes.
|
|
110
|
-
|
|
111
|
-
function buildAnyValue(variant: { type: 'string' | 'int' | 'double' | 'bool'; value: unknown }): Uint8Array {
|
|
112
|
-
const w = new PbWriter();
|
|
113
|
-
switch (variant.type) {
|
|
114
|
-
case 'string':
|
|
115
|
-
w.writeString(1, variant.value as string);
|
|
116
|
-
break;
|
|
117
|
-
case 'int':
|
|
118
|
-
w.writeVarintField(3, variant.value as number);
|
|
119
|
-
break;
|
|
120
|
-
case 'double':
|
|
121
|
-
w.writeDouble(4, variant.value as number);
|
|
122
|
-
break;
|
|
123
|
-
case 'bool':
|
|
124
|
-
w.writeVarintField(2, (variant.value as boolean) ? 1 : 0);
|
|
125
|
-
break;
|
|
126
|
-
}
|
|
127
|
-
return w.bytes();
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
function buildKeyValue(key: string, value: Uint8Array): Uint8Array {
|
|
131
|
-
const w = new PbWriter();
|
|
132
|
-
w.writeString(1, key);
|
|
133
|
-
w.writeMessage(2, value);
|
|
134
|
-
return w.bytes();
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function buildSpan(s: {
|
|
138
|
-
traceId: string;
|
|
139
|
-
spanId: string;
|
|
140
|
-
parentSpanId?: string;
|
|
141
|
-
name: string;
|
|
142
|
-
startNanos: bigint;
|
|
143
|
-
endNanos: bigint;
|
|
144
|
-
attrs: Array<{ key: string; type: 'string' | 'int' | 'double' | 'bool'; value: unknown }>;
|
|
145
|
-
}): Uint8Array {
|
|
146
|
-
const w = new PbWriter();
|
|
147
|
-
w.writeBytesHex(1, s.traceId);
|
|
148
|
-
w.writeBytesHex(2, s.spanId);
|
|
149
|
-
if (s.parentSpanId) w.writeBytesHex(4, s.parentSpanId);
|
|
150
|
-
w.writeString(5, s.name);
|
|
151
|
-
w.writeFixed64Uint(7, s.startNanos);
|
|
152
|
-
w.writeFixed64Uint(8, s.endNanos);
|
|
153
|
-
for (const a of s.attrs) {
|
|
154
|
-
w.writeMessage(9, buildKeyValue(a.key, buildAnyValue({ type: a.type, value: a.value })));
|
|
155
|
-
}
|
|
156
|
-
return w.bytes();
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
function buildExportTrace(span: Uint8Array, resourceAttrs: Array<{ key: string; value: string }> = []): Uint8Array {
|
|
160
|
-
const scopeSpans = new PbWriter();
|
|
161
|
-
scopeSpans.writeMessage(2, span);
|
|
162
|
-
|
|
163
|
-
const resource = new PbWriter();
|
|
164
|
-
for (const a of resourceAttrs) {
|
|
165
|
-
resource.writeMessage(1, buildKeyValue(a.key, buildAnyValue({ type: 'string', value: a.value })));
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
const resourceSpans = new PbWriter();
|
|
169
|
-
resourceSpans.writeMessage(1, resource.bytes());
|
|
170
|
-
resourceSpans.writeMessage(2, scopeSpans.bytes());
|
|
171
|
-
|
|
172
|
-
const req = new PbWriter();
|
|
173
|
-
req.writeMessage(1, resourceSpans.bytes());
|
|
174
|
-
return req.bytes();
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
function buildNumberDataPoint(opts: {
|
|
178
|
-
asDouble?: number;
|
|
179
|
-
asInt?: bigint;
|
|
180
|
-
attrs?: Array<{ key: string; value: string }>;
|
|
181
|
-
}): Uint8Array {
|
|
182
|
-
const w = new PbWriter();
|
|
183
|
-
if (opts.asDouble !== undefined) w.writeDouble(4, opts.asDouble);
|
|
184
|
-
if (opts.asInt !== undefined) w.writeFixed64Int(6, opts.asInt);
|
|
185
|
-
for (const a of opts.attrs ?? []) {
|
|
186
|
-
w.writeMessage(7, buildKeyValue(a.key, buildAnyValue({ type: 'string', value: a.value })));
|
|
187
|
-
}
|
|
188
|
-
return w.bytes();
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
function buildMetric(opts: {
|
|
192
|
-
name: string;
|
|
193
|
-
description?: string;
|
|
194
|
-
unit?: string;
|
|
195
|
-
variant: 'gauge' | 'sum';
|
|
196
|
-
dataPoint: Uint8Array;
|
|
197
|
-
}): Uint8Array {
|
|
198
|
-
const dpc = new PbWriter();
|
|
199
|
-
dpc.writeMessage(1, opts.dataPoint);
|
|
200
|
-
|
|
201
|
-
const m = new PbWriter();
|
|
202
|
-
m.writeString(1, opts.name);
|
|
203
|
-
if (opts.description) m.writeString(2, opts.description);
|
|
204
|
-
if (opts.unit) m.writeString(3, opts.unit);
|
|
205
|
-
if (opts.variant === 'gauge') {
|
|
206
|
-
m.writeMessage(5, dpc.bytes());
|
|
207
|
-
} else {
|
|
208
|
-
m.writeMessage(7, dpc.bytes());
|
|
209
|
-
}
|
|
210
|
-
return m.bytes();
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
function buildExportMetrics(metric: Uint8Array): Uint8Array {
|
|
214
|
-
const scopeMetrics = new PbWriter();
|
|
215
|
-
scopeMetrics.writeMessage(2, metric);
|
|
216
|
-
|
|
217
|
-
const resourceMetrics = new PbWriter();
|
|
218
|
-
resourceMetrics.writeMessage(2, scopeMetrics.bytes());
|
|
219
|
-
|
|
220
|
-
const req = new PbWriter();
|
|
221
|
-
req.writeMessage(1, resourceMetrics.bytes());
|
|
222
|
-
return req.bytes();
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
// ─── Tests ─────────────────────────────────────────────────────────────────
|
|
226
|
-
|
|
227
|
-
describe('PbReader: low-level wire format', () => {
|
|
228
|
-
it('decodes single-byte varint', () => {
|
|
229
|
-
const r = new PbReader(new Uint8Array([0x7f]));
|
|
230
|
-
expect(r.readVarintNumber()).toBe(127);
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it('decodes multi-byte varint (continuation bit)', () => {
|
|
234
|
-
// 300 = 0xAC, 0x02
|
|
235
|
-
const r = new PbReader(new Uint8Array([0xac, 0x02]));
|
|
236
|
-
expect(r.readVarintNumber()).toBe(300);
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('throws on varint exceeding 10 bytes', () => {
|
|
240
|
-
const r = new PbReader(new Uint8Array([0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff]));
|
|
241
|
-
expect(() => r.readVarint()).toThrow(/exceeds 10 bytes/);
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
it('readTag splits field number and wire type', () => {
|
|
245
|
-
const w = new PbWriter();
|
|
246
|
-
w.writeTag(42, WIRE_LEN);
|
|
247
|
-
const r = new PbReader(w.bytes());
|
|
248
|
-
const t = r.readTag();
|
|
249
|
-
expect(t.fieldNumber).toBe(42);
|
|
250
|
-
expect(t.wireType).toBe(WIRE_LEN);
|
|
251
|
-
});
|
|
252
|
-
|
|
253
|
-
it('readDouble round-trips little-endian IEEE 754', () => {
|
|
254
|
-
const w = new PbWriter();
|
|
255
|
-
w.writeDouble(1, 3.14159);
|
|
256
|
-
const r = new PbReader(w.bytes());
|
|
257
|
-
r.readTag();
|
|
258
|
-
expect(r.readDouble()).toBeCloseTo(3.14159, 10);
|
|
259
|
-
});
|
|
260
|
-
|
|
261
|
-
it('readFixed64Uint round-trips a large unsigned 64-bit', () => {
|
|
262
|
-
const w = new PbWriter();
|
|
263
|
-
const v = 1758000000000000000n; // realistic OTel timestamp (Sept 2025-ish)
|
|
264
|
-
w.writeFixed64Uint(1, v);
|
|
265
|
-
const r = new PbReader(w.bytes());
|
|
266
|
-
r.readTag();
|
|
267
|
-
expect(r.readFixed64Uint()).toBe(v);
|
|
268
|
-
});
|
|
269
|
-
|
|
270
|
-
it('readFixed64Int handles negative two\'s-complement', () => {
|
|
271
|
-
const w = new PbWriter();
|
|
272
|
-
w.writeFixed64Int(1, -42n);
|
|
273
|
-
const r = new PbReader(w.bytes());
|
|
274
|
-
r.readTag();
|
|
275
|
-
expect(r.readFixed64Int()).toBe(-42n);
|
|
276
|
-
});
|
|
277
|
-
|
|
278
|
-
it('readBytesAsHex produces canonical lowercase hex', () => {
|
|
279
|
-
const w = new PbWriter();
|
|
280
|
-
w.writeBytes(1, new Uint8Array([0x00, 0xfa, 0xce, 0xb0, 0x0c]));
|
|
281
|
-
const r = new PbReader(w.bytes());
|
|
282
|
-
r.readTag();
|
|
283
|
-
expect(r.readBytesAsHex()).toBe('00faceb00c');
|
|
284
|
-
});
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
describe('decodeExportTraceServiceRequest: span round-trip', () => {
|
|
288
|
-
it('decodes a single span with required fields', () => {
|
|
289
|
-
const span = buildSpan({
|
|
290
|
-
traceId: '0123456789abcdef0123456789abcdef',
|
|
291
|
-
spanId: 'fedcba9876543210',
|
|
292
|
-
name: 'openwop.run',
|
|
293
|
-
startNanos: 1700000000000000000n,
|
|
294
|
-
endNanos: 1700000000100000000n,
|
|
295
|
-
attrs: [
|
|
296
|
-
{ key: 'openwop.run_id', type: 'string', value: 'run-abc' },
|
|
297
|
-
{ key: 'openwop.workflow_id', type: 'string', value: 'conformance-noop' },
|
|
298
|
-
],
|
|
299
|
-
});
|
|
300
|
-
const req = buildExportTrace(span);
|
|
301
|
-
|
|
302
|
-
const decoded = decodeExportTraceServiceRequest(req);
|
|
303
|
-
expect(decoded.resourceSpans.length).toBe(1);
|
|
304
|
-
const spans = decoded.resourceSpans[0].scopeSpans?.[0]?.spans ?? [];
|
|
305
|
-
expect(spans.length).toBe(1);
|
|
306
|
-
const s = spans[0];
|
|
307
|
-
expect(s.traceId).toBe('0123456789abcdef0123456789abcdef');
|
|
308
|
-
expect(s.spanId).toBe('fedcba9876543210');
|
|
309
|
-
expect(s.parentSpanId).toBeUndefined();
|
|
310
|
-
expect(s.name).toBe('openwop.run');
|
|
311
|
-
expect(s.startTimeUnixNano).toBe('1700000000000000000');
|
|
312
|
-
expect(s.endTimeUnixNano).toBe('1700000000100000000');
|
|
313
|
-
expect(s.attributes?.map((a) => a.key)).toEqual([
|
|
314
|
-
'openwop.run_id',
|
|
315
|
-
'openwop.workflow_id',
|
|
316
|
-
]);
|
|
317
|
-
expect(s.attributes?.[0].value).toEqual({ stringValue: 'run-abc' });
|
|
318
|
-
});
|
|
319
|
-
|
|
320
|
-
it('handles parentSpanId when present', () => {
|
|
321
|
-
const span = buildSpan({
|
|
322
|
-
traceId: 'a'.repeat(32),
|
|
323
|
-
spanId: 'b'.repeat(16),
|
|
324
|
-
parentSpanId: 'c'.repeat(16),
|
|
325
|
-
name: 'child',
|
|
326
|
-
startNanos: 0n,
|
|
327
|
-
endNanos: 1n,
|
|
328
|
-
attrs: [],
|
|
329
|
-
});
|
|
330
|
-
const req = buildExportTrace(span);
|
|
331
|
-
const decoded = decodeExportTraceServiceRequest(req);
|
|
332
|
-
const s = decoded.resourceSpans[0].scopeSpans?.[0]?.spans?.[0];
|
|
333
|
-
expect(s?.parentSpanId).toBe('c'.repeat(16));
|
|
334
|
-
});
|
|
335
|
-
|
|
336
|
-
it('threads resource attributes through ResourceSpans', () => {
|
|
337
|
-
const span = buildSpan({
|
|
338
|
-
traceId: '00'.repeat(16),
|
|
339
|
-
spanId: '11'.repeat(8),
|
|
340
|
-
name: 's',
|
|
341
|
-
startNanos: 0n,
|
|
342
|
-
endNanos: 1n,
|
|
343
|
-
attrs: [],
|
|
344
|
-
});
|
|
345
|
-
const req = buildExportTrace(span, [{ key: 'service.name', value: 'openwop-host-sqlite' }]);
|
|
346
|
-
const decoded = decodeExportTraceServiceRequest(req);
|
|
347
|
-
const resource = decoded.resourceSpans[0].resource;
|
|
348
|
-
expect(resource?.attributes?.[0]).toEqual({
|
|
349
|
-
key: 'service.name',
|
|
350
|
-
value: { stringValue: 'openwop-host-sqlite' },
|
|
351
|
-
});
|
|
352
|
-
});
|
|
353
|
-
});
|
|
354
|
-
|
|
355
|
-
describe('decodeExportTraceServiceRequest: AnyValue oneof', () => {
|
|
356
|
-
it('decodes intValue as a string (matches JSON shape)', () => {
|
|
357
|
-
const span = buildSpan({
|
|
358
|
-
traceId: '00'.repeat(16),
|
|
359
|
-
spanId: '11'.repeat(8),
|
|
360
|
-
name: 's',
|
|
361
|
-
startNanos: 0n,
|
|
362
|
-
endNanos: 1n,
|
|
363
|
-
attrs: [{ key: 'openwop.node_count', type: 'int', value: 7 }],
|
|
364
|
-
});
|
|
365
|
-
const decoded = decodeExportTraceServiceRequest(buildExportTrace(span));
|
|
366
|
-
const attr = decoded.resourceSpans[0].scopeSpans?.[0]?.spans?.[0]?.attributes?.[0];
|
|
367
|
-
expect(attr?.value).toEqual({ intValue: '7' });
|
|
368
|
-
});
|
|
369
|
-
|
|
370
|
-
it('decodes doubleValue as a number', () => {
|
|
371
|
-
const span = buildSpan({
|
|
372
|
-
traceId: '00'.repeat(16),
|
|
373
|
-
spanId: '11'.repeat(8),
|
|
374
|
-
name: 's',
|
|
375
|
-
startNanos: 0n,
|
|
376
|
-
endNanos: 1n,
|
|
377
|
-
attrs: [{ key: 'openwop.cost.usd', type: 'double', value: 0.0123 }],
|
|
378
|
-
});
|
|
379
|
-
const decoded = decodeExportTraceServiceRequest(buildExportTrace(span));
|
|
380
|
-
const attr = decoded.resourceSpans[0].scopeSpans?.[0]?.spans?.[0]?.attributes?.[0];
|
|
381
|
-
expect(attr?.value).toEqual({ doubleValue: 0.0123 });
|
|
382
|
-
});
|
|
383
|
-
|
|
384
|
-
it('decodes boolValue', () => {
|
|
385
|
-
const span = buildSpan({
|
|
386
|
-
traceId: '00'.repeat(16),
|
|
387
|
-
spanId: '11'.repeat(8),
|
|
388
|
-
name: 's',
|
|
389
|
-
startNanos: 0n,
|
|
390
|
-
endNanos: 1n,
|
|
391
|
-
attrs: [{ key: 'openwop.is_replay', type: 'bool', value: true }],
|
|
392
|
-
});
|
|
393
|
-
const decoded = decodeExportTraceServiceRequest(buildExportTrace(span));
|
|
394
|
-
const attr = decoded.resourceSpans[0].scopeSpans?.[0]?.spans?.[0]?.attributes?.[0];
|
|
395
|
-
expect(attr?.value).toEqual({ boolValue: true });
|
|
396
|
-
});
|
|
397
|
-
});
|
|
398
|
-
|
|
399
|
-
describe('decodeExportMetricsServiceRequest: gauge + sum', () => {
|
|
400
|
-
it('decodes a gauge with double data point + attributes', () => {
|
|
401
|
-
const dp = buildNumberDataPoint({
|
|
402
|
-
asDouble: 5.5,
|
|
403
|
-
attrs: [{ key: 'openwop.run_id', value: 'run-xyz' }],
|
|
404
|
-
});
|
|
405
|
-
const m = buildMetric({
|
|
406
|
-
name: 'openwop.run.duration',
|
|
407
|
-
unit: 'ms',
|
|
408
|
-
variant: 'gauge',
|
|
409
|
-
dataPoint: dp,
|
|
410
|
-
});
|
|
411
|
-
const req = buildExportMetrics(m);
|
|
412
|
-
|
|
413
|
-
const decoded = decodeExportMetricsServiceRequest(req);
|
|
414
|
-
const metric = decoded.resourceMetrics[0].scopeMetrics?.[0]?.metrics?.[0];
|
|
415
|
-
expect(metric?.name).toBe('openwop.run.duration');
|
|
416
|
-
expect(metric?.unit).toBe('ms');
|
|
417
|
-
expect(metric?.gauge?.dataPoints?.[0].asDouble).toBe(5.5);
|
|
418
|
-
expect(metric?.gauge?.dataPoints?.[0].attributes?.[0]).toEqual({
|
|
419
|
-
key: 'openwop.run_id',
|
|
420
|
-
value: { stringValue: 'run-xyz' },
|
|
421
|
-
});
|
|
422
|
-
});
|
|
423
|
-
|
|
424
|
-
it('decodes a sum with sfixed64 as_int (negative value as string)', () => {
|
|
425
|
-
const dp = buildNumberDataPoint({ asInt: -1234n });
|
|
426
|
-
const m = buildMetric({
|
|
427
|
-
name: 'openwop.queue.depth',
|
|
428
|
-
variant: 'sum',
|
|
429
|
-
dataPoint: dp,
|
|
430
|
-
});
|
|
431
|
-
const decoded = decodeExportMetricsServiceRequest(buildExportMetrics(m));
|
|
432
|
-
const metric = decoded.resourceMetrics[0].scopeMetrics?.[0]?.metrics?.[0];
|
|
433
|
-
expect(metric?.sum?.dataPoints?.[0].asInt).toBe('-1234');
|
|
434
|
-
});
|
|
435
|
-
});
|
|
436
|
-
|
|
437
|
-
describe('decodeExportTraceServiceRequest: forward-compat (unknown fields)', () => {
|
|
438
|
-
it('skips unknown field numbers without erroring', () => {
|
|
439
|
-
// Build a Span body that includes an unknown field 99 (varint).
|
|
440
|
-
const w = new PbWriter();
|
|
441
|
-
w.writeBytesHex(1, '00'.repeat(16)); // traceId
|
|
442
|
-
w.writeBytesHex(2, '11'.repeat(8)); // spanId
|
|
443
|
-
w.writeString(5, 'forward-compat-span'); // name
|
|
444
|
-
w.writeFixed64Uint(7, 0n); // startTimeUnixNano
|
|
445
|
-
w.writeFixed64Uint(8, 1n); // endTimeUnixNano
|
|
446
|
-
w.writeVarintField(99, 1234); // unknown — decoder MUST skip
|
|
447
|
-
w.writeString(100, 'future-field'); // unknown LEN — decoder MUST skip
|
|
448
|
-
|
|
449
|
-
const req = buildExportTrace(w.bytes());
|
|
450
|
-
const decoded = decodeExportTraceServiceRequest(req);
|
|
451
|
-
const s = decoded.resourceSpans[0].scopeSpans?.[0]?.spans?.[0];
|
|
452
|
-
expect(s?.name).toBe('forward-compat-span');
|
|
453
|
-
});
|
|
454
|
-
});
|
|
455
|
-
|
|
456
|
-
describe('decodeExportTraceServiceRequest: empty payload', () => {
|
|
457
|
-
it('returns empty resourceSpans for an empty buffer', () => {
|
|
458
|
-
const decoded = decodeExportTraceServiceRequest(new Uint8Array(0));
|
|
459
|
-
expect(decoded.resourceSpans).toEqual([]);
|
|
460
|
-
});
|
|
461
|
-
});
|
package/src/lib/polling.test.ts
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for `polling.ts` — the poll-bound scaling knob.
|
|
3
|
-
*
|
|
4
|
-
* These exist because the defect being fixed was a knob that did not reach
|
|
5
|
-
* what its documentation said it reached: `OPENWOP_LIFECYCLE_TIMEOUT_MS` was
|
|
6
|
-
* documented as the way to bound long polls, but it supplies only the DEFAULT,
|
|
7
|
-
* so every call site passing an explicit `timeoutMs` ignored it. Shipping the
|
|
8
|
-
* replacement untested would repeat the defect one layer up, so the scale is
|
|
9
|
-
* asserted here rather than assumed.
|
|
10
|
-
*
|
|
11
|
-
* @see polling.ts
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
import { describe, it, expect, afterEach } from 'vitest';
|
|
15
|
-
import { scaledTimeoutMs } from './polling.js';
|
|
16
|
-
|
|
17
|
-
const KEY = 'OPENWOP_POLL_TIMEOUT_SCALE';
|
|
18
|
-
|
|
19
|
-
function withScale(value: string | undefined, fn: () => void): void {
|
|
20
|
-
if (value === undefined) delete process.env[KEY];
|
|
21
|
-
else process.env[KEY] = value;
|
|
22
|
-
fn();
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
afterEach(() => {
|
|
26
|
-
delete process.env[KEY];
|
|
27
|
-
});
|
|
28
|
-
|
|
29
|
-
describe('polling: scaledTimeoutMs', () => {
|
|
30
|
-
it('is an exact no-op when the knob is unset — no existing measurement moves', () => {
|
|
31
|
-
withScale(undefined, () => {
|
|
32
|
-
for (const ms of [100, 1_000, 5_000, 10_000, 15_000, 30_000, 60_000]) {
|
|
33
|
-
expect(scaledTimeoutMs(ms)).toBe(ms);
|
|
34
|
-
}
|
|
35
|
-
});
|
|
36
|
-
});
|
|
37
|
-
|
|
38
|
-
it('is an exact no-op at scale 1, including the string form', () => {
|
|
39
|
-
withScale('1', () => expect(scaledTimeoutMs(10_000)).toBe(10_000));
|
|
40
|
-
withScale('1.0', () => expect(scaledTimeoutMs(10_000)).toBe(10_000));
|
|
41
|
-
});
|
|
42
|
-
|
|
43
|
-
it('scales explicit bounds, which the documented env var could never reach', () => {
|
|
44
|
-
withScale('3', () => {
|
|
45
|
-
expect(scaledTimeoutMs(10_000)).toBe(30_000);
|
|
46
|
-
expect(scaledTimeoutMs(15_000)).toBe(45_000);
|
|
47
|
-
});
|
|
48
|
-
});
|
|
49
|
-
|
|
50
|
-
it('preserves the ORDERING of deliberately-short bounds — the reason this scales rather than floors', () => {
|
|
51
|
-
withScale('4', () => {
|
|
52
|
-
const short = scaledTimeoutMs(100);
|
|
53
|
-
const long = scaledTimeoutMs(10_000);
|
|
54
|
-
expect(short).toBe(400);
|
|
55
|
-
expect(long).toBe(40_000);
|
|
56
|
-
// A floor would have collapsed these two to the same value, silently
|
|
57
|
-
// rewriting every negative assertion that depends on a short bound.
|
|
58
|
-
expect(short).toBeLessThan(long);
|
|
59
|
-
});
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
it('rounds up, so a fractional scale never shortens a bound', () => {
|
|
63
|
-
withScale('1.5', () => expect(scaledTimeoutMs(1_001)).toBe(1_502));
|
|
64
|
-
withScale('1.0001', () => expect(scaledTimeoutMs(100)).toBeGreaterThanOrEqual(100));
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
it('falls back to 1 for a mis-set knob rather than producing an instant failure', () => {
|
|
68
|
-
// A zero, negative, or unparseable scale would turn every poll into an
|
|
69
|
-
// immediate timeout — which reads as a catastrophic host defect, the exact
|
|
70
|
-
// misattribution this knob exists to prevent.
|
|
71
|
-
for (const bad of ['0', '-2', 'abc', 'NaN', 'Infinity', '', ' ']) {
|
|
72
|
-
withScale(bad, () => expect(scaledTimeoutMs(10_000)).toBe(10_000));
|
|
73
|
-
}
|
|
74
|
-
});
|
|
75
|
-
|
|
76
|
-
it('reads the environment at CALL time, so a harness may set it after import', () => {
|
|
77
|
-
withScale('2', () => expect(scaledTimeoutMs(1_000)).toBe(2_000));
|
|
78
|
-
withScale('5', () => expect(scaledTimeoutMs(1_000)).toBe(5_000));
|
|
79
|
-
});
|
|
80
|
-
});
|
|
@@ -1,75 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for `requirement-ledger.ts` — recording precedence.
|
|
3
|
-
*
|
|
4
|
-
* These pin the property `setup.ts` relies on when it decides whether to write
|
|
5
|
-
* its automatic file-level record: a scenario that classified ITSELF must win
|
|
6
|
-
* outright. The comment on that line claimed as much for years while the code
|
|
7
|
-
* only delivered it on DISAGREEMENT — a same-disposition re-record reached
|
|
8
|
-
* `ledger.set` and replaced the scenario's own `detail` and `assertionCount`
|
|
9
|
-
* with the file-level ones. Harmless while details were rarely set on a pass;
|
|
10
|
-
* visible the moment `resolveFileRecord` began attaching a `partial-witness:`
|
|
11
|
-
* marker, which would have stamped "may not have witnessed this" over a
|
|
12
|
-
* scenario's own explicit finding.
|
|
13
|
-
*
|
|
14
|
-
* @see requirement-ledger.ts, setup.ts
|
|
15
|
-
*/
|
|
16
|
-
|
|
17
|
-
import { describe, it, expect, beforeEach } from 'vitest';
|
|
18
|
-
import {
|
|
19
|
-
recordRequirement,
|
|
20
|
-
hasRequirement,
|
|
21
|
-
entryOf,
|
|
22
|
-
dispositionOf,
|
|
23
|
-
resetLedger,
|
|
24
|
-
} from './requirement-ledger.js';
|
|
25
|
-
|
|
26
|
-
const ID = 'openwop.scenario.ledger-precedence-fixture';
|
|
27
|
-
|
|
28
|
-
beforeEach(() => {
|
|
29
|
-
resetLedger();
|
|
30
|
-
});
|
|
31
|
-
|
|
32
|
-
describe('requirement-ledger: recording precedence', () => {
|
|
33
|
-
it('hasRequirement distinguishes "not recorded" from "recorded", which dispositionOf cannot', () => {
|
|
34
|
-
// `dispositionOf` folds the absent case to `blocked`, so it reads the same
|
|
35
|
-
// for a requirement nobody touched and one deliberately recorded blocked.
|
|
36
|
-
expect(hasRequirement(ID)).toBe(false);
|
|
37
|
-
expect(dispositionOf(ID)).toBe('blocked');
|
|
38
|
-
|
|
39
|
-
recordRequirement(ID, 'blocked', 'seam absent');
|
|
40
|
-
expect(hasRequirement(ID)).toBe(true);
|
|
41
|
-
expect(dispositionOf(ID)).toBe('blocked');
|
|
42
|
-
});
|
|
43
|
-
|
|
44
|
-
it('a same-disposition re-record OVERWRITES detail and assertionCount — the reason setup.ts must guard', () => {
|
|
45
|
-
recordRequirement(ID, 'executed-pass', 'witnessed the MUST NOT on the wire', { assertionCount: 9 });
|
|
46
|
-
expect(entryOf(ID).detail).toBe('witnessed the MUST NOT on the wire');
|
|
47
|
-
|
|
48
|
-
// No throw: `recordRequirement` only rejects a CONFLICTING disposition.
|
|
49
|
-
recordRequirement(ID, 'executed-pass', 'partial-witness: inapplicable: branch leg skipped', { assertionCount: 2 });
|
|
50
|
-
expect(entryOf(ID).detail).toBe('partial-witness: inapplicable: branch leg skipped');
|
|
51
|
-
expect(entryOf(ID).assertionCount).toBe(2);
|
|
52
|
-
});
|
|
53
|
-
|
|
54
|
-
it('a CONFLICTING disposition throws, which is how the explicit record already won on disagreement', () => {
|
|
55
|
-
recordRequirement(ID, 'executed-pass', undefined, { assertionCount: 4 });
|
|
56
|
-
expect(() => recordRequirement(ID, 'blocked', 'file-level fold said blocked')).toThrow(/already recorded/);
|
|
57
|
-
// The first recording survives the rejected second one.
|
|
58
|
-
expect(dispositionOf(ID)).toBe('executed-pass');
|
|
59
|
-
});
|
|
60
|
-
|
|
61
|
-
it('guarding on hasRequirement preserves the explicit record in BOTH directions', () => {
|
|
62
|
-
// This is precisely what setup.ts now does before its automatic write.
|
|
63
|
-
recordRequirement(ID, 'executed-pass', 'witnessed the MUST NOT on the wire', { assertionCount: 9 });
|
|
64
|
-
if (!hasRequirement(ID)) {
|
|
65
|
-
recordRequirement(ID, 'executed-pass', 'partial-witness: inapplicable: branch leg skipped', { assertionCount: 2 });
|
|
66
|
-
}
|
|
67
|
-
expect(entryOf(ID).detail).toBe('witnessed the MUST NOT on the wire');
|
|
68
|
-
expect(entryOf(ID).assertionCount).toBe(9);
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
it('still refuses a non-pass disposition with no reason — an unactionable row', () => {
|
|
72
|
-
expect(() => recordRequirement(ID, 'blocked', ' ')).toThrow(/without a reason/);
|
|
73
|
-
expect(hasRequirement(ID)).toBe(false);
|
|
74
|
-
});
|
|
75
|
-
});
|