@openwop/openwop-conformance 1.1.0 → 1.2.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 +25 -0
- package/README.md +2 -2
- package/coverage.md +29 -17
- package/fixtures/conformance-agent-low-confidence.json +7 -4
- package/fixtures/conformance-agent-pack-handoff-schema-validation.json +30 -0
- package/fixtures/conformance-agent-reasoning.json +23 -4
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-a.json +27 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff-child-b.json +25 -0
- package/fixtures/conformance-dispatch-cross-worker-handoff.json +60 -0
- package/fixtures/conformance-dispatch-input-mapping-child.json +25 -0
- package/fixtures/conformance-dispatch-input-mapping.json +49 -0
- package/fixtures/conformance-dispatch-output-mapping-child.json +27 -0
- package/fixtures/conformance-dispatch-output-mapping.json +49 -0
- package/fixtures/conformance-subworkflow-input-mapping-child.json +27 -0
- package/fixtures/conformance-subworkflow-input-mapping.json +33 -0
- package/fixtures.md +12 -2
- package/package.json +1 -1
- package/schemas/README.md +7 -0
- package/schemas/agent-ref.schema.json +1 -1
- package/schemas/ai-envelope.schema.json +106 -0
- package/schemas/capabilities.schema.json +300 -3
- package/schemas/core-conformance-mock-agent-config.schema.json +147 -0
- package/schemas/dispatch-config.schema.json +26 -0
- package/schemas/envelopes/clarification.request.schema.json +43 -0
- package/schemas/envelopes/error.schema.json +26 -0
- package/schemas/envelopes/schema.request.schema.json +22 -0
- package/schemas/envelopes/schema.response.schema.json +22 -0
- package/schemas/node-pack-manifest.schema.json +5 -0
- package/schemas/pack-lockfile.schema.json +16 -0
- package/schemas/run-event-payloads.schema.json +18 -2
- package/schemas/run-event.schema.json +2 -1
- package/schemas/workflow-chain-pack-manifest.schema.json +226 -0
- package/src/lib/behavior-gate.ts +44 -5
- package/src/lib/env.ts +27 -0
- package/src/lib/webhook-receiver.ts +137 -0
- package/src/lib/workflow-chain-expansion.ts +213 -0
- package/src/scenarios/agentPackCatalog.test.ts +216 -0
- package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +146 -0
- package/src/scenarios/agentReasoningEvents.test.ts +58 -7
- package/src/scenarios/agents-run-tool-allowlist.test.ts +182 -0
- package/src/scenarios/ai-envelope-shape.test.ts +362 -0
- package/src/scenarios/aiEnvelope.capBreached.test.ts +173 -0
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +150 -0
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +69 -0
- package/src/scenarios/aiEnvelope.redaction.test.ts +73 -0
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +87 -0
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +143 -0
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +176 -0
- package/src/scenarios/append-ordering.test.ts +44 -0
- package/src/scenarios/artifact-auth.test.ts +58 -0
- package/src/scenarios/blob-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/blob-presign-expiry.test.ts +66 -0
- package/src/scenarios/blob-roundtrip.test.ts +48 -0
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +61 -0
- package/src/scenarios/cache-ttl-expiry.test.ts +47 -0
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +98 -0
- package/src/scenarios/dispatch-input-mapping.test.ts +94 -0
- package/src/scenarios/dispatch-output-mapping.test.ts +65 -0
- package/src/scenarios/fs-path-traversal.test.ts +124 -0
- package/src/scenarios/idempotency-key-determinism.test.ts +230 -0
- package/src/scenarios/interrupt-token-matrix.test.ts +126 -0
- package/src/scenarios/kv-atomic-increment.test.ts +74 -0
- package/src/scenarios/kv-cas.test.ts +75 -0
- package/src/scenarios/kv-cross-tenant-isolation.test.ts +85 -0
- package/src/scenarios/kv-ttl-expiry.test.ts +47 -0
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +92 -0
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +80 -0
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +82 -0
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +84 -0
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +107 -0
- package/src/scenarios/mcp-server-untrusted-args.test.ts +105 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +13 -6
- package/src/scenarios/memory-compaction-event-emitted.test.ts +121 -0
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +116 -0
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +127 -0
- package/src/scenarios/multi-region-idempotency.test.ts +39 -4
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +139 -0
- package/src/scenarios/pause-resume.test.ts +43 -0
- package/src/scenarios/queue-ack-nack-dlq.test.ts +67 -0
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +66 -0
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +48 -0
- package/src/scenarios/registry-public.test.ts +91 -0
- package/src/scenarios/search-bm25-roundtrip.test.ts +47 -0
- package/src/scenarios/spec-corpus-validity.test.ts +28 -7
- package/src/scenarios/sql-injection-rejection.test.ts +84 -0
- package/src/scenarios/sql-transaction-atomicity.test.ts +66 -0
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +66 -0
- package/src/scenarios/subworkflow-input-mapping.test.ts +100 -0
- package/src/scenarios/table-cross-tenant-isolation.test.ts +65 -0
- package/src/scenarios/table-cursor-pagination.test.ts +47 -0
- package/src/scenarios/table-schema-enforcement.test.ts +47 -0
- package/src/scenarios/vector-knn-roundtrip.test.ts +48 -0
- package/src/scenarios/webhook-receiver-adversarial.test.ts +210 -0
- package/src/scenarios/workflow-chain-expansion.test.ts +366 -0
- package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +232 -0
- package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +138 -0
- package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +170 -0
|
@@ -0,0 +1,139 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Track 11 close-out: cross-run trace-context propagation across
|
|
3
|
+
* `core.subWorkflow` invocation.
|
|
4
|
+
*
|
|
5
|
+
* `otel-trace-propagation.test.ts` verifies that a single run's spans
|
|
6
|
+
* inherit an inbound `traceparent`'s traceId. This scenario closes the
|
|
7
|
+
* remaining gap (`conformance/coverage.md` row 52: "Cross-host
|
|
8
|
+
* propagation across `core.subWorkflow` invocation"): when a parent
|
|
9
|
+
* run with a known inbound traceparent dispatches a child run via
|
|
10
|
+
* `core.subWorkflow`, the CHILD run's emitted spans MUST also share
|
|
11
|
+
* the parent's traceId — distributed traces stitch across the
|
|
12
|
+
* sub-workflow boundary without operator-side correlation hacks.
|
|
13
|
+
*
|
|
14
|
+
* Operator-tier value: in production deployments, a sub-workflow may
|
|
15
|
+
* execute on a different host instance (`core.subWorkflow` is a
|
|
16
|
+
* dispatch boundary, not necessarily an in-process call). The
|
|
17
|
+
* traceparent-propagation contract guarantees the operator's OTel
|
|
18
|
+
* backend can render parent + child as one trace tree even when
|
|
19
|
+
* they're on separate hosts.
|
|
20
|
+
*
|
|
21
|
+
* Skip conditions:
|
|
22
|
+
* - Collector disabled.
|
|
23
|
+
* - Host doesn't advertise `capabilities.observability`.
|
|
24
|
+
* - `conformance-subworkflow-parent` fixture not advertised (host
|
|
25
|
+
* doesn't implement `core.subWorkflow`).
|
|
26
|
+
*
|
|
27
|
+
* @see spec/v1/observability.md §"Trace context propagation"
|
|
28
|
+
* @see spec/v1/node-packs.md §`core.subWorkflow`
|
|
29
|
+
* @see conformance/coverage.md row 52
|
|
30
|
+
*/
|
|
31
|
+
|
|
32
|
+
import { describe, it, expect } from 'vitest';
|
|
33
|
+
import { driver } from '../lib/driver.js';
|
|
34
|
+
import { pollUntilTerminal } from '../lib/polling.js';
|
|
35
|
+
import { isFixtureAdvertised } from '../lib/fixtures.js';
|
|
36
|
+
import { getCollector, waitForRunSpans } from '../lib/otel-collector.js';
|
|
37
|
+
|
|
38
|
+
const PARENT_FIXTURE = 'conformance-subworkflow-parent';
|
|
39
|
+
|
|
40
|
+
interface RunEvent {
|
|
41
|
+
type: string;
|
|
42
|
+
nodeId?: string;
|
|
43
|
+
payload?: { outputs?: { childRunId?: string } };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeTraceparent(): { header: string; traceId: string } {
|
|
47
|
+
// W3C format: 00-<32 hex traceId>-<16 hex spanId>-01.
|
|
48
|
+
// Use a distinct id from the parent-only scenario so collector
|
|
49
|
+
// matching is unambiguous when both scenarios run back-to-back.
|
|
50
|
+
const traceId = '7c3e51b9d2a04e6f8b1c0d2e3f4a5b6c';
|
|
51
|
+
const spanId = '00f067aa0ba902b7';
|
|
52
|
+
return { header: `00-${traceId}-${spanId}-01`, traceId };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
async function isObservabilityAdvertised(): Promise<boolean> {
|
|
56
|
+
try {
|
|
57
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
58
|
+
const caps = (disco.json as { capabilities?: { observability?: unknown } }).capabilities ?? {};
|
|
59
|
+
return caps.observability !== undefined;
|
|
60
|
+
} catch {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
describe('otel-trace-propagation-subworkflow: traceparent threads parent → child via core.subWorkflow', () => {
|
|
66
|
+
it('child run spans inherit the parent run\'s inbound traceId', async () => {
|
|
67
|
+
if (!getCollector()) {
|
|
68
|
+
// eslint-disable-next-line no-console
|
|
69
|
+
console.warn('[otel-trace-propagation-subworkflow] collector not started; skipping');
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
if (!isFixtureAdvertised(PARENT_FIXTURE)) {
|
|
73
|
+
// eslint-disable-next-line no-console
|
|
74
|
+
console.warn(`[otel-trace-propagation-subworkflow] ${PARENT_FIXTURE} not advertised; skipping`);
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
if (!(await isObservabilityAdvertised())) {
|
|
78
|
+
// eslint-disable-next-line no-console
|
|
79
|
+
console.warn('[otel-trace-propagation-subworkflow] capabilities.observability not advertised; skipping');
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const collector = getCollector()!;
|
|
84
|
+
collector.reset();
|
|
85
|
+
|
|
86
|
+
const { header, traceId } = makeTraceparent();
|
|
87
|
+
const create = await driver.post(
|
|
88
|
+
'/v1/runs',
|
|
89
|
+
{ workflowId: PARENT_FIXTURE },
|
|
90
|
+
{ headers: { traceparent: header } },
|
|
91
|
+
);
|
|
92
|
+
expect(create.status).toBe(201);
|
|
93
|
+
const parentRunId = (create.json as { runId: string }).runId;
|
|
94
|
+
|
|
95
|
+
await pollUntilTerminal(parentRunId, { timeoutMs: 30_000 });
|
|
96
|
+
|
|
97
|
+
// Walk the parent's event log to discover the child run id.
|
|
98
|
+
const eventsRes = await driver.get(
|
|
99
|
+
`/v1/runs/${encodeURIComponent(parentRunId)}/events/poll?lastSequence=0&timeout=1`,
|
|
100
|
+
);
|
|
101
|
+
expect(eventsRes.status).toBe(200);
|
|
102
|
+
const events = (eventsRes.json as { events?: RunEvent[] } | undefined)?.events ?? [];
|
|
103
|
+
|
|
104
|
+
const subwfCompleted = events.find(
|
|
105
|
+
(e) => e.type === 'node.completed' && e.nodeId === 'subwf-call',
|
|
106
|
+
);
|
|
107
|
+
expect(subwfCompleted, driver.describe(
|
|
108
|
+
'node-packs.md §core.subWorkflow',
|
|
109
|
+
'parent event log MUST include node.completed for the subwf-call node',
|
|
110
|
+
)).toBeDefined();
|
|
111
|
+
|
|
112
|
+
const childRunId = subwfCompleted?.payload?.outputs?.childRunId;
|
|
113
|
+
expect(typeof childRunId, driver.describe(
|
|
114
|
+
'node-packs.md §core.subWorkflow outputSchema',
|
|
115
|
+
'subwf-call node.completed payload MUST carry outputs.childRunId',
|
|
116
|
+
)).toBe('string');
|
|
117
|
+
|
|
118
|
+
// Both parent + child spans MUST share the inbound traceId.
|
|
119
|
+
const parentSpans = await waitForRunSpans(parentRunId, { timeoutMs: 10_000, minCount: 1 });
|
|
120
|
+
const childSpans = await waitForRunSpans(childRunId!, { timeoutMs: 10_000, minCount: 1 });
|
|
121
|
+
|
|
122
|
+
expect(parentSpans.length, 'collector MUST receive ≥1 span for the parent run').toBeGreaterThan(0);
|
|
123
|
+
expect(childSpans.length, 'collector MUST receive ≥1 span for the child run').toBeGreaterThan(0);
|
|
124
|
+
|
|
125
|
+
const wantTrace = traceId.toLowerCase();
|
|
126
|
+
|
|
127
|
+
const parentMatching = parentSpans.filter((s) => s.traceId.toLowerCase() === wantTrace);
|
|
128
|
+
expect(parentMatching.length, driver.describe(
|
|
129
|
+
'observability.md §"Trace context propagation"',
|
|
130
|
+
'parent-run spans MUST share the inbound traceparent traceId',
|
|
131
|
+
)).toBeGreaterThan(0);
|
|
132
|
+
|
|
133
|
+
const childMatching = childSpans.filter((s) => s.traceId.toLowerCase() === wantTrace);
|
|
134
|
+
expect(childMatching.length, driver.describe(
|
|
135
|
+
'observability.md §"Trace context propagation" + node-packs.md §core.subWorkflow',
|
|
136
|
+
'child-run spans dispatched via core.subWorkflow MUST inherit the parent run\'s traceId so distributed traces stitch across the dispatch boundary',
|
|
137
|
+
)).toBeGreaterThan(0);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
@@ -226,3 +226,46 @@ describe.skipIf(SKIP)('pause/resume: :pause-during-suspend race', () => {
|
|
|
226
226
|
});
|
|
227
227
|
});
|
|
228
228
|
});
|
|
229
|
+
|
|
230
|
+
// CF-2 close-out — drain-policy discrimination per
|
|
231
|
+
// `capabilities.md` §`runs.pauseResume`. When a host advertises
|
|
232
|
+
// `drainPolicies[]`, each advertised value MUST be accepted with 202.
|
|
233
|
+
// Skips entirely when no advertisement is present.
|
|
234
|
+
describe.skipIf(SKIP)('pause/resume: drainPolicy discrimination per capabilities advertisement', () => {
|
|
235
|
+
it('every drainPolicy advertised by the host is accepted on :pause', async () => {
|
|
236
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
237
|
+
const drainPolicies =
|
|
238
|
+
(disco.json as {
|
|
239
|
+
capabilities?: { runs?: { pauseResume?: { drainPolicies?: string[] } } };
|
|
240
|
+
}).capabilities?.runs?.pauseResume?.drainPolicies ?? [];
|
|
241
|
+
if (drainPolicies.length === 0) {
|
|
242
|
+
// eslint-disable-next-line no-console
|
|
243
|
+
console.warn('[pause-resume] host advertises no drainPolicies; skipping policy-discrimination subtest');
|
|
244
|
+
return;
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
for (const policy of drainPolicies) {
|
|
248
|
+
const create = await driver.post('/v1/runs', {
|
|
249
|
+
workflowId: FIXTURE!,
|
|
250
|
+
inputs: { delaySeconds: 30 },
|
|
251
|
+
});
|
|
252
|
+
expect(create.status).toBe(201);
|
|
253
|
+
const runId = (create.json as { runId: string }).runId;
|
|
254
|
+
|
|
255
|
+
await pollUntilStatus(runId, 'running', { timeoutMs: 10_000 });
|
|
256
|
+
|
|
257
|
+
const pause = await driver.post(`/v1/runs/${encodeURIComponent(runId)}:pause`, {
|
|
258
|
+
reason: `conformance-drainpolicy-${policy}`,
|
|
259
|
+
drainPolicy: policy,
|
|
260
|
+
});
|
|
261
|
+
expect(pause.status, driver.describe(
|
|
262
|
+
'capabilities.md §`runs.pauseResume.drainPolicies` + rest-endpoints.md POST /v1/runs/{runId}:pause',
|
|
263
|
+
`host-advertised drainPolicy='${policy}' MUST be accepted on :pause`,
|
|
264
|
+
)).toBe(202);
|
|
265
|
+
|
|
266
|
+
await driver.post(`/v1/runs/${encodeURIComponent(runId)}/cancel`, {
|
|
267
|
+
reason: 'conformance-cleanup',
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
});
|
|
271
|
+
});
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queue-ack-nack-dlq — RFC 0017 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0017 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.queueBus` block has landed in
|
|
6
|
+
* `schemas/capabilities.schema.json`. This scenario asserts the advertisement
|
|
7
|
+
* shape against any host that boots the conformance suite, and keeps the
|
|
8
|
+
* deeper behavioral assertions as `it.todo()` until a reference host wires
|
|
9
|
+
* a test seam.
|
|
10
|
+
*
|
|
11
|
+
* Summary: nack returns for redelivery; deadLetter routes to the configured DLQ.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0017-*.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
|
|
19
|
+
interface DiscoveryDoc {
|
|
20
|
+
capabilities?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
|
+
const res = await driver.get('/.well-known/openwop');
|
|
25
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
27
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('queue-ack-nack-dlq: advertisement shape (RFC 0017)', () => {
|
|
32
|
+
it('capabilities.queueBus is either absent or a well-formed object', async () => {
|
|
33
|
+
const cap = await readCap();
|
|
34
|
+
if (cap === null) return; // host doesn't advertise — skip
|
|
35
|
+
expect(
|
|
36
|
+
typeof cap.supported,
|
|
37
|
+
driver.describe(
|
|
38
|
+
'capabilities.schema.json §queueBus',
|
|
39
|
+
'capabilities.queueBus.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('deadLetterSupported is a boolean when set', async () => {
|
|
45
|
+
const cap = await readCap();
|
|
46
|
+
if (!cap || cap.supported !== true) return;
|
|
47
|
+
const subParts = ["deadLetterSupported"];
|
|
48
|
+
let sub: unknown = cap;
|
|
49
|
+
for (const p of subParts) {
|
|
50
|
+
if (sub && typeof sub === 'object') sub = (sub as Record<string, unknown>)[p];
|
|
51
|
+
else { sub = undefined; break; }
|
|
52
|
+
}
|
|
53
|
+
if (sub === undefined) return; // optional sub-field
|
|
54
|
+
expect(
|
|
55
|
+
typeof sub,
|
|
56
|
+
driver.describe(
|
|
57
|
+
'RFC 0017 §A',
|
|
58
|
+
'queueBus.deadLetterSupported MUST be boolean when present',
|
|
59
|
+
),
|
|
60
|
+
).toBe('boolean');
|
|
61
|
+
});
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
describe('queue-ack-nack-dlq: behavioral assertions (placeholders — need host test seam)', () => {
|
|
65
|
+
it.todo("nack(requeue=true) → message is redelivered on next consume");
|
|
66
|
+
it.todo("deadLetter → message appears on the configured DLQ");
|
|
67
|
+
});
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queue-cross-tenant-isolation — RFC 0017 §C + SECURITY/invariants.yaml
|
|
3
|
+
* `queue-cross-tenant-isolation`.
|
|
4
|
+
*
|
|
5
|
+
* Status: ACTIVE (advertisement + behavioral). Asserts that messages
|
|
6
|
+
* published under tenant A on topic T MUST NOT be consumed under tenant B
|
|
7
|
+
* on the same topic.
|
|
8
|
+
*
|
|
9
|
+
* @see RFCS/0017-host-queue-bus-capability.md
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { driver } from '../lib/driver.js';
|
|
14
|
+
|
|
15
|
+
interface DiscoveryDoc {
|
|
16
|
+
capabilities?: Record<string, unknown>;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
20
|
+
const res = await driver.get('/.well-known/openwop');
|
|
21
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
22
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
23
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
|
|
24
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function call(tenantId: string, op: string, args: Record<string, unknown>) {
|
|
28
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId, surface: 'queueBus', op, args });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('queue-cross-tenant-isolation: advertisement shape (RFC 0017)', () => {
|
|
32
|
+
it('capabilities.queueBus is either absent or a well-formed object', async () => {
|
|
33
|
+
const cap = await readCap();
|
|
34
|
+
if (cap === null) return;
|
|
35
|
+
expect(
|
|
36
|
+
typeof cap.supported,
|
|
37
|
+
driver.describe(
|
|
38
|
+
'capabilities.schema.json §queueBus',
|
|
39
|
+
'capabilities.queueBus.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('queue-cross-tenant-isolation: behavioral (RFC 0017 §C)', () => {
|
|
46
|
+
it('publish under tenant A → consume under tenant B returns not-found', async () => {
|
|
47
|
+
const cap = await readCap();
|
|
48
|
+
if (!cap || cap.supported !== true) return;
|
|
49
|
+
const topic = `xtenant.${Date.now()}.${Math.random().toString(36).slice(2, 6)}`;
|
|
50
|
+
|
|
51
|
+
const pubRes = await call('tenant-a', 'publish', { topic, payload: { hello: 'A' } });
|
|
52
|
+
if (pubRes.status === 404) return;
|
|
53
|
+
expect(pubRes.status, 'publish MUST succeed').toBe(200);
|
|
54
|
+
|
|
55
|
+
const consRes = await call('tenant-b', 'consume', { topic, timeoutMs: 100 });
|
|
56
|
+
expect(consRes.status).toBe(200);
|
|
57
|
+
const body = consRes.json as { found?: boolean };
|
|
58
|
+
expect(
|
|
59
|
+
body.found,
|
|
60
|
+
driver.describe(
|
|
61
|
+
'SECURITY/invariants.yaml queue-cross-tenant-isolation',
|
|
62
|
+
'tenant B MUST NOT consume tenant A messages on the same topic',
|
|
63
|
+
),
|
|
64
|
+
).toBe(false);
|
|
65
|
+
});
|
|
66
|
+
});
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* queue-publish-consume-roundtrip — RFC 0017 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0017 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.queueBus` block has landed in
|
|
6
|
+
* `schemas/capabilities.schema.json`. This scenario asserts the advertisement
|
|
7
|
+
* shape against any host that boots the conformance suite, and keeps the
|
|
8
|
+
* deeper behavioral assertions as `it.todo()` until a reference host wires
|
|
9
|
+
* a test seam.
|
|
10
|
+
*
|
|
11
|
+
* Summary: publish + consume + ack roundtrip.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0017-*.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
|
|
19
|
+
interface DiscoveryDoc {
|
|
20
|
+
capabilities?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
|
+
const res = await driver.get('/.well-known/openwop');
|
|
25
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
27
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["queueBus"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('queue-publish-consume-roundtrip: advertisement shape (RFC 0017)', () => {
|
|
32
|
+
it('capabilities.queueBus is either absent or a well-formed object', async () => {
|
|
33
|
+
const cap = await readCap();
|
|
34
|
+
if (cap === null) return; // host doesn't advertise — skip
|
|
35
|
+
expect(
|
|
36
|
+
typeof cap.supported,
|
|
37
|
+
driver.describe(
|
|
38
|
+
'capabilities.schema.json §queueBus',
|
|
39
|
+
'capabilities.queueBus.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('queue-publish-consume-roundtrip: behavioral assertions (placeholders — need host test seam)', () => {
|
|
46
|
+
it.todo("publish → consume returns the message with the right payload + headers");
|
|
47
|
+
it.todo("ack removes the message; subsequent consume returns not-found within timeout");
|
|
48
|
+
});
|
|
@@ -26,6 +26,7 @@
|
|
|
26
26
|
*/
|
|
27
27
|
|
|
28
28
|
import { describe, it, expect } from 'vitest';
|
|
29
|
+
import { createHash, createPublicKey, verify as cryptoVerify } from 'node:crypto';
|
|
29
30
|
|
|
30
31
|
const REGISTRY_BASE = 'https://packs.openwop.dev';
|
|
31
32
|
const ENABLED = process.env.OPENWOP_TEST_PUBLIC_REGISTRY === 'true';
|
|
@@ -129,3 +130,93 @@ describe('registry-public: spec-canonical pack manifests resolve', () => {
|
|
|
129
130
|
});
|
|
130
131
|
}
|
|
131
132
|
});
|
|
133
|
+
|
|
134
|
+
describe('registry-public: tarball + signature + Ed25519 verify roundtrip', () => {
|
|
135
|
+
// core.openwop.examples@1.0.0 is the canonical reference pack for this
|
|
136
|
+
// check: published since the registry MVP, signed with the
|
|
137
|
+
// `openwop-registry-root` key over the whole tarball (method='ed25519').
|
|
138
|
+
// The same recipe applies to any pack at the registry; this scenario
|
|
139
|
+
// exercises the worst-case full roundtrip so clients have a wire-level
|
|
140
|
+
// contract for verifying packs before installing them.
|
|
141
|
+
//
|
|
142
|
+
// What gets asserted, in order:
|
|
143
|
+
// 1. Version manifest, tarball, signature, and public key all 200.
|
|
144
|
+
// 2. SRI integrity in the manifest matches a fresh sha256 of the
|
|
145
|
+
// tarball bytes — protects against tarball tampering between
|
|
146
|
+
// publish + retrieval.
|
|
147
|
+
// 3. Detached Ed25519 signature verifies against the public key over
|
|
148
|
+
// the bytes the publisher signed (per signing.method).
|
|
149
|
+
const PACK_NAME = 'core.openwop.examples';
|
|
150
|
+
const PACK_VERSION = '1.0.0';
|
|
151
|
+
|
|
152
|
+
async function getBinary(path: string): Promise<{ status: number; bytes: Buffer }> {
|
|
153
|
+
const res = await fetch(`${REGISTRY_BASE}${path}`);
|
|
154
|
+
const ab = await res.arrayBuffer();
|
|
155
|
+
return { status: res.status, bytes: Buffer.from(ab) };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
async function getText(path: string): Promise<{ status: number; body: string }> {
|
|
159
|
+
const res = await fetch(`${REGISTRY_BASE}${path}`);
|
|
160
|
+
const body = await res.text();
|
|
161
|
+
return { status: res.status, body };
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
it(`tarball + sig + public key all retrievable, SRI matches, Ed25519 verifies for ${PACK_NAME}@${PACK_VERSION}`, async () => {
|
|
165
|
+
if (!ENABLED) return;
|
|
166
|
+
|
|
167
|
+
// 1. Manifest (JSON).
|
|
168
|
+
const manifestRes = await get(`/v1/packs/${PACK_NAME}/-/${PACK_VERSION}.json`);
|
|
169
|
+
expect(manifestRes.status).toBe(200);
|
|
170
|
+
const manifest = manifestRes.json as {
|
|
171
|
+
signing?: { method?: string; keyId?: string; publicKeyUrl?: string };
|
|
172
|
+
integrity?: string;
|
|
173
|
+
};
|
|
174
|
+
expect(typeof manifest.signing?.method).toBe('string');
|
|
175
|
+
expect(typeof manifest.signing?.keyId).toBe('string');
|
|
176
|
+
expect(typeof manifest.integrity).toBe('string');
|
|
177
|
+
|
|
178
|
+
// 2. Tarball.
|
|
179
|
+
const tarball = await getBinary(`/v1/packs/${PACK_NAME}/-/${PACK_VERSION}.tgz`);
|
|
180
|
+
expect(tarball.status, 'tarball MUST be retrievable').toBe(200);
|
|
181
|
+
expect(tarball.bytes.byteLength, 'tarball MUST be non-empty').toBeGreaterThan(0);
|
|
182
|
+
|
|
183
|
+
// 3. Detached signature (64-byte raw Ed25519).
|
|
184
|
+
const sig = await getBinary(`/v1/packs/${PACK_NAME}/-/${PACK_VERSION}.sig`);
|
|
185
|
+
expect(sig.status, 'signature MUST be retrievable').toBe(200);
|
|
186
|
+
expect(sig.bytes.byteLength, 'Ed25519 detached signature MUST be 64 bytes').toBe(64);
|
|
187
|
+
|
|
188
|
+
// 4. Public key — fetch from the publisher-declared URL when present,
|
|
189
|
+
// else fall back to the canonical `/keys/<keyId>.pub` shape.
|
|
190
|
+
const keyUrl = manifest.signing!.publicKeyUrl ?? `/keys/${manifest.signing!.keyId}.pub`;
|
|
191
|
+
const keyRes = await getText(keyUrl);
|
|
192
|
+
expect(keyRes.status, `public key MUST be retrievable at ${keyUrl}`).toBe(200);
|
|
193
|
+
const publicKey = createPublicKey(keyRes.body);
|
|
194
|
+
|
|
195
|
+
// 5. SRI integrity check: `sha256-<base64>=` MUST match a fresh
|
|
196
|
+
// sha256 of the tarball bytes per registry-operations.md.
|
|
197
|
+
expect(manifest.integrity).toMatch(/^sha256-[A-Za-z0-9+/]+=*$/);
|
|
198
|
+
const expectedSri = `sha256-${createHash('sha256').update(tarball.bytes).digest('base64')}`;
|
|
199
|
+
expect(expectedSri, 'SRI integrity in manifest MUST match a fresh sha256 of the tarball').toBe(
|
|
200
|
+
manifest.integrity,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
// 6. Ed25519 verification. Two canonical signing conventions per
|
|
204
|
+
// `node-packs.md` §"Signing recipe": `method=ed25519` signs the
|
|
205
|
+
// whole tarball; `method=manual` signs the pack.json bytes inside
|
|
206
|
+
// the tarball. core.openwop.examples uses `ed25519`.
|
|
207
|
+
const method = manifest.signing!.method;
|
|
208
|
+
expect(['ed25519', 'manual']).toContain(method);
|
|
209
|
+
|
|
210
|
+
// For `ed25519` the signed bytes are the tarball; for `manual` the
|
|
211
|
+
// signed bytes are pack.json extracted from the tarball. This
|
|
212
|
+
// scenario picks `core.openwop.examples` specifically because it's
|
|
213
|
+
// `method=ed25519` — the simpler path. Extending to `manual` would
|
|
214
|
+
// require the tarball extractor from registry/scripts/verify-
|
|
215
|
+
// signatures.mjs which is intentionally out of scope here.
|
|
216
|
+
if (method !== 'ed25519') return;
|
|
217
|
+
|
|
218
|
+
const verified = cryptoVerify(null, tarball.bytes, publicKey, sig.bytes);
|
|
219
|
+
expect(verified, `Ed25519 signature over ${PACK_NAME}@${PACK_VERSION}.tgz MUST verify against ${manifest.signing!.keyId}`)
|
|
220
|
+
.toBe(true);
|
|
221
|
+
});
|
|
222
|
+
});
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* search-bm25-roundtrip — RFC 0018 advertisement-shape verification + behavioral placeholders.
|
|
3
|
+
*
|
|
4
|
+
* Status: ACTIVE (advertisement-shape). RFC 0018 promoted to `Active`
|
|
5
|
+
* 2026-05-17. The matching `capabilities.searchIndex` block has landed in
|
|
6
|
+
* `schemas/capabilities.schema.json`. This scenario asserts the advertisement
|
|
7
|
+
* shape against any host that boots the conformance suite, and keeps the
|
|
8
|
+
* deeper behavioral assertions as `it.todo()` until a reference host wires
|
|
9
|
+
* a test seam.
|
|
10
|
+
*
|
|
11
|
+
* Summary: index then query returns relevant documents.
|
|
12
|
+
*
|
|
13
|
+
* @see RFCS/0018-*.md
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { describe, it, expect } from 'vitest';
|
|
17
|
+
import { driver } from '../lib/driver.js';
|
|
18
|
+
|
|
19
|
+
interface DiscoveryDoc {
|
|
20
|
+
capabilities?: Record<string, unknown>;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
24
|
+
const res = await driver.get('/.well-known/openwop');
|
|
25
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
26
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
27
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["searchIndex"] : undefined;
|
|
28
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
describe('search-bm25-roundtrip: advertisement shape (RFC 0018)', () => {
|
|
32
|
+
it('capabilities.searchIndex is either absent or a well-formed object', async () => {
|
|
33
|
+
const cap = await readCap();
|
|
34
|
+
if (cap === null) return; // host doesn't advertise — skip
|
|
35
|
+
expect(
|
|
36
|
+
typeof cap.supported,
|
|
37
|
+
driver.describe(
|
|
38
|
+
'capabilities.schema.json §searchIndex',
|
|
39
|
+
'capabilities.searchIndex.supported MUST be a boolean when present',
|
|
40
|
+
),
|
|
41
|
+
).toBe('boolean');
|
|
42
|
+
});
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
describe('search-bm25-roundtrip: behavioral assertions (placeholders — need host test seam)', () => {
|
|
46
|
+
it.todo("index 3 docs → query returns relevance-ranked hits");
|
|
47
|
+
});
|
|
@@ -69,7 +69,23 @@ import {
|
|
|
69
69
|
// ── Helpers ─────────────────────────────────────────────────────────────
|
|
70
70
|
|
|
71
71
|
function listJsonFiles(dir: string): string[] {
|
|
72
|
-
|
|
72
|
+
// Recurse into subdirectories so e.g. `schemas/envelopes/*.schema.json`
|
|
73
|
+
// appears as `envelopes/<file>` to match the README's path-prefixed
|
|
74
|
+
// table entries. Preserves the non-recursive-relative-output contract
|
|
75
|
+
// for files directly under `dir`.
|
|
76
|
+
const out: string[] = [];
|
|
77
|
+
const walk = (subPath: string): void => {
|
|
78
|
+
const fullPath = subPath === '' ? dir : `${dir}/${subPath}`;
|
|
79
|
+
for (const entry of readdirSync(fullPath, { withFileTypes: true })) {
|
|
80
|
+
if (entry.isDirectory()) {
|
|
81
|
+
walk(subPath === '' ? entry.name : `${subPath}/${entry.name}`);
|
|
82
|
+
} else if (entry.isFile() && entry.name.endsWith('.json')) {
|
|
83
|
+
out.push(subPath === '' ? entry.name : `${subPath}/${entry.name}`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
walk('');
|
|
88
|
+
return out;
|
|
73
89
|
}
|
|
74
90
|
|
|
75
91
|
function listScenarioTestFiles(dir: string): string[] {
|
|
@@ -789,11 +805,14 @@ describe.skipIf(
|
|
|
789
805
|
() => {
|
|
790
806
|
// describe.skipIf still evaluates the body for test registration; defaults guard against null
|
|
791
807
|
// dirname() when sources are missing under the published-tarball layout. it() blocks below are
|
|
792
|
-
// skipped at run time, so the path values are never actually read.
|
|
808
|
+
// skipped at run time, so the path values are never actually read. The sentinel is
|
|
809
|
+
// intentionally an obviously-invalid path so a stack trace from any future code that DOES
|
|
810
|
+
// dereference it points the reader at this comment.
|
|
811
|
+
const UNUSED_IN_PUBLISHED_LAYOUT = '/__sdk_paths_unused_in_published_layout__';
|
|
793
812
|
const sdkSources = {
|
|
794
|
-
typescript: TYPESCRIPT_RUN_HELPERS_PATH ??
|
|
795
|
-
python: PYTHON_TYPES_PATH ??
|
|
796
|
-
go: GO_TYPES_PATH ??
|
|
813
|
+
typescript: TYPESCRIPT_RUN_HELPERS_PATH ?? UNUSED_IN_PUBLISHED_LAYOUT,
|
|
814
|
+
python: PYTHON_TYPES_PATH ?? UNUSED_IN_PUBLISHED_LAYOUT,
|
|
815
|
+
go: GO_TYPES_PATH ?? UNUSED_IN_PUBLISHED_LAYOUT,
|
|
797
816
|
};
|
|
798
817
|
const sdkReadmes = {
|
|
799
818
|
typescript: pathResolve(dirname(sdkSources.typescript), '..', 'README.md'),
|
|
@@ -1145,8 +1164,10 @@ describe.skipIf(README_PATH === null)('spec-corpus: local Markdown links resolve
|
|
|
1145
1164
|
|
|
1146
1165
|
const target = pathResolve(dirname(file), decoded);
|
|
1147
1166
|
// Published-tarball layout: the conformance README references ../spec/v1/... and other paths
|
|
1148
|
-
// that resolve OUTSIDE the package boundary. Repo layout has the full tree available.
|
|
1149
|
-
|
|
1167
|
+
// that resolve OUTSIDE the package boundary. Repo layout has the full tree available. The
|
|
1168
|
+
// `target === repoRoot || target.startsWith(repoRoot + sep)` form avoids a sibling-path
|
|
1169
|
+
// false-negative when repoRoot=/foo/bar and target=/foo/barbaz.
|
|
1170
|
+
if (LAYOUT === 'published' && target !== repoRoot && !target.startsWith(repoRoot + '/')) continue;
|
|
1150
1171
|
expect(
|
|
1151
1172
|
existsSync(target),
|
|
1152
1173
|
`${relFile} links to missing local target: ${link}`,
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sql-injection-rejection — RFC 0018 §C + SECURITY/invariants.yaml
|
|
3
|
+
* `sql-parametric-only`.
|
|
4
|
+
*
|
|
5
|
+
* Status: ACTIVE (advertisement + behavioral). The host's SQL surface
|
|
6
|
+
* MUST treat parameter values as literal data, not SQL fragments. We
|
|
7
|
+
* verify by binding an injection-shape string as a parameter and
|
|
8
|
+
* confirming it returns no rows (parametric binding turns it into a
|
|
9
|
+
* literal value comparison rather than an OR-true).
|
|
10
|
+
*
|
|
11
|
+
* @see RFCS/0018-host-sql-vector-search-capability.md
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { driver } from '../lib/driver.js';
|
|
16
|
+
|
|
17
|
+
interface DiscoveryDoc {
|
|
18
|
+
capabilities?: Record<string, unknown>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function readCap(): Promise<Record<string, unknown> | null> {
|
|
22
|
+
const res = await driver.get('/.well-known/openwop');
|
|
23
|
+
const body = res.json as DiscoveryDoc | undefined;
|
|
24
|
+
const top = body?.capabilities as Record<string, unknown> | undefined;
|
|
25
|
+
const final = (top && typeof top === 'object') ? (top as Record<string, unknown>)["sql"] : undefined;
|
|
26
|
+
return (final && typeof final === 'object' ? (final as Record<string, unknown>) : null);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async function call(op: string, args: Record<string, unknown>) {
|
|
30
|
+
return driver.post('/v1/host/sample/test/surface', { tenantId: 'tenant-a', surface: 'sql', op, args });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
describe('sql-injection-rejection: advertisement shape (RFC 0018)', () => {
|
|
34
|
+
it('capabilities.sql is either absent or a well-formed object', async () => {
|
|
35
|
+
const cap = await readCap();
|
|
36
|
+
if (cap === null) return;
|
|
37
|
+
expect(
|
|
38
|
+
typeof cap.supported,
|
|
39
|
+
driver.describe(
|
|
40
|
+
'capabilities.schema.json §sql',
|
|
41
|
+
'capabilities.sql.supported MUST be a boolean when present',
|
|
42
|
+
),
|
|
43
|
+
).toBe('boolean');
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
describe('sql-injection-rejection: behavioral (RFC 0018 §C)', () => {
|
|
48
|
+
it('parametric SELECT with bound user input rejects injection-shape strings as data', async () => {
|
|
49
|
+
const cap = await readCap();
|
|
50
|
+
if (!cap || cap.supported !== true) return;
|
|
51
|
+
|
|
52
|
+
const create = await call('execute', {
|
|
53
|
+
sql: `CREATE TABLE IF NOT EXISTS sql_inj_t (id TEXT PRIMARY KEY, body TEXT)`,
|
|
54
|
+
params: [],
|
|
55
|
+
});
|
|
56
|
+
if (create.status === 404) return;
|
|
57
|
+
await call('execute', { sql: `INSERT OR REPLACE INTO sql_inj_t VALUES (?, ?)`, params: ['k1', 'ok'] });
|
|
58
|
+
|
|
59
|
+
// Parametric round-trip MUST succeed.
|
|
60
|
+
const okRes = await call('query', {
|
|
61
|
+
sql: `SELECT body FROM sql_inj_t WHERE id = ?`,
|
|
62
|
+
params: ['k1'],
|
|
63
|
+
});
|
|
64
|
+
expect(okRes.status).toBe(200);
|
|
65
|
+
const okBody = okRes.json as { rows?: Array<Record<string, unknown>> };
|
|
66
|
+
expect(okBody.rows?.[0]?.body, 'parametric round-trip MUST return stored value').toBe('ok');
|
|
67
|
+
|
|
68
|
+
// Injection-shape input MUST be bound as a literal value, not SQL.
|
|
69
|
+
const attack = `' OR '1'='1`;
|
|
70
|
+
const attackRes = await call('query', {
|
|
71
|
+
sql: `SELECT body FROM sql_inj_t WHERE id = ?`,
|
|
72
|
+
params: [attack],
|
|
73
|
+
});
|
|
74
|
+
expect(attackRes.status).toBe(200);
|
|
75
|
+
const attackBody = attackRes.json as { rows?: Array<Record<string, unknown>> };
|
|
76
|
+
expect(
|
|
77
|
+
Array.isArray(attackBody.rows) ? attackBody.rows.length : -1,
|
|
78
|
+
driver.describe(
|
|
79
|
+
'SECURITY/invariants.yaml sql-parametric-only',
|
|
80
|
+
'parametric binding MUST treat injection-shape input as a literal value, not SQL',
|
|
81
|
+
),
|
|
82
|
+
).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
});
|