@openwop/openwop-conformance 1.37.0 → 1.43.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.
@@ -0,0 +1,188 @@
1
+ /**
2
+ * RFC 0113 — Memory Injection Budget.
3
+ *
4
+ * Verifies the new token-denominated bound on the live injection read:
5
+ * `MemoryAdapter.list(memoryRef, { tokenBudget, rank, query })`
6
+ * (`spec/v1/agent-memory.md` §"Injection budget"). The genuinely new
7
+ * contribution is `tokenBudget`; `rank:'relevance'` DELEGATES to the
8
+ * existing `memory.search` semantic mode (RFC 0080) — this scenario does
9
+ * NOT assert a parallel ranking primitive, and the relevance leg soft-skips
10
+ * unless the host ALSO advertises `memory.search` semantic.
11
+ *
12
+ * Capability-gated on `capabilities.memory.injectionBudget.supported === true`
13
+ * (root-first per RFC 0073) via `behaviorGate`. Driven through the host-sample
14
+ * memory seam — the `conformance-agent-memory-injection-budget` fixture (the
15
+ * same `/v1/runs` + run-variable seam the other `agentMemory*` scenarios use to
16
+ * reach the adapter), which seeds a set whose total exceeds the budget AND
17
+ * includes one single entry larger than the whole budget, plus a BYOK-redacted
18
+ * entry and a cross-tenant probe.
19
+ *
20
+ * Asserts: cumulative tokens ≤ `tokenBudget`; an over-budget single entry is
21
+ * omitted (not truncated); `rank:'relevance'` ordering differs from recency on
22
+ * the crafted fixture (only when `memory.search` semantic is advertised, else
23
+ * soft-skip); and re-asserts SR-1 (redacted content) + CTI-1 (cross-tenant
24
+ * probe empty) on the budgeted path as a regression guard.
25
+ *
26
+ * @see RFCS/0113-memory-injection-budget.md
27
+ * @see spec/v1/agent-memory.md §"Injection budget"
28
+ */
29
+
30
+ import { describe, it, expect } from 'vitest';
31
+ import { driver } from '../lib/driver.js';
32
+ import { pollUntilTerminal } from '../lib/polling.js';
33
+ import { behaviorGate } from '../lib/behavior-gate.js';
34
+ import { isFixtureAdvertised } from '../lib/fixtures.js';
35
+ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
36
+
37
+ const FIXTURE = 'conformance-agent-memory-injection-budget';
38
+ const PROFILE = 'openwop-memory-injection-budget';
39
+
40
+ interface MemoryInjectionBudgetCap {
41
+ readonly supported?: boolean;
42
+ readonly tokenCounter?: string;
43
+ }
44
+ interface MemorySearchCap {
45
+ readonly supported?: boolean;
46
+ readonly modes?: readonly string[];
47
+ }
48
+ interface MemoryCap {
49
+ readonly injectionBudget?: MemoryInjectionBudgetCap;
50
+ readonly search?: MemorySearchCap;
51
+ }
52
+
53
+ // ── cast-free typed accessors (no `as`) ──────────────────────────────────
54
+ function isRecord(v: unknown): v is Record<string, unknown> {
55
+ return typeof v === 'object' && v !== null && !Array.isArray(v);
56
+ }
57
+ function isString(v: unknown): v is string {
58
+ return typeof v === 'string';
59
+ }
60
+ function isNumber(v: unknown): v is number {
61
+ return typeof v === 'number';
62
+ }
63
+ function isBoolean(v: unknown): v is boolean {
64
+ return typeof v === 'boolean';
65
+ }
66
+ function stringOf(v: unknown): string | undefined {
67
+ return isString(v) ? v : undefined;
68
+ }
69
+ function numberOf(v: unknown): number | undefined {
70
+ return isNumber(v) ? v : undefined;
71
+ }
72
+ function booleanOf(v: unknown): boolean | undefined {
73
+ return isBoolean(v) ? v : undefined;
74
+ }
75
+ function stringArrayOf(v: unknown): string[] | undefined {
76
+ return Array.isArray(v) && v.every(isString) ? v : undefined;
77
+ }
78
+ function recordArrayOf(v: unknown): Record<string, unknown>[] | undefined {
79
+ return Array.isArray(v) && v.every(isRecord) ? v : undefined;
80
+ }
81
+ function runIdOf(v: unknown): string | undefined {
82
+ return isRecord(v) ? stringOf(v['runId']) : undefined;
83
+ }
84
+ function variablesOf(v: unknown): Record<string, unknown> | undefined {
85
+ if (!isRecord(v)) return undefined;
86
+ const vars = v['variables'];
87
+ return isRecord(vars) ? vars : undefined;
88
+ }
89
+
90
+ function advertisesSemanticSearch(mem: MemoryCap | undefined): boolean {
91
+ const modes = mem?.search?.modes;
92
+ return mem?.search?.supported === true && Array.isArray(modes) && modes.includes('semantic');
93
+ }
94
+
95
+ async function driveFixtureVariables(): Promise<Record<string, unknown> | undefined> {
96
+ const create = await driver.post('/v1/runs', { workflowId: FIXTURE });
97
+ expect(create.status).toBe(201);
98
+ const runId = runIdOf(create.json);
99
+ expect(runId, 'POST /v1/runs MUST return a runId').toBeDefined();
100
+ if (runId === undefined) return undefined;
101
+
102
+ const terminal = await pollUntilTerminal(runId);
103
+ expect(terminal.status).toBe('completed');
104
+
105
+ const snap = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
106
+ return variablesOf(snap.json);
107
+ }
108
+
109
+ describe('memory-injection-budget (RFC 0113)', () => {
110
+ it('token-bounds the injection read, omits the over-budget entry, and preserves SR-1 + CTI-1', async () => {
111
+ const mem = await readCapabilityFamily<MemoryCap>('memory');
112
+ if (!behaviorGate(PROFILE, mem?.injectionBudget?.supported === true)) return;
113
+ if (!isFixtureAdvertised(FIXTURE)) return; // fixture-gated soft-skip
114
+
115
+ const v = await driveFixtureVariables();
116
+ expect(v, 'fixture MUST surface run variables').toBeDefined();
117
+ if (v === undefined) return;
118
+
119
+ // ── tokenBudget bound (the new lever) ──────────────────────────────
120
+ const tokenBudget = numberOf(v['tokenBudget']);
121
+ const total = numberOf(v['budgetedTokenTotal']);
122
+ expect(tokenBudget, 'fixture MUST echo the requested tokenBudget').toBeDefined();
123
+ expect(total, 'fixture MUST surface the budgeted cumulative token total').toBeDefined();
124
+ // Cumulative tokens across the returned prefix MUST NOT exceed the budget.
125
+ if (tokenBudget !== undefined && total !== undefined) {
126
+ expect(total).toBeLessThanOrEqual(tokenBudget);
127
+ }
128
+
129
+ // ── over-budget single entry omitted (not truncated) ───────────────
130
+ expect(
131
+ booleanOf(v['overBudgetEntryOmitted']),
132
+ 'an entry larger than the whole budget MUST be omitted, not truncated mid-entry',
133
+ ).toBe(true);
134
+ const entries = recordArrayOf(v['budgetedEntries']);
135
+ expect(entries, 'fixture MUST surface the budgeted entry slice').toBeDefined();
136
+ const overId = stringOf(v['overBudgetEntryId']);
137
+ if (entries !== undefined && overId !== undefined) {
138
+ const ids = entries.map((e) => stringOf(e['id']));
139
+ expect(ids, 'the over-budget entry MUST NOT appear in the returned slice').not.toContain(overId);
140
+ }
141
+
142
+ // ── SR-1 re-assertion on the budgeted path ─────────────────────────
143
+ // A budgeted/ranked read ranks over already-redacted content; the read
144
+ // surface MUST carry the redaction marker, never the plaintext.
145
+ const redacted = stringOf(v['redactedContentSample']);
146
+ expect(redacted, 'budgeted read MUST surface a redacted-content sample').toBeDefined();
147
+ if (redacted !== undefined) {
148
+ expect(redacted).toMatch(/\[REDACTED:[^\]]+\]/);
149
+ }
150
+
151
+ // ── CTI-1 re-assertion on the budgeted path ────────────────────────
152
+ // A budget/rank prefix of an already-single-tenant list stays single-tenant:
153
+ // the cross-tenant probe under the budgeted path MUST return empty.
154
+ const probe = v['crossTenantBudgetedProbe'];
155
+ if (Array.isArray(probe)) {
156
+ expect(probe.length, 'cross-tenant probe on the budgeted path MUST return []').toBe(0);
157
+ } else {
158
+ expect(probe, 'cross-tenant probe on the budgeted path MUST return [] / null').toBeFalsy();
159
+ }
160
+ });
161
+
162
+ it("rank:'relevance' reorders vs recency — only when memory.search semantic is ALSO advertised", async () => {
163
+ const mem = await readCapabilityFamily<MemoryCap>('memory');
164
+ if (!behaviorGate(PROFILE, mem?.injectionBudget?.supported === true)) return;
165
+ // RFC 0113: rank:'relevance' DELEGATES to memory.search semantic (RFC 0080).
166
+ // A host that does not advertise memory.search semantic MUST NOT fabricate a
167
+ // relevance ranking — the relevance leg soft-skips here (it is not a new
168
+ // ranking surface advertised by injectionBudget).
169
+ if (!advertisesSemanticSearch(mem)) {
170
+ // eslint-disable-next-line no-console
171
+ console.warn(`[${PROFILE}] memory.search semantic not advertised; relevance leg soft-skipped`);
172
+ return;
173
+ }
174
+ if (!isFixtureAdvertised(FIXTURE)) return;
175
+
176
+ const v = await driveFixtureVariables();
177
+ expect(v, 'fixture MUST surface run variables').toBeDefined();
178
+ if (v === undefined) return;
179
+
180
+ const recencyOrder = stringArrayOf(v['recencyOrder']);
181
+ const relevanceOrder = stringArrayOf(v['relevanceOrder']);
182
+ expect(recencyOrder, 'fixture MUST surface the recency ordering').toBeDefined();
183
+ expect(relevanceOrder, 'fixture MUST surface the relevance ordering').toBeDefined();
184
+ // The crafted fixture pins a query whose semantic top-k differs from the
185
+ // most-recent-first order — relevance MUST reorder (not echo recency).
186
+ expect(relevanceOrder).not.toEqual(recencyOrder);
187
+ });
188
+ });
@@ -0,0 +1,200 @@
1
+ /**
2
+ * prompt-prefix-cache — RFC 0116 + SECURITY/invariants.yaml
3
+ * `prompt-prefix-cache-cross-tenant-isolation`.
4
+ *
5
+ * Status: ACTIVE (advertisement-shape + behavioral). The behavioral legs drive
6
+ * the host's real envelope/provider generate path through the OPTIONAL test
7
+ * seam `POST /v1/host/sample/ai/generate` (`host-sample-test-seams.md` §16,
8
+ * env-gated on `OPENWOP_TEST_SEAM_ENABLED=true`). Hosts that don't advertise
9
+ * `aiProviders.promptPrefixCache.supported` soft-skip; hosts that advertise it
10
+ * but don't wire the seam (HTTP 404/405) soft-skip the behavioral legs and
11
+ * verify advertisement shape only.
12
+ *
13
+ * RFC 0116 makes the optional `cachePrefixId` generate hint safe + testable via
14
+ * three pillars, each asserted here:
15
+ * (a) outcome-invariance — a generate with `cachePrefixId` and a control
16
+ * without produce the same accepted envelope + identical
17
+ * `inputTokens`/`outputTokens` (cost-hint-only, replay-invariant).
18
+ * (b) cache hit observable — a repeat generate shows
19
+ * `provider.usage.cacheReadTokens > 0`.
20
+ * (c) cross-tenant isolation — tenant B's first use of tenant A's
21
+ * `cachePrefixId` shows `cacheReadTokens == 0` (no cross-tenant share).
22
+ * THIS is the public test for the `prompt-prefix-cache-cross-tenant-isolation`
23
+ * invariant: the host MUST key its provider cache by `(tenant, cachePrefixId)`.
24
+ * (d) secret-free — a `cachePrefixId` is never emitted where SR-1 would
25
+ * redact, and the usage block carries no prompt substrings.
26
+ *
27
+ * @see RFCS/0116-prompt-prefix-cache.md
28
+ * @see spec/v1/ai-envelope.md §"Prompt-prefix cache (RFC 0116)"
29
+ * @see SECURITY/invariants.yaml — prompt-prefix-cache-cross-tenant-isolation
30
+ */
31
+
32
+ import { describe, it, expect } from 'vitest';
33
+ import { driver } from '../lib/driver.js';
34
+ import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
35
+
36
+ interface PromptPrefixCacheCap {
37
+ supported?: unknown;
38
+ providers?: unknown;
39
+ }
40
+
41
+ interface AiProvidersCap {
42
+ promptPrefixCache?: PromptPrefixCacheCap;
43
+ }
44
+
45
+ interface GenerateUsage {
46
+ inputTokens?: number;
47
+ outputTokens?: number;
48
+ cacheReadTokens?: number;
49
+ cacheWriteTokens?: number;
50
+ }
51
+
52
+ interface GenerateResponse {
53
+ envelope?: { envelopeType?: string; payload?: unknown; envelopeId?: string };
54
+ usage?: GenerateUsage;
55
+ }
56
+
57
+ async function readCap(): Promise<PromptPrefixCacheCap | null> {
58
+ const fam = await readCapabilityFamily<AiProvidersCap>('aiProviders');
59
+ const block = fam?.promptPrefixCache;
60
+ return block && typeof block === 'object' ? block : null;
61
+ }
62
+
63
+ async function generate(args: {
64
+ tenantId: string;
65
+ cachePrefixId?: string;
66
+ }): Promise<{ status: number; body: GenerateResponse }> {
67
+ const res = await driver.post('/v1/host/sample/ai/generate', {
68
+ tenantId: args.tenantId,
69
+ envelopeType: 'clarification.request',
70
+ systemPrompt: 'You are a helpful assistant. Answer concisely.',
71
+ ...(args.cachePrefixId !== undefined ? { cachePrefixId: args.cachePrefixId } : {}),
72
+ });
73
+ return { status: res.status, body: (res.json ?? {}) as GenerateResponse };
74
+ }
75
+
76
+ describe('prompt-prefix-cache: advertisement shape (RFC 0116)', () => {
77
+ it('aiProviders.promptPrefixCache is either absent or a well-formed object', async () => {
78
+ const cap = await readCap();
79
+ if (cap === null) return; // not advertised — skip
80
+ expect(
81
+ typeof cap.supported,
82
+ driver.describe(
83
+ 'capabilities.schema.json §aiProviders.promptPrefixCache',
84
+ 'promptPrefixCache.supported MUST be a boolean when the block is present',
85
+ ),
86
+ ).toBe('boolean');
87
+ if (cap.providers !== undefined) {
88
+ expect(
89
+ Array.isArray(cap.providers),
90
+ driver.describe(
91
+ 'capabilities.schema.json §aiProviders.promptPrefixCache',
92
+ 'promptPrefixCache.providers MUST be an array of provider ids when present (provider-scoped)',
93
+ ),
94
+ ).toBe(true);
95
+ }
96
+ });
97
+ });
98
+
99
+ describe('prompt-prefix-cache: behavioral (RFC 0116 §"Normative requirements")', () => {
100
+ it('(a) outcome-invariance — cachePrefixId vs control → same envelope + identical input/output tokens', async () => {
101
+ const cap = await readCap();
102
+ if (!cap || cap.supported !== true) return; // not advertised — skip
103
+ const prefixId = `inv-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
104
+
105
+ const control = await generate({ tenantId: 'tenant-a' });
106
+ if (control.status === 404 || control.status === 405) return; // seam not wired
107
+ expect(control.status, driver.describe('host-sample-test-seams.md §16', 'generate seam MUST return 200')).toBe(200);
108
+
109
+ const withPrefix = await generate({ tenantId: 'tenant-a', cachePrefixId: prefixId });
110
+ expect(withPrefix.status).toBe(200);
111
+
112
+ expect(
113
+ withPrefix.body.envelope?.envelopeType,
114
+ driver.describe(
115
+ 'ai-envelope.md §"Prompt-prefix cache (RFC 0116)" rule 3',
116
+ 'cachePrefixId is a cost hint, never semantic: the accepted envelope MUST be identical hit-vs-miss',
117
+ ),
118
+ ).toBe(control.body.envelope?.envelopeType);
119
+ expect(withPrefix.body.usage?.inputTokens).toBe(control.body.usage?.inputTokens);
120
+ expect(
121
+ withPrefix.body.usage?.outputTokens,
122
+ driver.describe(
123
+ 'ai-envelope.md §"Prompt-prefix cache (RFC 0116)" rule 3',
124
+ 'provider.usage.inputTokens/outputTokens MUST be identical hit-vs-miss (replay-invariant)',
125
+ ),
126
+ ).toBe(control.body.usage?.outputTokens);
127
+ });
128
+
129
+ it('(b) cache hit observable — a repeat generate shows cacheReadTokens > 0 while tokens stay invariant', async () => {
130
+ const cap = await readCap();
131
+ if (!cap || cap.supported !== true) return; // not advertised — skip
132
+ const prefixId = `hit-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
133
+
134
+ const prime = await generate({ tenantId: 'tenant-a', cachePrefixId: prefixId });
135
+ if (prime.status === 404 || prime.status === 405) return; // seam not wired
136
+ expect(prime.status).toBe(200);
137
+
138
+ const repeat = await generate({ tenantId: 'tenant-a', cachePrefixId: prefixId });
139
+ expect(repeat.status).toBe(200);
140
+ expect(
141
+ repeat.body.usage?.cacheReadTokens ?? 0,
142
+ driver.describe(
143
+ 'ai-envelope.md §"Prompt-prefix cache (RFC 0116)" rule 4',
144
+ 'a repeat generate with the same cachePrefixId for the SAME tenant MUST be an observable cache hit (cacheReadTokens > 0)',
145
+ ),
146
+ ).toBeGreaterThan(0);
147
+ // The cost-only witness MUST NOT have changed the recorded outcome.
148
+ expect(repeat.body.usage?.inputTokens).toBe(prime.body.usage?.inputTokens);
149
+ expect(repeat.body.usage?.outputTokens).toBe(prime.body.usage?.outputTokens);
150
+ });
151
+
152
+ it('(c) cross-tenant isolation — tenant B first use of tenant A\'s cachePrefixId → cacheReadTokens == 0', async () => {
153
+ const cap = await readCap();
154
+ if (!cap || cap.supported !== true) return; // not advertised — skip
155
+ const prefixId = `xtenant-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
156
+
157
+ // Tenant A primes the cache under a shared, predictable cachePrefixId.
158
+ const aPrime = await generate({ tenantId: 'tenant-a', cachePrefixId: prefixId });
159
+ if (aPrime.status === 404 || aPrime.status === 405) return; // seam not wired
160
+ expect(aPrime.status).toBe(200);
161
+
162
+ // Tenant B's FIRST use of the SAME cachePrefixId MUST be a miss — the host
163
+ // keys its provider cache by (resolved tenant, cachePrefixId), never global.
164
+ const bFirst = await generate({ tenantId: 'tenant-b', cachePrefixId: prefixId });
165
+ expect(bFirst.status).toBe(200);
166
+ expect(
167
+ bFirst.body.usage?.cacheReadTokens ?? 0,
168
+ driver.describe(
169
+ 'SECURITY/invariants.yaml prompt-prefix-cache-cross-tenant-isolation',
170
+ 'tenant B\'s first use of tenant A\'s cachePrefixId MUST be a cache MISS (cacheReadTokens == 0) — the cache MUST be keyed by (tenant, cachePrefixId), never global; cross-tenant sharing is context leakage',
171
+ ),
172
+ ).toBe(0);
173
+ });
174
+
175
+ it('(d) secret-free — the response never echoes cachePrefixId in a SR-1-sensitive position', async () => {
176
+ const cap = await readCap();
177
+ if (!cap || cap.supported !== true) return; // not advertised — skip
178
+ const prefixId = `secretfree-${Date.now()}`;
179
+
180
+ const res = await generate({ tenantId: 'tenant-a', cachePrefixId: prefixId });
181
+ if (res.status === 404 || res.status === 405) return; // seam not wired
182
+ expect(res.status).toBe(200);
183
+ // The usage block is cost-only; it MUST NOT carry prompt/response substrings
184
+ // (SR-1). cachePrefixId is a public cache key, but the cost witness fields
185
+ // themselves are integers — assert the usage block is shape-clean.
186
+ const usage = res.body.usage ?? {};
187
+ for (const k of ['inputTokens', 'outputTokens', 'cacheReadTokens', 'cacheWriteTokens'] as const) {
188
+ const v = usage[k];
189
+ if (v !== undefined) {
190
+ expect(
191
+ typeof v,
192
+ driver.describe(
193
+ 'run-event-payloads.schema.json §providerUsage',
194
+ `provider.usage.${k} MUST be a cost-only integer (no prompt substrings per SR-1)`,
195
+ ),
196
+ ).toBe('number');
197
+ }
198
+ }
199
+ });
200
+ });
@@ -0,0 +1,236 @@
1
+ /**
2
+ * run-transport-economy — RFC 0115 behavioral.
3
+ *
4
+ * Status: ACTIVE (capability-gated behavioral). Gated on
5
+ * `capabilities.restTransport.conditionalRunGet === true` (conditional-GET
6
+ * leg) and on a non-empty `capabilities.restTransport.contentEncodings`
7
+ * (compression leg). Both legs soft-skip on hosts that do not advertise the
8
+ * surface (incl. the reference workflow-engine, which has not yet wired the
9
+ * sequence-derived `ETag` path), so they light up the moment a host advertises
10
+ * `restTransport`.
11
+ *
12
+ * Asserts (per `spec/v1/rest-endpoints.md` §"`GET /v1/runs/{runId}`
13
+ * conditional read + Content-Encoding (RFC 0115)"):
14
+ *
15
+ * 1. `GET /v1/runs/{runId}` carries a strong `ETag` on the `200`.
16
+ * 2. A re-`GET` with `If-None-Match: <current ETag>` returns `304 Not
17
+ * Modified` with an empty body while the run has NOT advanced (the
18
+ * validator is stable while no observable transition occurs).
19
+ * 3. After the run advances (the `conformance-approval` fixture is resumed
20
+ * from `waiting-approval` to `completed`), the `ETag` CHANGES — proving
21
+ * it is derived from the run's latest persisted event-log sequence
22
+ * number, not a coarser signal that could leave a `304` stale.
23
+ * 4. For each advertised `contentEncodings` value, requesting it via
24
+ * `Accept-Encoding` yields a `Content-Encoding`-tagged response whose
25
+ * decoded bytes are byte-identical to the identity body.
26
+ *
27
+ * Non-vacuity: the `conformance-approval` fixture gives two deterministic,
28
+ * stable observable states (suspended → completed), so the ETag-stability and
29
+ * ETag-change assertions are exact rather than racing a fast run.
30
+ *
31
+ * @see RFCS/0115-run-transport-economy.md
32
+ * @see spec/v1/rest-endpoints.md §"`GET /v1/runs/{runId}` conditional read + Content-Encoding (RFC 0115)"
33
+ * @see spec/v1/replay.md §"durable event log" (the monotonic sequence the ETag derives from)
34
+ */
35
+
36
+ import { describe, it, expect } from 'vitest';
37
+ import { gunzipSync, brotliDecompressSync } from 'node:zlib';
38
+ import * as zlib from 'node:zlib';
39
+ import { driver } from '../lib/driver.js';
40
+ import { loadEnv } from '../lib/env.js';
41
+ import { capabilityFamily } from '../lib/discovery-capabilities.js';
42
+ import { pollUntilStatus, pollUntilTerminal } from '../lib/polling.js';
43
+
44
+ const HTTP_SKIP = !process.env.OPENWOP_BASE_URL;
45
+ const APPROVAL_FIXTURE = 'conformance-approval';
46
+ const APPROVAL_NODE_ID = 'gate';
47
+ const NOOP_FIXTURE = 'conformance-noop';
48
+
49
+ type Encoding = 'gzip' | 'br' | 'zstd';
50
+
51
+ interface RestTransportCaps {
52
+ conditionalRunGet?: unknown;
53
+ contentEncodings?: unknown;
54
+ }
55
+
56
+ async function readRestTransport(): Promise<RestTransportCaps | undefined> {
57
+ try {
58
+ const res = await driver.get('/.well-known/openwop');
59
+ if (res.status !== 200) return undefined;
60
+ return capabilityFamily<RestTransportCaps>(res.json, 'restTransport');
61
+ } catch {
62
+ return undefined;
63
+ }
64
+ }
65
+
66
+ /** Advertised content-encodings, narrowed to the spec enum. */
67
+ function advertisedEncodings(caps: RestTransportCaps | undefined): Encoding[] {
68
+ const raw = caps?.contentEncodings;
69
+ if (!Array.isArray(raw)) return [];
70
+ return raw.filter((e): e is Encoding => e === 'gzip' || e === 'br' || e === 'zstd');
71
+ }
72
+
73
+ /** Read the run snapshot's ETag header (case-insensitive via Headers). */
74
+ function etagOf(res: { headers: Headers }): string | null {
75
+ return res.headers.get('etag');
76
+ }
77
+
78
+ describe.skipIf(HTTP_SKIP)('run-transport-economy: conditional GET on run reads (RFC 0115)', () => {
79
+ it('emits a sequence-derived strong ETag, honors If-None-Match with 304, and rotates the ETag when the run advances', async (ctx) => {
80
+ const caps = await readRestTransport();
81
+ if (caps?.conditionalRunGet !== true) {
82
+ ctx.skip(); // host does not advertise restTransport.conditionalRunGet
83
+ return;
84
+ }
85
+
86
+ // Use the approval fixture: it parks at a stable `waiting-approval` state,
87
+ // then advances to `completed` on resolve — two deterministic snapshots.
88
+ const create = await driver.post('/v1/runs', { workflowId: APPROVAL_FIXTURE });
89
+ if (create.status === 404 || create.status === 422) {
90
+ ctx.skip(); // fixture not advertised by this host
91
+ return;
92
+ }
93
+ expect(create.status).toBe(201);
94
+ const runId = (create.json as { runId: string }).runId;
95
+ await pollUntilStatus(runId, 'waiting-approval', { timeoutMs: 10_000 });
96
+
97
+ // (1) Strong ETag present on the 200.
98
+ const suspendedRead = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
99
+ expect(suspendedRead.status).toBe(200);
100
+ const etagSuspended = etagOf(suspendedRead);
101
+ expect(
102
+ etagSuspended,
103
+ driver.describe(
104
+ 'rest-endpoints.md §GET /v1/runs/{runId} conditional read (RFC 0115)',
105
+ 'a host advertising restTransport.conditionalRunGet MUST return a strong ETag on the 200',
106
+ ),
107
+ ).toBeTruthy();
108
+
109
+ // (2) If-None-Match with the current ETag → 304, empty body, while unchanged.
110
+ const revalidate = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`, {
111
+ headers: { 'If-None-Match': etagSuspended as string },
112
+ });
113
+ expect(
114
+ revalidate.status,
115
+ driver.describe(
116
+ 'rest-endpoints.md §GET /v1/runs/{runId} conditional read (RFC 0115)',
117
+ 'If-None-Match matching the current ETag MUST return 304 Not Modified',
118
+ ),
119
+ ).toBe(304);
120
+ expect(
121
+ revalidate.text,
122
+ driver.describe(
123
+ 'rest-endpoints.md §GET /v1/runs/{runId} conditional read (RFC 0115)',
124
+ '304 Not Modified MUST carry no body',
125
+ ),
126
+ ).toBe('');
127
+
128
+ // Advance the run: resolve the approval interrupt → terminal `completed`.
129
+ const resolve = await driver.post(
130
+ `/v1/runs/${encodeURIComponent(runId)}/interrupts/${encodeURIComponent(APPROVAL_NODE_ID)}`,
131
+ { resumeValue: { action: 'accept' } },
132
+ );
133
+ expect(resolve.status).toBe(200);
134
+ const terminal = await pollUntilTerminal(runId, { timeoutMs: 10_000 });
135
+ expect(terminal.status).toBe('completed');
136
+
137
+ // (3) ETag rotates after the observable state advanced.
138
+ const completedRead = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`);
139
+ expect(completedRead.status).toBe(200);
140
+ const etagCompleted = etagOf(completedRead);
141
+ expect(etagCompleted).toBeTruthy();
142
+ expect(
143
+ etagCompleted,
144
+ driver.describe(
145
+ 'rest-endpoints.md §GET /v1/runs/{runId} conditional read (RFC 0115)',
146
+ 'the ETag MUST change once the run advances (it is derived from the latest event-log sequence); a stable ETag across an observable transition would leave a 304 stale',
147
+ ),
148
+ ).not.toBe(etagSuspended);
149
+
150
+ // The new ETag is itself stable while the (now terminal) run does not change.
151
+ const revalidateTerminal = await driver.get(`/v1/runs/${encodeURIComponent(runId)}`, {
152
+ headers: { 'If-None-Match': etagCompleted as string },
153
+ });
154
+ expect(
155
+ revalidateTerminal.status,
156
+ driver.describe(
157
+ 'rest-endpoints.md §GET /v1/runs/{runId} conditional read (RFC 0115)',
158
+ 'the terminal ETag MUST be stable — If-None-Match against it returns 304',
159
+ ),
160
+ ).toBe(304);
161
+ });
162
+ });
163
+
164
+ describe.skipIf(HTTP_SKIP)('run-transport-economy: Content-Encoding round-trips byte-identically (RFC 0115)', () => {
165
+ it('each advertised contentEncodings value decodes to the identity body byte-for-byte', async (ctx) => {
166
+ const caps = await readRestTransport();
167
+ const encodings = advertisedEncodings(caps);
168
+ if (encodings.length === 0) {
169
+ ctx.skip(); // host advertises no run-read content encodings
170
+ return;
171
+ }
172
+
173
+ // A terminal run gives a stable body to compare encodings against.
174
+ const create = await driver.post('/v1/runs', { workflowId: NOOP_FIXTURE });
175
+ if (create.status === 404 || create.status === 422) {
176
+ ctx.skip();
177
+ return;
178
+ }
179
+ expect(create.status).toBe(201);
180
+ const runId = (create.json as { runId: string }).runId;
181
+ await pollUntilTerminal(runId, { timeoutMs: 10_000 });
182
+
183
+ const env = loadEnv();
184
+ const url = `${env.baseUrl}/v1/runs/${encodeURIComponent(runId)}`;
185
+ const auth = { Authorization: `Bearer ${env.apiKey}`, Accept: 'application/json' };
186
+
187
+ // Identity baseline — explicit Accept-Encoding: identity so the host does
188
+ // not compress; raw bytes are the comparison oracle.
189
+ const identityRes = await fetch(url, { headers: { ...auth, 'Accept-Encoding': 'identity' } });
190
+ expect(identityRes.status).toBe(200);
191
+ const identityBytes = Buffer.from(await identityRes.arrayBuffer());
192
+ expect(identityBytes.length).toBeGreaterThan(0);
193
+
194
+ // Feature-detect zstd decode (Node >= 22.15 / 23.8); when absent we still
195
+ // assert the host negotiated Content-Encoding but defer the byte-compare.
196
+ // Cast-free: the optional-property view is assignable from the zlib module
197
+ // namespace under structural typing whether or not @types/node declares it.
198
+ const zlibMaybeZstd: { zstdDecompressSync?: (b: Buffer) => Buffer } = zlib;
199
+ const zstdDecode: ((b: Buffer) => Buffer) | undefined =
200
+ typeof zlibMaybeZstd.zstdDecompressSync === 'function'
201
+ ? zlibMaybeZstd.zstdDecompressSync
202
+ : undefined;
203
+
204
+ for (const enc of encodings) {
205
+ // Manually set Accept-Encoding so undici returns the raw compressed
206
+ // bytes (it only auto-decompresses encodings it negotiated itself).
207
+ const res = await fetch(url, { headers: { ...auth, 'Accept-Encoding': enc } });
208
+ expect(res.status).toBe(200);
209
+ const contentEncoding = res.headers.get('content-encoding');
210
+ expect(
211
+ contentEncoding,
212
+ driver.describe(
213
+ 'rest-endpoints.md §GET /v1/runs/{runId} conditional read + Content-Encoding (RFC 0115)',
214
+ `a host advertising restTransport.contentEncodings:["...","${enc}"] MUST set Content-Encoding: ${enc} when that encoding is requested`,
215
+ ),
216
+ ).toBe(enc);
217
+
218
+ const compressedBytes = Buffer.from(await res.arrayBuffer());
219
+ const decode = enc === 'gzip' ? gunzipSync : enc === 'br' ? brotliDecompressSync : zstdDecode;
220
+ if (!decode) {
221
+ // zstd decode unavailable in this runtime: negotiation already
222
+ // asserted above; skip only the byte-compare for this encoding.
223
+ expect(compressedBytes.length).toBeGreaterThan(0);
224
+ continue;
225
+ }
226
+ const decoded = Buffer.from(decode(compressedBytes));
227
+ expect(
228
+ decoded.equals(identityBytes),
229
+ driver.describe(
230
+ 'rest-endpoints.md §GET /v1/runs/{runId} conditional read + Content-Encoding (RFC 0115)',
231
+ `the ${enc}-decoded body MUST be byte-identical to the identity body (Content-Encoding MUST NOT alter decoded bytes or semantics)`,
232
+ ),
233
+ ).toBe(true);
234
+ }
235
+ });
236
+ });