@openwop/openwop-conformance 1.106.1 → 1.124.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/README.md +3 -3
- package/coverage.md +6 -0
- package/dist/cli.js +89 -45
- package/dist/lib/certification-bundle-verify.js +274 -0
- package/dist/lib/profiles.js +102 -19
- package/dist/lib/requirement-ledger.js +182 -0
- package/dist/lib/requirement-registry.js +110 -0
- package/dist/lib/scenario-disposition.js +249 -0
- package/dist/lib/soft-skip.js +82 -0
- package/fixtures/conformance-a2a-task-roundtrip.json +1 -1
- package/fixtures.md +1 -1
- package/package.json +1 -1
- package/schemas/CORPUS-STAMP.json +2 -2
- package/schemas/README.md +1 -0
- package/schemas/capabilities.schema.json +2 -2
- package/schemas/compensation-policy.schema.json +64 -0
- package/schemas/run-snapshot.schema.json +5 -0
- package/schemas/workflow-chain-pack-manifest.schema.json +125 -0
- package/schemas/workflow-definition.schema.json +4 -0
- package/src/cli.ts +99 -45
- package/src/lib/a2a-fake-peer.ts +290 -110
- package/src/lib/certification-bundle-verify.ts +356 -0
- package/src/lib/error-envelope.ts +77 -0
- package/src/lib/llm-cache-key-recipe.ts +6 -5
- package/src/lib/mcp-fake-server.ts +235 -72
- package/src/lib/profiles.ts +132 -19
- package/src/lib/requirement-ledger.ts +98 -1
- package/src/lib/requirement-registry.ts +47 -4
- package/src/lib/scenario-disposition.ts +291 -0
- package/src/lib/soft-skip.ts +89 -0
- package/src/lib/workflow-chain-expansion.ts +189 -0
- package/src/scenarios/a2a-1-0-agent-card.test.ts +179 -0
- package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +109 -0
- package/src/scenarios/a2a-card-runtime-consistency.test.ts +167 -0
- package/src/scenarios/a2a-peer-authority.test.ts +99 -0
- package/src/scenarios/a2a-task-roundtrip.test.ts +18 -6
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -2
- package/src/scenarios/agent-loop-stateful-resume.test.ts +3 -2
- package/src/scenarios/agent-loop-version5-shape.test.ts +2 -1
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +4 -3
- package/src/scenarios/approval-gate-flow.test.ts +5 -4
- package/src/scenarios/artifact-type-store-without-render.test.ts +2 -1
- package/src/scenarios/auth-scim-profile.test.ts +5 -4
- package/src/scenarios/authorization-fail-closed.test.ts +4 -3
- package/src/scenarios/certification-bundle-non-vacuous.test.ts +249 -0
- package/src/scenarios/certification-bundle-redaction.test.ts +182 -0
- package/src/scenarios/certification-floor-enforcement.test.ts +30 -6
- package/src/scenarios/chain-compensation-expansion.test.ts +243 -0
- package/src/scenarios/commitment-fired.test.ts +3 -2
- package/src/scenarios/compensation-behavior.test.ts +63 -0
- package/src/scenarios/compensation-profile.test.ts +144 -1
- package/src/scenarios/conformance-advertised-seam-required.test.ts +118 -0
- package/src/scenarios/conformance-execution-witness.test.ts +104 -0
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +3 -2
- package/src/scenarios/core-manifest-and-extension-registry.test.ts +26 -0
- package/src/scenarios/credential-payload-redaction.test.ts +4 -3
- package/src/scenarios/credentials-capability-shape.test.ts +4 -3
- package/src/scenarios/cross-engine-append-behavior.test.ts +5 -4
- package/src/scenarios/cross-engine-append-ordering.test.ts +3 -2
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +5 -4
- package/src/scenarios/cross-host-causation-shape.test.ts +3 -2
- package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
- package/src/scenarios/data-residency-admission.test.ts +6 -6
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +5 -4
- package/src/scenarios/distillation-index-roundtrip.test.ts +3 -2
- package/src/scenarios/distillation-secret-carryforward.test.ts +3 -2
- package/src/scenarios/distillation-shape.test.ts +2 -1
- package/src/scenarios/distillation-stable-archive.test.ts +4 -3
- package/src/scenarios/distillation-token-budget.test.ts +4 -3
- package/src/scenarios/envelope-recovery-applied.test.ts +3 -2
- package/src/scenarios/envelope-refusal-shape.test.ts +4 -3
- package/src/scenarios/error-envelope-canonical-shape.test.ts +64 -0
- package/src/scenarios/feedback-correction-redaction.test.ts +4 -3
- package/src/scenarios/feedback-cross-tenant-isolation.test.ts +4 -3
- package/src/scenarios/feedback-fork-not-copied.test.ts +6 -5
- package/src/scenarios/feedback-on-terminal-run.test.ts +4 -3
- package/src/scenarios/feedback-record-and-list.test.ts +4 -3
- package/src/scenarios/feedback-unsupported-501.test.ts +3 -2
- package/src/scenarios/fixtures-valid.test.ts +16 -18
- package/src/scenarios/fs-path-traversal.test.ts +3 -2
- package/src/scenarios/heartbeat-capability-shape.test.ts +2 -1
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +4 -3
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +6 -5
- package/src/scenarios/heartbeat-runtime-bound.test.ts +4 -3
- package/src/scenarios/mcp-2026-07-28-discover.test.ts +207 -0
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +66 -0
- package/src/scenarios/mcp-current-auth-boundary.test.ts +55 -0
- package/src/scenarios/mcp-extension-opacity.test.ts +79 -0
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +140 -0
- package/src/scenarios/mcp-stateless-request.test.ts +77 -0
- package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
- package/src/scenarios/mcp-toolcall-redaction.test.ts +2 -1
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +6 -5
- package/src/scenarios/memory-attribution-no-content.test.ts +4 -3
- package/src/scenarios/memory-attribution-replay-stable.test.ts +6 -5
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +4 -3
- package/src/scenarios/memory-compaction-event-emitted.test.ts +3 -2
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +6 -5
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +4 -3
- package/src/scenarios/memory-consolidation-idempotent.test.ts +4 -3
- package/src/scenarios/memory-injection-budget.test.ts +4 -3
- package/src/scenarios/metric-emission.test.ts +4 -3
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +5 -4
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +4 -3
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +5 -4
- package/src/scenarios/multi-party-conversation-behavioral.test.ts +3 -2
- package/src/scenarios/multi-region-idempotency-behavior.test.ts +7 -6
- package/src/scenarios/multi-region-idempotency.test.ts +4 -3
- package/src/scenarios/normative-example-extraction.test.ts +242 -0
- package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +4 -3
- package/src/scenarios/oauth-capability-shape.test.ts +4 -3
- package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
- package/src/scenarios/openapi-asyncapi-sdk-parity.test.ts +309 -0
- package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -1
- package/src/scenarios/otel-emission-grpc.test.ts +4 -3
- package/src/scenarios/otel-emission.test.ts +4 -3
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +5 -4
- package/src/scenarios/otel-trace-propagation.test.ts +4 -3
- package/src/scenarios/pack-registry-isolation.test.ts +4 -3
- package/src/scenarios/pack-registry-publish.test.ts +65 -3
- package/src/scenarios/pack-registry.test.ts +22 -0
- package/src/scenarios/profile-discovery-core-alias.test.ts +137 -0
- package/src/scenarios/profileDerivation.test.ts +2 -1
- package/src/scenarios/provider-usage.test.ts +2 -2
- package/src/scenarios/rate-limit-envelope.test.ts +2 -1
- package/src/scenarios/registry-public.test.ts +6 -5
- package/src/scenarios/replay-divergence-at-refusal.test.ts +9 -8
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +19 -13
- package/src/scenarios/replay-llm-cache-key.test.ts +120 -47
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +5 -4
- package/src/scenarios/replay-side-effect-suppression.test.ts +20 -1
- package/src/scenarios/requirement-ledger.test.ts +29 -6
- package/src/scenarios/runner-ledger.test.ts +279 -0
- package/src/scenarios/runtime-requires-install-gate.test.ts +6 -5
- package/src/scenarios/sandbox-memory-cap.test.ts +3 -2
- package/src/scenarios/sandbox-mvp-behavior.test.ts +11 -10
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +2 -1
- package/src/scenarios/sandbox-timeout-cap.test.ts +3 -2
- package/src/scenarios/stream-text-fixture.test.ts +5 -4
- package/src/scenarios/subrun-approval-fail-closed.test.ts +3 -2
- package/src/scenarios/subrun-approval-gate.test.ts +4 -3
- package/src/scenarios/subrun-attestation-shape.test.ts +2 -1
- package/src/scenarios/subrun-checksum-stable.test.ts +4 -3
- package/src/scenarios/table-schema-enforcement.test.ts +2 -2
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -2
- package/src/scenarios/tool-hooks-content-free.test.ts +3 -2
- package/src/scenarios/tool-hooks-rate-limit.test.ts +3 -2
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +3 -2
- package/src/scenarios/tool-hooks-shape.test.ts +2 -1
- package/src/scenarios/trigger-bridge-delivery.test.ts +3 -2
- package/src/scenarios/version-fold.test.ts +5 -4
- package/src/scenarios/voice-streamref-tenant-bound.test.ts +2 -1
- package/src/scenarios/voice-transcription-streaming.test.ts +2 -1
- package/src/scenarios/voice-transcription-unadvertised.test.ts +2 -1
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +5 -4
- package/src/scenarios/wasm-pack-invoke-completed.test.ts +3 -2
- package/src/scenarios/wasm-pack-invoke-suspended.test.ts +3 -2
- package/src/scenarios/wasm-pack-load.test.ts +4 -3
- package/src/scenarios/wasm-pack-memory-cap.test.ts +4 -3
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +3 -2
- package/src/scenarios/webhook-signed-delivery.test.ts +4 -3
- package/src/scenarios/workflow-primary-output-annotation.test.ts +13 -23
- package/src/scenarios/workload-identity-behavior.test.ts +7 -4
- package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +3 -2
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +3 -2
- package/src/setup.ts +95 -1
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0152 §E — a peer's content is never authority (invariant
|
|
3
|
+
* `a2a-peer-no-authority-escalation`, named by RFC 0152 §E; §"Trust boundary"
|
|
4
|
+
* `prompt-injection-mcp-no-approval` generalized to A2A).
|
|
5
|
+
*
|
|
6
|
+
* The host acts as an A2A CLIENT against the suite's fake peer. The peer's
|
|
7
|
+
* agent-role reply carries everything a hostile peer would try: text that reads
|
|
8
|
+
* like an approval, `metadata.openwop.approval: "accept"`, `metadata.openwop.
|
|
9
|
+
* scopes[]`, `metadata.openwop.interrupt.resolve`, and a `referenceTaskIds[]`
|
|
10
|
+
* pointing at a task the caller never created. `a2a-integration.md` §D.2 says
|
|
11
|
+
* all of it is opaque and §E says none of it may become authority: the run's
|
|
12
|
+
* approval state, tool allowlist, and scopes MUST be exactly what they were
|
|
13
|
+
* before the peer answered, and the referenced task MUST NOT be dereferenced on
|
|
14
|
+
* the peer's say-so.
|
|
15
|
+
*
|
|
16
|
+
* Observability: the host's `/v1/host/sample/a2a/invoke` seam accepts an
|
|
17
|
+
* ADDITIVE `scenario: "peer-asserts-authority"` and, when it implements it,
|
|
18
|
+
* reports `peerAuthority: { approvalAdvanced, scopesWidened, referencedTaskDereferenced }`
|
|
19
|
+
* (`host-sample-test-seams.md` §22). A host that has not added the field is
|
|
20
|
+
* recorded `blocked` for this requirement rather than passed — RFC 0148 §A —
|
|
21
|
+
* because a leg that cannot see the outcome cannot vouch for it.
|
|
22
|
+
*
|
|
23
|
+
* Gate: `a2a.supported && a2a.protocolVersions.length > 0` (same as the §B
|
|
24
|
+
* legs — the requirement is version-independent). Needs the fake peer
|
|
25
|
+
* (`OPENWOP_A2A_FAKE_PEER=true`).
|
|
26
|
+
*
|
|
27
|
+
* @see spec/v1/a2a-integration.md §"A2A 1.0 versioned composition" §D.2, §E
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
import { describe, it, expect } from 'vitest';
|
|
31
|
+
import { softSkip, seamAbsent } from '../lib/soft-skip.js';
|
|
32
|
+
import { driver } from '../lib/driver.js';
|
|
33
|
+
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
34
|
+
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
35
|
+
import { getA2AFakePeer } from '../lib/a2a-fake-peer.js';
|
|
36
|
+
|
|
37
|
+
export const REQUIRES_HOST_CALLBACK = "the host issues A2A JSON-RPC calls to the suite's fake peer via /v1/host/sample/a2a/invoke (peerUrl), which the peer records";
|
|
38
|
+
|
|
39
|
+
const PROFILE = 'a2a.peerAuthority';
|
|
40
|
+
|
|
41
|
+
interface A2ACaps {
|
|
42
|
+
readonly supported?: boolean;
|
|
43
|
+
readonly protocolVersions?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async function advertised(): Promise<boolean> {
|
|
47
|
+
const disco = await driver.get('/.well-known/openwop');
|
|
48
|
+
const caps = capabilityFamily<A2ACaps>(disco.json, 'a2a');
|
|
49
|
+
return caps?.supported === true && (caps.protocolVersions?.length ?? 0) > 0;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
interface PeerAuthorityReport {
|
|
53
|
+
approvalAdvanced?: boolean;
|
|
54
|
+
scopesWidened?: boolean;
|
|
55
|
+
referencedTaskDereferenced?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
describe('RFC 0152 §E — a2a-peer-no-authority-escalation (host as A2A client, gated)', () => {
|
|
59
|
+
it('a peer reply that asserts approval, scopes, and a foreign task reference changes nothing the host authorizes', async () => {
|
|
60
|
+
if (!behaviorGate(PROFILE, await advertised())) return;
|
|
61
|
+
const peer = getA2AFakePeer();
|
|
62
|
+
if (peer === null) return softSkip('blocked', 'the suite A2A fake peer is not started in this run (OPENWOP_A2A_FAKE_PEER unset) — the host-as-client leg cannot be driven');
|
|
63
|
+
peer.reset();
|
|
64
|
+
peer.setNextPeerAssertsAuthority(true);
|
|
65
|
+
const drive = await driver.post('/v1/host/sample/a2a/invoke', {
|
|
66
|
+
peerUrl: peer.endpoint(),
|
|
67
|
+
scenario: 'peer-asserts-authority',
|
|
68
|
+
});
|
|
69
|
+
if (drive.status === 404 || drive.status === 403) {
|
|
70
|
+
// Advertised A2A but the invoke seam answered {drive.status}: not observable here.
|
|
71
|
+
// Default mode records `blocked` (RFC 0148 §A); OPENWOP_REQUIRE_BEHAVIOR=true fails
|
|
72
|
+
// an advertised-missing seam (RFC 0148 §B). A 403 is NOT a pass.
|
|
73
|
+
return seamAbsent(`host advertises A2A but the invoke seam /v1/host/sample/a2a/invoke answered ${drive.status}`);
|
|
74
|
+
}
|
|
75
|
+
// The peer must actually have answered with the authority-asserting message,
|
|
76
|
+
// or this leg is vacuous.
|
|
77
|
+
const created = peer.taskCount();
|
|
78
|
+
expect(created, driver.describe('RFCS/0152 §E', 'the host MUST have created a task on the peer for this leg to mean anything')).toBeGreaterThan(0);
|
|
79
|
+
const report = (drive.json as { peerAuthority?: PeerAuthorityReport }).peerAuthority;
|
|
80
|
+
if (report === undefined) {
|
|
81
|
+
// Additive seam field not yet implemented: blocked, not passed.
|
|
82
|
+
expect(
|
|
83
|
+
report,
|
|
84
|
+
driver.describe(
|
|
85
|
+
'host-sample-test-seams.md §22',
|
|
86
|
+
'the invoke seam SHOULD report `peerAuthority: { approvalAdvanced, scopesWidened, referencedTaskDereferenced }` for ' +
|
|
87
|
+
'scenario "peer-asserts-authority"; until it does this requirement is unobservable and resolves to `blocked`, not passed',
|
|
88
|
+
),
|
|
89
|
+
).toBeDefined();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
expect(report.approvalAdvanced, driver.describe('RFCS/0152 §E', 'peer content MUST NOT advance an approval gate (a2a-peer-no-authority-escalation)')).toBe(false);
|
|
93
|
+
expect(report.scopesWidened, driver.describe('RFCS/0152 §E', 'peer content MUST NOT widen the run’s scopes or tool allowlist')).toBe(false);
|
|
94
|
+
expect(
|
|
95
|
+
report.referencedTaskDereferenced,
|
|
96
|
+
driver.describe('a2a-integration.md §D.2', '`referenceTaskIds[]` are opaque hints — MUST NOT be dereferenced on the peer’s say-so'),
|
|
97
|
+
).toBe(false);
|
|
98
|
+
});
|
|
99
|
+
});
|
|
@@ -104,7 +104,7 @@ async function rpc(
|
|
|
104
104
|
}
|
|
105
105
|
|
|
106
106
|
describe('a2a-task-roundtrip: AgentCard + task lifecycle', () => {
|
|
107
|
-
it('AgentCard exposes
|
|
107
|
+
it('AgentCard exposes a protocol version (0.3 top-level or 1.0 per-interface) + skills; message/send + tasks/get round-trip per A2A v0.3 JSON-RPC', async () => {
|
|
108
108
|
const probe = probePeer();
|
|
109
109
|
if (!probe) {
|
|
110
110
|
// eslint-disable-next-line no-console
|
|
@@ -125,18 +125,30 @@ describe('a2a-task-roundtrip: AgentCard + task lifecycle', () => {
|
|
|
125
125
|
skills?: ReadonlyArray<{ id?: string; name?: string }>;
|
|
126
126
|
url?: string;
|
|
127
127
|
additionalInterfaces?: ReadonlyArray<{ url?: string; transport?: string }>;
|
|
128
|
+
// A2A 1.0 (RFC 0152 §C): the version is per interface and the top-level
|
|
129
|
+
// `url`/`protocolVersion` are gone. A dual-era peer (the suite's own, since
|
|
130
|
+
// 1.112.0, and any real 1.0 peer) serves THIS shape while still speaking
|
|
131
|
+
// 0.3 to a header-less request — which is what this leg exercises.
|
|
132
|
+
supportedInterfaces?: ReadonlyArray<{ url?: string; protocolBinding?: string; protocolVersion?: string }>;
|
|
128
133
|
};
|
|
129
|
-
|
|
134
|
+
const advertisedVersion =
|
|
135
|
+
cardJson.protocolVersion ??
|
|
136
|
+
cardJson.supportedInterfaces?.find((i) => typeof i.protocolVersion === 'string')?.protocolVersion;
|
|
137
|
+
expect(
|
|
138
|
+
typeof advertisedVersion,
|
|
139
|
+
'the card MUST advertise a protocol version — top-level (0.3) or per interface (1.0)',
|
|
140
|
+
).toBe('string');
|
|
130
141
|
expect(Array.isArray(cardJson.skills)).toBe(true);
|
|
131
142
|
expect((cardJson.skills ?? []).length).toBeGreaterThan(0);
|
|
132
143
|
|
|
133
|
-
// Find the JSON-RPC transport endpoint
|
|
134
|
-
//
|
|
135
|
-
// JSONRPC
|
|
144
|
+
// Find the JSON-RPC transport endpoint: 1.0 `supportedInterfaces[]`
|
|
145
|
+
// (`protocolBinding: "JSONRPC"`), else 0.3 `additionalInterfaces[]`
|
|
146
|
+
// (`transport: "JSONRPC"`), else 0.3 `card.url`.
|
|
147
|
+
const jsonrpc10 = (cardJson.supportedInterfaces ?? []).find((i) => i.protocolBinding === 'JSONRPC');
|
|
136
148
|
const jsonrpcIface = (cardJson.additionalInterfaces ?? []).find(
|
|
137
149
|
(i) => i.transport === 'JSONRPC',
|
|
138
150
|
);
|
|
139
|
-
const rpcUrl = jsonrpcIface?.url ?? cardJson.url ?? `${probe.url}/a2a/jsonrpc`;
|
|
151
|
+
const rpcUrl = jsonrpc10?.url ?? jsonrpcIface?.url ?? cardJson.url ?? `${probe.url}/a2a/jsonrpc`;
|
|
140
152
|
expect(typeof rpcUrl).toBe('string');
|
|
141
153
|
|
|
142
154
|
if (probe.isReal) {
|
|
@@ -10,14 +10,15 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
13
14
|
import { driver } from '../lib/driver.js';
|
|
14
15
|
import { readExecutionModelCap, isVersion5, invokeAgentLoop } from '../lib/agentLoop.js';
|
|
15
16
|
|
|
16
17
|
describe('agent-loop-iteration-monotonic (RFC 0061 §B)', () => {
|
|
17
18
|
it('iteration increments by exactly 1 per orchestrator turn, 1-based', async () => {
|
|
18
|
-
if (!isVersion5(await readExecutionModelCap())) return;
|
|
19
|
+
if (!isVersion5(await readExecutionModelCap())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isVersion5(await readExecutionModelCap())` returned early');
|
|
19
20
|
const res = await invokeAgentLoop({ turns: 3 });
|
|
20
|
-
if (res === null) return
|
|
21
|
+
if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
21
22
|
const decisions = res.decisions ?? [];
|
|
22
23
|
expect(
|
|
23
24
|
decisions.length >= 1,
|
|
@@ -10,16 +10,17 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import { describe, it, expect } from 'vitest';
|
|
13
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
13
14
|
import { driver } from '../lib/driver.js';
|
|
14
15
|
import { readExecutionModelCap, invokeAgentLoop } from '../lib/agentLoop.js';
|
|
15
16
|
|
|
16
17
|
describe('agent-loop-stateful-resume (RFC 0061 §D)', () => {
|
|
17
18
|
it('a mid-loop suspend resumes at the same iteration, counter intact', async () => {
|
|
18
19
|
const em = await readExecutionModelCap();
|
|
19
|
-
if (em?.statefulResume !== true) return;
|
|
20
|
+
if (em?.statefulResume !== true) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `em?.statefulResume !== true` returned early');
|
|
20
21
|
// Suspend at turn 2, then resume: the resumed iteration MUST be 2, not 1 or 3.
|
|
21
22
|
const res = await invokeAgentLoop({ turns: 4, suspendAtTurn: 2, resume: true });
|
|
22
|
-
if (res === null) return
|
|
23
|
+
if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
23
24
|
expect(
|
|
24
25
|
res.resumedIteration,
|
|
25
26
|
driver.describe('RFC 0061 §D', 'a stateful resume MUST continue at the suspend iteration — the counter does not reset or skip'),
|
|
@@ -12,13 +12,14 @@
|
|
|
12
12
|
*/
|
|
13
13
|
|
|
14
14
|
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
15
16
|
import { driver } from '../lib/driver.js';
|
|
16
17
|
import { readExecutionModelCap } from '../lib/agentLoop.js';
|
|
17
18
|
|
|
18
19
|
describe('agent-loop-version5-shape: advertisement (RFC 0061 §A)', () => {
|
|
19
20
|
it('executionModel.statefulResume/transcriptWindow are well-formed when present', async () => {
|
|
20
21
|
const em = await readExecutionModelCap();
|
|
21
|
-
if (em === null) return
|
|
22
|
+
if (em === null) return softSkip('inapplicable', 'no execution model — valid (em === null)');
|
|
22
23
|
if (em.statefulResume !== undefined) {
|
|
23
24
|
expect(
|
|
24
25
|
typeof em.statefulResume,
|
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { describe, it, expect } from 'vitest';
|
|
14
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
14
15
|
import { driver } from '../lib/driver.js';
|
|
15
16
|
import { readExecutionModelCap, isVersion5, hasWorkspace, invokeAgentLoop } from '../lib/agentLoop.js';
|
|
16
17
|
|
|
17
18
|
describe('agent-loop-workspace-snapshot (RFC 0061 §C)', () => {
|
|
18
19
|
it('a turn-i workspace write is invisible to turn i, visible to turn i+1', async () => {
|
|
19
|
-
if (!isVersion5(await readExecutionModelCap())) return;
|
|
20
|
-
if (!(await hasWorkspace())) return
|
|
20
|
+
if (!isVersion5(await readExecutionModelCap())) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!isVersion5(await readExecutionModelCap())` returned early');
|
|
21
|
+
if (!(await hasWorkspace())) return softSkip('inapplicable', 'workspace optional — soft-skip (!(await hasWorkspace()))');
|
|
21
22
|
const res = await invokeAgentLoop({ turns: 2, workspaceWriteAtTurn: 1 });
|
|
22
|
-
if (res === null) return
|
|
23
|
+
if (res === null) return softSkip('blocked', 'seam absent — soft-skip');
|
|
23
24
|
const vis = res.workspaceVisible ?? {};
|
|
24
25
|
expect(
|
|
25
26
|
vis.atWriteTurn,
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
*/
|
|
22
22
|
|
|
23
23
|
import { describe, it, expect } from 'vitest';
|
|
24
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
24
25
|
import { driver } from '../lib/driver.js';
|
|
25
26
|
import { readCapabilityFamily } from '../lib/discovery-capabilities.js';
|
|
26
27
|
|
|
@@ -32,12 +33,12 @@ async function authorizationSupported(): Promise<boolean> {
|
|
|
32
33
|
|
|
33
34
|
describe('approval-gate-flow: role-gated, audited approval (RFC 0051 §A)', () => {
|
|
34
35
|
it('an unauthorized principal does NOT release the gate (fail-closed)', async () => {
|
|
35
|
-
if (!(await authorizationSupported())) return
|
|
36
|
+
if (!(await authorizationSupported())) return softSkip('inapplicable', 'capability-gated');
|
|
36
37
|
const res = await driver.post('/v1/host/sample/governance/approval-gate', {
|
|
37
38
|
scenario: 'unauthorized-grant',
|
|
38
39
|
principal: 'conformance-unauthorized-principal',
|
|
39
40
|
});
|
|
40
|
-
if (res.status === 404) return
|
|
41
|
+
if (res.status === 404) return softSkip('blocked', 'seam unwired — soft-skip');
|
|
41
42
|
const body = res.json as { released?: boolean } | undefined;
|
|
42
43
|
expect(
|
|
43
44
|
body?.released,
|
|
@@ -46,13 +47,13 @@ describe('approval-gate-flow: role-gated, audited approval (RFC 0051 §A)', () =
|
|
|
46
47
|
});
|
|
47
48
|
|
|
48
49
|
it('the override path emits an audited approval.overridden with a reason', async () => {
|
|
49
|
-
if (!(await authorizationSupported())) return
|
|
50
|
+
if (!(await authorizationSupported())) return softSkip('inapplicable', 'capability-gated');
|
|
50
51
|
const res = await driver.post('/v1/host/sample/governance/approval-gate', {
|
|
51
52
|
scenario: 'override',
|
|
52
53
|
principal: 'conformance-owner-principal',
|
|
53
54
|
reason: 'conformance emergency publish',
|
|
54
55
|
});
|
|
55
|
-
if (res.status === 404) return
|
|
56
|
+
if (res.status === 404) return softSkip('blocked', 'seam unwired — soft-skip');
|
|
56
57
|
const body = res.json as { event?: { type?: string; payload?: { reason?: string } } } | undefined;
|
|
57
58
|
expect(
|
|
58
59
|
body?.event?.type,
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
26
|
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
27
28
|
import { driver } from '../lib/driver.js';
|
|
28
29
|
import { behaviorGate, behaviorGatePresent } from '../lib/behavior-gate.js';
|
|
29
30
|
|
|
@@ -43,7 +44,7 @@ describe('artifact-type-store-without-render: store-only hosts must not fail the
|
|
|
43
44
|
// Only meaningful for a host that stores but does NOT render.
|
|
44
45
|
// NOT a behaviorGate: this is a SHAPE precondition, not advertise-and-skip. A host that
|
|
45
46
|
// renders is not failing to implement anything — this scenario simply does not apply to it.
|
|
46
|
-
if (cap?.['store'] !== true || cap?.['render'] !== false) return
|
|
47
|
+
if (cap?.['store'] !== true || cap?.['render'] !== false) return softSkip('inapplicable', 'host does not advertise artifactTypes { store: true, render: false } — the store-without-render shape is not this host\'s');
|
|
47
48
|
|
|
48
49
|
const { artifactTypeId, manifest, schema } = sampleArtifactTypePack();
|
|
49
50
|
if (!behaviorGate(PROFILE, (await installArtifactTypePack(manifest, { [artifactTypeId]: schema })) !== null)) return;
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
*/
|
|
17
17
|
|
|
18
18
|
import { describe, it, expect } from 'vitest';
|
|
19
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
19
20
|
import { driver } from '../lib/driver.js';
|
|
20
21
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
21
22
|
|
|
@@ -39,7 +40,7 @@ async function readProfiles(): Promise<string[] | null> {
|
|
|
39
40
|
describe('auth-scim-profile: advertisement shape (RFC 0050)', () => {
|
|
40
41
|
it('claims openwop-auth-scim as a well-formed profile id when advertised', async () => {
|
|
41
42
|
const profiles = await readProfiles();
|
|
42
|
-
if (profiles === null || !profiles.includes(SCIM_PROFILE)) return
|
|
43
|
+
if (profiles === null || !profiles.includes(SCIM_PROFILE)) return softSkip('inapplicable', 'profile not claimed');
|
|
43
44
|
expect(
|
|
44
45
|
profiles.includes(SCIM_PROFILE),
|
|
45
46
|
driver.describe('RFC 0050 §B', 'openwop-auth-scim MUST appear verbatim in capabilities.auth.profiles when claimed'),
|
|
@@ -52,12 +53,12 @@ describe('auth-scim-profile: provisioning roundtrip (RFC 0050 §B — opt-in)',
|
|
|
52
53
|
|
|
53
54
|
it('provisions a SCIM user → principal (SCIM endpoint required)', async () => {
|
|
54
55
|
const profiles = await readProfiles();
|
|
55
|
-
if (profiles === null || !profiles.includes(SCIM_PROFILE)) return
|
|
56
|
-
if (scimUrl === undefined || scimUrl.length === 0) return
|
|
56
|
+
if (profiles === null || !profiles.includes(SCIM_PROFILE)) return softSkip('inapplicable', 'capability-gated');
|
|
57
|
+
if (scimUrl === undefined || scimUrl.length === 0) return softSkip('inapplicable', 'opt-in: SCIM endpoint not provided');
|
|
57
58
|
// A SCIM POST /Users against the operator-supplied endpoint MUST upsert a
|
|
58
59
|
// principal the host can subsequently resolve.
|
|
59
60
|
const res = await driver.post('/v1/host/sample/auth/scim/provision', { scimUrl, op: 'create-user' });
|
|
60
|
-
if (res.status === 404) return
|
|
61
|
+
if (res.status === 404) return softSkip('blocked', 'seam unwired');
|
|
61
62
|
expect(
|
|
62
63
|
res.status,
|
|
63
64
|
driver.describe('RFC 0050 §B', 'a SCIM user provisioning MUST succeed (2xx) and upsert an RFC 0048 principal'),
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
*/
|
|
24
24
|
|
|
25
25
|
import { describe, it, expect } from 'vitest';
|
|
26
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
26
27
|
import { driver } from '../lib/driver.js';
|
|
27
28
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
28
29
|
|
|
@@ -46,7 +47,7 @@ async function readAuthorization(): Promise<DiscoveryAuthorization | null> {
|
|
|
46
47
|
describe('authorization-fail-closed: advertisement shape (RFC 0049 §C)', () => {
|
|
47
48
|
it('failClosed is exactly true when authorization is supported', async () => {
|
|
48
49
|
const authz = await readAuthorization();
|
|
49
|
-
if (!authz?.supported || authz.failClosed === undefined) return;
|
|
50
|
+
if (!authz?.supported || authz.failClosed === undefined) return softSkip('inapplicable', 'capability or profile not advertised by this host — gate `!authz?.supported || authz.failClosed === undefined` returned early');
|
|
50
51
|
expect(
|
|
51
52
|
authz.failClosed,
|
|
52
53
|
driver.describe('RFC 0049 §C', 'capabilities.authorization.failClosed MUST be `true`'),
|
|
@@ -57,7 +58,7 @@ describe('authorization-fail-closed: advertisement shape (RFC 0049 §C)', () =>
|
|
|
57
58
|
describe('authorization-fail-closed: absent/unseeded role MUST deny (RFC 0049 §C)', () => {
|
|
58
59
|
it('a decision for an unseeded-role principal resolves allowed=false', async () => {
|
|
59
60
|
const authz = await readAuthorization();
|
|
60
|
-
if (!authz?.supported) return
|
|
61
|
+
if (!authz?.supported) return softSkip('inapplicable', 'capability-gated');
|
|
61
62
|
|
|
62
63
|
// Seam contract: request an authorization decision for a principal whose
|
|
63
64
|
// role is absent/unseeded. The host MUST fail closed.
|
|
@@ -67,7 +68,7 @@ describe('authorization-fail-closed: absent/unseeded role MUST deny (RFC 0049 §
|
|
|
67
68
|
resource: 'run-conformance-probe',
|
|
68
69
|
});
|
|
69
70
|
// 404 from a host that hasn't wired the seam is a soft-skip.
|
|
70
|
-
if (res.status === 404) return;
|
|
71
|
+
if (res.status === 404) return softSkip('blocked', 'precondition not met — `res.status === 404` returned early (seam, prior step, or fixture unavailable)');
|
|
71
72
|
|
|
72
73
|
const decision = res.json as { allowed?: boolean } | undefined;
|
|
73
74
|
expect(
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0148 §A/§C — `certification-bundle-non-vacuous`: a bundle with any
|
|
3
|
+
* unwitnessed required assertion is rejected (RFC 0147 program scenario;
|
|
4
|
+
* RFC 0148 "missing witness and blocked floor reject").
|
|
5
|
+
*
|
|
6
|
+
* This is the CONSUMER half of what `--certify` enforces on emit (S6). A
|
|
7
|
+
* bundle is a claim; a consumer re-derives it from the rows (RFC 0089 §B) and
|
|
8
|
+
* the rows must be witnesses, not lists. `verifyBundleV2` is that
|
|
9
|
+
* re-derivation, and these are its vectors — the shapes RFC 0148 names as
|
|
10
|
+
* fixtures: valid-v2, early-return, missing-floor, duplicate-requirement,
|
|
11
|
+
* tampered-witness — built inline against the LIVE `openwop-core-standard`
|
|
12
|
+
* floor so a floor edit re-runs them against the real requirement ids.
|
|
13
|
+
*
|
|
14
|
+
* Two verdicts are kept apart on purpose:
|
|
15
|
+
* - `evidenceValid: false` — the document is not acceptable evidence
|
|
16
|
+
* (unwitnessed / vacuous / duplicate / tampered / canary);
|
|
17
|
+
* - `certified: false` with valid evidence — an honest `blocked` or
|
|
18
|
+
* `executed-fail` on a required row. That is the host's state, not the
|
|
19
|
+
* bundle's defect, and RFC 0148 §A says so.
|
|
20
|
+
*
|
|
21
|
+
* Server-free, always-on; MUST NOT capability-skip (RFC 0148 §Conformance).
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { readFileSync } from 'node:fs';
|
|
26
|
+
import { join } from 'node:path';
|
|
27
|
+
import Ajv2020 from 'ajv/dist/2020.js';
|
|
28
|
+
import addFormats from 'ajv-formats';
|
|
29
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
30
|
+
import { PROFILE_FLOOR_SCENARIOS } from '../lib/profiles.js';
|
|
31
|
+
import { requirementIdForScenario, requirementIdForPrefix } from '../lib/requirement-registry.js';
|
|
32
|
+
import {
|
|
33
|
+
verifyBundleV2,
|
|
34
|
+
type BundleV2Like,
|
|
35
|
+
type BundleV2Requirement,
|
|
36
|
+
} from '../lib/certification-bundle-verify.js';
|
|
37
|
+
import { UNCLASSIFIED_RETURN_DETAIL } from '../lib/soft-skip.js';
|
|
38
|
+
|
|
39
|
+
export const HOST_CALLBACK_NOT_REQUIRED = 'server-free: verifies bundle documents built in-process; no host is contacted';
|
|
40
|
+
|
|
41
|
+
const HEX = 'a'.repeat(64);
|
|
42
|
+
const PROFILE = 'openwop-core-standard';
|
|
43
|
+
|
|
44
|
+
/** A discovery document from which `openwop-core-standard` derives (§B predicate). */
|
|
45
|
+
const CORE_STANDARD_DISCOVERY = {
|
|
46
|
+
protocolVersion: '1.0',
|
|
47
|
+
supportedEnvelopes: ['clarification.request'],
|
|
48
|
+
supportedTransports: ['rest'],
|
|
49
|
+
schemaVersions: { workflow: '1.0' },
|
|
50
|
+
limits: { clarificationRounds: 3, schemaRounds: 3, envelopesPerTurn: 8 },
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
function floor(): { files: string[]; prefixes: string[] } {
|
|
54
|
+
const f = PROFILE_FLOOR_SCENARIOS[PROFILE];
|
|
55
|
+
if (f === undefined) throw new Error(`${PROFILE} has no floor — this scenario is bound to the live floor`);
|
|
56
|
+
return { files: [...f.required], prefixes: [...(f.requiredAnyPrefix ?? [])] };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function witnessedRows(): BundleV2Requirement[] {
|
|
60
|
+
const { files, prefixes } = floor();
|
|
61
|
+
const rows: BundleV2Requirement[] = files.map((f) => ({
|
|
62
|
+
requirementId: requirementIdForScenario(f),
|
|
63
|
+
scenarioId: f,
|
|
64
|
+
disposition: 'executed-pass',
|
|
65
|
+
assertionCount: 7,
|
|
66
|
+
}));
|
|
67
|
+
for (const p of prefixes) {
|
|
68
|
+
// one matching scenario row + the emitter's summary row
|
|
69
|
+
rows.push({ requirementId: `openwop.scenario.${p}alpha`, scenarioId: `${p}alpha.test.ts`, disposition: 'executed-pass', assertionCount: 3 });
|
|
70
|
+
rows.push({ requirementId: requirementIdForPrefix(p), scenarioId: `${p}*`, disposition: 'executed-pass', assertionCount: 3 });
|
|
71
|
+
}
|
|
72
|
+
return rows;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function totalsFor(rows: readonly BundleV2Requirement[]): BundleV2Like['results']['totals'] {
|
|
76
|
+
const c = (d: string): number => rows.filter((r) => r.disposition === d).length;
|
|
77
|
+
return { executedPass: c('executed-pass'), executedFail: c('executed-fail'), skipped: c('skipped'), inapplicable: c('inapplicable'), blocked: c('blocked') };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function bundle(rows: readonly BundleV2Requirement[], overrides: Partial<BundleV2Like> = {}): BundleV2Like & Record<string, unknown> {
|
|
81
|
+
return {
|
|
82
|
+
bundleVersion: '2',
|
|
83
|
+
generatedAt: '2026-08-16T00:00:00.000Z',
|
|
84
|
+
generator: { name: 'test', version: '0.0.0' },
|
|
85
|
+
suite: { package: '@openwop/openwop-conformance', version: '1.114.0' },
|
|
86
|
+
host: { name: 'vector-host', version: '0.0.0' },
|
|
87
|
+
discovery: { url: 'https://example.invalid/.well-known/openwop', sha256: HEX, document: CORE_STANDARD_DISCOVERY },
|
|
88
|
+
claimedProfiles: [PROFILE],
|
|
89
|
+
results: { totals: totalsFor(rows), requirements: [...rows] },
|
|
90
|
+
scenarioManifestSha256: HEX,
|
|
91
|
+
targetConfigurationSha256: HEX,
|
|
92
|
+
...overrides,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
const ajv = new Ajv2020({ allErrors: true, strict: false });
|
|
97
|
+
addFormats(ajv);
|
|
98
|
+
const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'certification-bundle-v2.schema.json'), 'utf8')) as Record<string, unknown>;
|
|
99
|
+
const schemaValid = ajv.compile(schema);
|
|
100
|
+
|
|
101
|
+
describe('RFC 0148 §A/§C — certification-bundle-non-vacuous (consumer re-derivation)', () => {
|
|
102
|
+
it('valid-v2: every required row witnessed → evidence valid AND certified (and the vector is schema-valid)', () => {
|
|
103
|
+
const b = bundle(witnessedRows());
|
|
104
|
+
expect(schemaValid(b), JSON.stringify(schemaValid.errors)).toBe(true);
|
|
105
|
+
const v = verifyBundleV2(b);
|
|
106
|
+
expect(v.rejections).toEqual([]);
|
|
107
|
+
expect(v.evidenceValid).toBe(true);
|
|
108
|
+
expect(v.certified).toBe(true);
|
|
109
|
+
const p = v.profiles.find((x) => x.profile === PROFILE);
|
|
110
|
+
expect(p?.derivable).toBe(true);
|
|
111
|
+
expect(p?.required.length).toBeGreaterThan(5);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it('early-return: a required row that is executed-pass with assertionCount 0 REJECTS the evidence', () => {
|
|
115
|
+
const rows = witnessedRows();
|
|
116
|
+
rows[0] = { ...(rows[0] as BundleV2Requirement), assertionCount: 0 };
|
|
117
|
+
const v = verifyBundleV2(bundle(rows));
|
|
118
|
+
expect(v.evidenceValid).toBe(false);
|
|
119
|
+
expect(v.certified).toBe(false);
|
|
120
|
+
const p = v.profiles[0];
|
|
121
|
+
expect(p?.rejections.map((r) => r.kind)).toEqual(['vacuous-pass']);
|
|
122
|
+
expect(p?.rejections[0]?.requirementId).toBe(rows[0]?.requirementId);
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it('early-return (no count at all): an executed-pass with NO assertionCount is unwitnessed and REJECTS', () => {
|
|
126
|
+
const rows = witnessedRows();
|
|
127
|
+
const { assertionCount: _drop, ...noCount } = rows[1] as BundleV2Requirement & { assertionCount: number };
|
|
128
|
+
rows[1] = noCount;
|
|
129
|
+
const v = verifyBundleV2(bundle(rows));
|
|
130
|
+
expect(v.evidenceValid).toBe(false);
|
|
131
|
+
expect(v.profiles[0]?.rejections[0]?.kind).toBe('vacuous-pass');
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("the emitter's §A resolution of a silent zero-assertion file (blocked + marker detail) on a required row REJECTS as unwitnessed", () => {
|
|
135
|
+
const rows = witnessedRows();
|
|
136
|
+
rows[0] = { requirementId: (rows[0] as BundleV2Requirement).requirementId, scenarioId: (rows[0] as BundleV2Requirement).scenarioId, disposition: 'blocked', detail: UNCLASSIFIED_RETURN_DETAIL, assertionCount: 0 };
|
|
137
|
+
const v = verifyBundleV2(bundle(rows));
|
|
138
|
+
expect(v.evidenceValid).toBe(false);
|
|
139
|
+
expect(v.profiles[0]?.rejections.map((r) => r.kind)).toEqual(['unwitnessed-requirement']);
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it('missing-floor: a required requirement with no row at all REJECTS (unwitnessed), never "not certified"', () => {
|
|
143
|
+
const rows = witnessedRows().slice(1); // drop the first floor row
|
|
144
|
+
const dropped = requirementIdForScenario(floor().files[0] as string);
|
|
145
|
+
const v = verifyBundleV2(bundle(rows));
|
|
146
|
+
expect(v.evidenceValid).toBe(false);
|
|
147
|
+
const rej = v.profiles[0]?.rejections ?? [];
|
|
148
|
+
expect(rej.map((r) => r.kind)).toEqual(['unwitnessed-requirement']);
|
|
149
|
+
expect(rej[0]?.requirementId).toBe(dropped);
|
|
150
|
+
// and the row is NOT reported as merely not-certifiable
|
|
151
|
+
expect(v.profiles[0]?.notCertifiable).not.toContain(dropped);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('missing prefix witness: no `interrupt-*` scenario row and no summary row REJECTS the prefix requirement', () => {
|
|
155
|
+
const { prefixes } = floor();
|
|
156
|
+
if (prefixes.length === 0) return; // floor without a prefix requirement — nothing to assert here
|
|
157
|
+
const rows = witnessedRows().filter((r) => !prefixes.some((p) => r.scenarioId.startsWith(p)));
|
|
158
|
+
const v = verifyBundleV2(bundle(rows));
|
|
159
|
+
expect(v.evidenceValid).toBe(false);
|
|
160
|
+
expect(v.profiles[0]?.rejections.some((r) => r.kind === 'unwitnessed-requirement' && r.requirementId === requirementIdForPrefix(prefixes[0] as string))).toBe(true);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it('duplicate-requirement: two rows for one requirement REJECTS (exactly one disposition per requirement)', () => {
|
|
164
|
+
const rows = witnessedRows();
|
|
165
|
+
rows.push({ ...(rows[0] as BundleV2Requirement) });
|
|
166
|
+
const v = verifyBundleV2(bundle(rows));
|
|
167
|
+
expect(v.evidenceValid).toBe(false);
|
|
168
|
+
expect(v.rejections.map((r) => r.kind)).toContain('duplicate-requirement');
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it('tampered-witness: totals that disagree with the rows REJECT — a hand-edited count is not a witness', () => {
|
|
172
|
+
const rows = witnessedRows();
|
|
173
|
+
const b = bundle(rows);
|
|
174
|
+
const tampered = { ...b, results: { ...b.results, totals: { ...b.results.totals, blocked: 0, executedPass: b.results.totals.executedPass + 1 } } };
|
|
175
|
+
const v = verifyBundleV2(tampered);
|
|
176
|
+
expect(v.evidenceValid).toBe(false);
|
|
177
|
+
expect(v.rejections.map((r) => r.kind)).toEqual(['totals-mismatch']);
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('tampered-witness: an invented disposition name REJECTS', () => {
|
|
181
|
+
const rows = witnessedRows();
|
|
182
|
+
rows[0] = { ...(rows[0] as BundleV2Requirement), disposition: 'passed' };
|
|
183
|
+
const v = verifyBundleV2(bundle(rows));
|
|
184
|
+
expect(v.rejections.some((r) => r.kind === 'unknown-disposition')).toBe(true);
|
|
185
|
+
expect(v.evidenceValid).toBe(false);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it('a non-pass without a reason REJECTS (RFC 0148 §A: anything other than executed-pass says why)', () => {
|
|
189
|
+
const rows = witnessedRows();
|
|
190
|
+
rows[0] = { requirementId: (rows[0] as BundleV2Requirement).requirementId, scenarioId: (rows[0] as BundleV2Requirement).scenarioId, disposition: 'blocked' };
|
|
191
|
+
const v = verifyBundleV2(bundle(rows));
|
|
192
|
+
expect(v.rejections.map((r) => r.kind)).toContain('reason-missing');
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
it('blocked-floor: an HONEST blocked row on a required requirement is VALID evidence that does NOT certify', () => {
|
|
196
|
+
const rows = witnessedRows();
|
|
197
|
+
rows[0] = { ...(rows[0] as BundleV2Requirement), disposition: 'blocked', detail: 'seam unreachable from this runner (host-callback)' };
|
|
198
|
+
delete (rows[0] as { assertionCount?: number }).assertionCount;
|
|
199
|
+
const v = verifyBundleV2(bundle(rows));
|
|
200
|
+
expect(v.rejections).toEqual([]);
|
|
201
|
+
expect(v.evidenceValid).toBe(true);
|
|
202
|
+
expect(v.certified).toBe(false);
|
|
203
|
+
expect(v.profiles[0]?.notCertifiable).toEqual([(rows[0] as BundleV2Requirement).requirementId]);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it('executed-fail on a required row: valid evidence, not certified', () => {
|
|
207
|
+
const rows = witnessedRows();
|
|
208
|
+
rows[2] = { ...(rows[2] as BundleV2Requirement), disposition: 'executed-fail', detail: 'assertion failed against target', assertionCount: 4 };
|
|
209
|
+
const v = verifyBundleV2(bundle(rows));
|
|
210
|
+
expect(v.evidenceValid).toBe(true);
|
|
211
|
+
expect(v.certified).toBe(false);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it('inapplicable / skipped with a reason on a required row certifies (RFC 0148 §A CERTIFIABLE set)', () => {
|
|
215
|
+
const rows = witnessedRows();
|
|
216
|
+
rows[0] = { requirementId: (rows[0] as BundleV2Requirement).requirementId, scenarioId: (rows[0] as BundleV2Requirement).scenarioId, disposition: 'inapplicable', detail: 'profile not advertised in the captured discovery set', assertionCount: 0 };
|
|
217
|
+
rows[1] = { requirementId: (rows[1] as BundleV2Requirement).requirementId, scenarioId: (rows[1] as BundleV2Requirement).scenarioId, disposition: 'skipped', detail: 'operator opted the profile out (OPENWOP_OPTED_OUT_PROFILES)' };
|
|
218
|
+
const v = verifyBundleV2(bundle(rows));
|
|
219
|
+
expect(v.evidenceValid).toBe(true);
|
|
220
|
+
expect(v.certified).toBe(true);
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it('a claim the discovery document does not derive is not certified even with perfect rows (RFC 0089 §B(1))', () => {
|
|
224
|
+
const b = bundle(witnessedRows(), { discovery: { document: { protocolVersion: '1.0' } } });
|
|
225
|
+
const v = verifyBundleV2(b);
|
|
226
|
+
expect(v.evidenceValid).toBe(true);
|
|
227
|
+
expect(v.profiles[0]?.derivable).toBe(false);
|
|
228
|
+
expect(v.certified).toBe(false);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('a claimed profile with NO defined floor is unprovable — floorUnspecified, never certified (G6)', () => {
|
|
232
|
+
const b = bundle(witnessedRows(), { claimedProfiles: ['openwop-not-a-profile'] });
|
|
233
|
+
const v = verifyBundleV2(b);
|
|
234
|
+
expect(v.profiles[0]?.floorUnspecified).toBe(true);
|
|
235
|
+
expect(v.certified).toBe(false);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it('a discovery-only profile certifies on derivation alone (RFC 0155 §A: openwop-discovery-core)', () => {
|
|
239
|
+
const b = bundle(witnessedRows(), { claimedProfiles: ['openwop-discovery-core'] });
|
|
240
|
+
const v = verifyBundleV2(b);
|
|
241
|
+
expect(v.certified).toBe(true);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it('a v1 document handed to the v2 verifier is rejected as not-v2 rather than silently read', () => {
|
|
245
|
+
const b = bundle(witnessedRows(), { bundleVersion: '1' });
|
|
246
|
+
const v = verifyBundleV2(b);
|
|
247
|
+
expect(v.rejections.map((r) => r.kind)).toContain('not-v2');
|
|
248
|
+
});
|
|
249
|
+
});
|