@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
|
@@ -20,35 +20,25 @@
|
|
|
20
20
|
import { describe, it, expect } from 'vitest';
|
|
21
21
|
import Ajv2020 from 'ajv/dist/2020.js';
|
|
22
22
|
import addFormats from 'ajv-formats';
|
|
23
|
-
import { readFileSync } from 'node:fs';
|
|
23
|
+
import { readFileSync, readdirSync } from 'node:fs';
|
|
24
24
|
import { join } from 'node:path';
|
|
25
25
|
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
26
26
|
|
|
27
27
|
function compileWorkflowDefinition(): ReturnType<Ajv2020['compile']> {
|
|
28
28
|
const ajv = new Ajv2020({ strict: false, allErrors: true });
|
|
29
29
|
addFormats(ajv);
|
|
30
|
-
// Register
|
|
31
|
-
//
|
|
32
|
-
// `
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
) as Record<string, unknown>;
|
|
43
|
-
ajv.addSchema(agentRefSchema, 'agent-ref.schema.json');
|
|
44
|
-
ajv.addSchema(promptRefSchema, 'prompt-ref.schema.json');
|
|
45
|
-
ajv.addSchema(promptRefSchema, './prompt-ref.schema.json');
|
|
46
|
-
ajv.addSchema(promptKindSchema, 'prompt-kind.schema.json');
|
|
47
|
-
ajv.addSchema(promptKindSchema, './prompt-kind.schema.json');
|
|
48
|
-
const schema = JSON.parse(
|
|
49
|
-
readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8'),
|
|
50
|
-
) as Record<string, unknown>;
|
|
51
|
-
return ajv.compile(schema);
|
|
30
|
+
// Register EVERY schema in the corpus (S14, 2026-08-16) — a fixed ref list is
|
|
31
|
+
// a claim about the schema graph that nothing keeps true; the fourth cross-file
|
|
32
|
+
// `$ref` (#1009) broke the previous list at describe time.
|
|
33
|
+
for (const file of readdirSync(SCHEMAS_DIR).filter((f) => f.endsWith('.schema.json'))) {
|
|
34
|
+
const s = JSON.parse(readFileSync(join(SCHEMAS_DIR, file), 'utf8')) as Record<string, unknown>;
|
|
35
|
+
ajv.addSchema(s, file);
|
|
36
|
+
ajv.addSchema(s, `./${file}`);
|
|
37
|
+
}
|
|
38
|
+
return (
|
|
39
|
+
ajv.getSchema('workflow-definition.schema.json') ??
|
|
40
|
+
ajv.compile(JSON.parse(readFileSync(join(SCHEMAS_DIR, 'workflow-definition.schema.json'), 'utf8')) as Record<string, unknown>)
|
|
41
|
+
);
|
|
52
42
|
}
|
|
53
43
|
|
|
54
44
|
/** Build the minimal-required shape of a WorkflowDefinition. Tests
|
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
|
|
35
35
|
import { describe, it, expect } from 'vitest';
|
|
36
|
+
import { readErrorCode, readRetriable } from '../lib/error-envelope.js';
|
|
36
37
|
import { driver } from '../lib/driver.js';
|
|
37
38
|
import { behaviorGate } from '../lib/behavior-gate.js';
|
|
38
39
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
@@ -76,7 +77,7 @@ async function resolve(body: Record<string, unknown>): Promise<{ status: number;
|
|
|
76
77
|
}
|
|
77
78
|
|
|
78
79
|
function reasonOf(json: unknown): string | undefined {
|
|
79
|
-
return (json
|
|
80
|
+
return readErrorCode(json);
|
|
80
81
|
}
|
|
81
82
|
|
|
82
83
|
const IDENTITY = { scheme: 'spiffe', subject: 'spiffe://example/dispatcher', issuer: 'spiffe://example' };
|
|
@@ -154,9 +155,11 @@ describe('RFC 0154 §A — workload identity resolution (capability-gated behavi
|
|
|
154
155
|
if (!behaviorGate(PROFILE, (await caps())?.supported === true)) return;
|
|
155
156
|
const r = await resolve({ identity: { scheme: 'spiffe', subject: 'spiffe://example/unknown' } });
|
|
156
157
|
if (r === null || r.status < 400) return;
|
|
157
|
-
|
|
158
|
+
// canonical flat envelope: code = `error`, retriable = `details.retriable`
|
|
159
|
+
// (the legacy nested `error.{code,retriable}` the §20 catalog prescribed until
|
|
160
|
+
// 2026-08-16 is tolerated by the helpers for the deprecation window)
|
|
158
161
|
expect(
|
|
159
|
-
|
|
162
|
+
readRetriable(r.json),
|
|
160
163
|
driver.describe(
|
|
161
164
|
'spec/v1/host-sample-test-seams.md §20',
|
|
162
165
|
'an identity that does not resolve will not resolve on retry. Marking it retriable invites ' +
|
|
@@ -166,7 +169,7 @@ describe('RFC 0154 §A — workload identity resolution (capability-gated behavi
|
|
|
166
169
|
expect(
|
|
167
170
|
['identity_unverified', 'identity_unresolvable', 'audience_mismatch', 'delegation_expired', 'sender_constraint_missing'],
|
|
168
171
|
driver.describe('RFCS/0154 §A', 'failures use a closed reason vocabulary'),
|
|
169
|
-
).toContain(
|
|
172
|
+
).toContain(readErrorCode(r.json));
|
|
170
173
|
});
|
|
171
174
|
|
|
172
175
|
it('a resolution response carries no credential material', async () => {
|
|
@@ -24,6 +24,7 @@
|
|
|
24
24
|
* @see SECURITY/invariants.yaml workspace-cross-tenant-isolation
|
|
25
25
|
*/
|
|
26
26
|
import { describe, it, expect } from 'vitest';
|
|
27
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
27
28
|
import { randomUUID } from 'node:crypto';
|
|
28
29
|
import { driver } from '../lib/driver.js';
|
|
29
30
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
@@ -42,11 +43,11 @@ const asTenantB = { headers: { Authorization: `Bearer ${tenantBKey ?? ''}` } };
|
|
|
42
43
|
|
|
43
44
|
describe('workspace-cross-tenant-isolation (black-box): a §C file MUST NOT leak across owners (RFC 0059 §E WCT-1)', () => {
|
|
44
45
|
it('a file written by owner A is unreadable + un-enumerable by a second-tenant credential', async () => {
|
|
45
|
-
if (!(await workspaceSupported())) return
|
|
46
|
+
if (!(await workspaceSupported())) return softSkip('inapplicable', 'capability not advertised — skip');
|
|
46
47
|
if (!tenantBKey) {
|
|
47
48
|
// eslint-disable-next-line no-console
|
|
48
49
|
console.warn('[workspace-cross-tenant-isolation-blackbox] OPENWOP_TEST_TENANT_B_API_KEY not supplied; skipping the production-path cross-tenant assertion');
|
|
49
|
-
return;
|
|
50
|
+
return softSkip('blocked', '[workspace-cross-tenant-isolation-blackbox] OPENWOP_TEST_TENANT_B_API_KEY not supplied; skipping the production-path cross-tenant assertion');
|
|
50
51
|
}
|
|
51
52
|
|
|
52
53
|
const path = `wct-blackbox-${randomUUID()}.md`;
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
|
|
21
21
|
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
22
23
|
import { driver } from '../lib/driver.js';
|
|
23
24
|
import { capabilityFamily } from '../lib/discovery-capabilities.js';
|
|
24
25
|
|
|
@@ -40,11 +41,11 @@ const SEAM_PATH = 'WCT1-SECRET.md';
|
|
|
40
41
|
|
|
41
42
|
describe('workspace-cross-tenant-isolation: a workspace file MUST NOT leak across owners (RFC 0059 §E WCT-1)', () => {
|
|
42
43
|
it('a file written under {tenant A, workspace A} is not readable under a different owner', async () => {
|
|
43
|
-
if (!(await workspaceSupported())) return
|
|
44
|
+
if (!(await workspaceSupported())) return softSkip('inapplicable', 'capability not advertised — skip');
|
|
44
45
|
|
|
45
46
|
// Owner A writes a file.
|
|
46
47
|
const put = await seam({ tenant: 'wct1-tenant-a', workspace: 'ws-a', op: 'put', path: SEAM_PATH, content: 'A-only secret body' });
|
|
47
|
-
if (put.status === 404) return
|
|
48
|
+
if (put.status === 404) return softSkip('blocked', 'seam unwired — soft-skip the behavioral probe');
|
|
48
49
|
expect(put.status, driver.describe('agent-workspace.md §C PUT', 'seam put MUST succeed for the owning workspace')).toBe(200);
|
|
49
50
|
|
|
50
51
|
// A DIFFERENT workspace (same tenant) MUST NOT read it.
|
package/src/setup.ts
CHANGED
|
@@ -30,6 +30,11 @@ import { setMultiAgentCapabilities } from './lib/multi-agent-capabilities.js';
|
|
|
30
30
|
import { OtelCollector, setCollector } from './lib/otel-collector.js';
|
|
31
31
|
import { McpFakeServer, setMcpFakeServer } from './lib/mcp-fake-server.js';
|
|
32
32
|
import { A2AFakePeer, setA2AFakePeer } from './lib/a2a-fake-peer.js';
|
|
33
|
+
import { afterAll, afterEach, beforeAll, expect } from 'vitest';
|
|
34
|
+
import { basename } from 'node:path';
|
|
35
|
+
import { recordRequirement, journalLength, journalSince } from './lib/requirement-ledger.js';
|
|
36
|
+
import { requirementIdForFile, resolveFileRecord, type FileTestState } from './lib/scenario-disposition.js';
|
|
37
|
+
import { softSkipDisposition } from './lib/soft-skip.js';
|
|
33
38
|
import type { DiscoveryPayload } from './lib/profiles.js';
|
|
34
39
|
|
|
35
40
|
const SUITE_INIT_TIMEOUT_MS = 5_000;
|
|
@@ -181,7 +186,15 @@ async function maybeStartA2AFakePeer(): Promise<void> {
|
|
|
181
186
|
}
|
|
182
187
|
const portEnv = process.env.OPENWOP_A2A_FAKE_PEER_PORT;
|
|
183
188
|
const requestedPort = portEnv ? Number(portEnv) : 0;
|
|
184
|
-
|
|
189
|
+
// Dual-era since 1.112.0. Order = card shape for a header-less GET (first
|
|
190
|
+
// wins); both eras are spoken on the RPC path. Default keeps the 0.3 card so
|
|
191
|
+
// today's 0.3 clients (which read `card.url`) keep working; set
|
|
192
|
+
// OPENWOP_A2A_FAKE_PEER_VERSIONS=1.0,0.3 to serve the 1.0 card by default.
|
|
193
|
+
const versionsEnv = process.env.OPENWOP_A2A_FAKE_PEER_VERSIONS;
|
|
194
|
+
const versions = versionsEnv
|
|
195
|
+
? (versionsEnv.split(',').map((v) => v.trim()).filter((v) => v === '1.0' || v === '0.3') as Array<'1.0' | '0.3'>)
|
|
196
|
+
: undefined;
|
|
197
|
+
const peer = new A2AFakePeer(versions && versions.length > 0 ? { protocolVersions: versions } : undefined);
|
|
185
198
|
await peer.start(requestedPort);
|
|
186
199
|
setA2AFakePeer(peer);
|
|
187
200
|
// eslint-disable-next-line no-console
|
|
@@ -195,3 +208,84 @@ await loadHostFixtures();
|
|
|
195
208
|
await maybeStartOtelCollector();
|
|
196
209
|
await maybeStartMcpFakeServer();
|
|
197
210
|
await maybeStartA2AFakePeer();
|
|
211
|
+
|
|
212
|
+
// ─── RFC 0148 §A file-level disposition recording (S6, acceptance item 2) ─────
|
|
213
|
+
//
|
|
214
|
+
// Every scenario FILE records exactly one disposition for its own requirement
|
|
215
|
+
// id when it finishes. `afterEach` collects per-test states; `afterAll` folds
|
|
216
|
+
// them (fileDisposition) and records — `executed-fail` / `executed-pass` /
|
|
217
|
+
// `inapplicable` or `skipped` (when a behaviorGate in the file recorded that
|
|
218
|
+
// reason for the profile it gates on) / else `blocked` (an all-skipped file
|
|
219
|
+
// with no reason is an unclassified return, and §A resolves it to blocked).
|
|
220
|
+
// When the runner set OPENWOP_LEDGER_PATH the recording also lands in the JSONL
|
|
221
|
+
// sink, which is how `--certify` gets requirement-level dispositions instead of
|
|
222
|
+
// inferring them from per-file pass/fail/skip.
|
|
223
|
+
//
|
|
224
|
+
// Setup-file hooks apply to every file in the worker; state is keyed by file.
|
|
225
|
+
const _fileStates = new Map<string, FileTestState[]>();
|
|
226
|
+
const _fileAssertions = new Map<string, number>();
|
|
227
|
+
const _ledgerMarks = new Map<string, number>();
|
|
228
|
+
function _fileOf(task: { file?: { filepath?: string; name?: string } } | undefined): string | null {
|
|
229
|
+
const f = task?.file?.filepath ?? task?.file?.name;
|
|
230
|
+
return typeof f === 'string' && f.length > 0 ? basename(f) : null;
|
|
231
|
+
}
|
|
232
|
+
beforeAll(({}, suite) => {
|
|
233
|
+
// Mark the ledger journal BEFORE the file's tests run, so a behaviorGate
|
|
234
|
+
// decision made by the very first test is inside the file's window.
|
|
235
|
+
const s = suite as unknown as { filepath?: string; name?: string; file?: { filepath?: string; name?: string } };
|
|
236
|
+
const file = _fileOf({ file: s.file ?? s });
|
|
237
|
+
if (file !== null && !_ledgerMarks.has(file)) _ledgerMarks.set(file, journalLength());
|
|
238
|
+
});
|
|
239
|
+
afterEach(({ task }) => {
|
|
240
|
+
const file = _fileOf(task as { file?: { filepath?: string; name?: string } });
|
|
241
|
+
if (file === null) return;
|
|
242
|
+
if (!_ledgerMarks.has(file)) _ledgerMarks.set(file, 0);
|
|
243
|
+
const state = task.result?.state;
|
|
244
|
+
const arr = _fileStates.get(file) ?? [];
|
|
245
|
+
arr.push(state === 'pass' ? 'pass' : state === 'fail' ? 'fail' : 'skip');
|
|
246
|
+
_fileStates.set(file, arr);
|
|
247
|
+
// RFC 0148 §C assertionCount: how many `expect` calls this test actually made.
|
|
248
|
+
// A leg that early-returns from a gate makes zero, and a file of such legs is
|
|
249
|
+
// an `executed-pass` with assertionCount 0 — visible, and unclassified for a
|
|
250
|
+
// claimed floor.
|
|
251
|
+
let calls = 0;
|
|
252
|
+
try {
|
|
253
|
+
calls = (expect.getState() as { assertionCalls?: number }).assertionCalls ?? 0;
|
|
254
|
+
} catch {
|
|
255
|
+
/* no state — count 0 */
|
|
256
|
+
}
|
|
257
|
+
_fileAssertions.set(file, (_fileAssertions.get(file) ?? 0) + calls);
|
|
258
|
+
});
|
|
259
|
+
afterAll(({}, suite) => {
|
|
260
|
+
// vitest 4: the suite/file task is the SECOND argument. For a file-level
|
|
261
|
+
// afterAll registered from a setup file, `suite` is the File task itself.
|
|
262
|
+
const s = suite as unknown as { filepath?: string; name?: string; file?: { filepath?: string; name?: string } };
|
|
263
|
+
const file = _fileOf({ file: s.file ?? s });
|
|
264
|
+
if (file === null || !file.endsWith('.test.ts')) return;
|
|
265
|
+
const states = _fileStates.get(file) ?? [];
|
|
266
|
+
// Did a behaviorGate in this file record inapplicable/skipped for its profile?
|
|
267
|
+
const mark = _ledgerMarks.get(file) ?? 0;
|
|
268
|
+
const since = journalSince(mark);
|
|
269
|
+
const gateReason = since.some((e) => e.disposition === 'inapplicable')
|
|
270
|
+
? 'inapplicable'
|
|
271
|
+
: since.some((e) => e.disposition === 'skipped')
|
|
272
|
+
? 'skipped'
|
|
273
|
+
: undefined;
|
|
274
|
+
const assertionCount = _fileAssertions.get(file) ?? 0;
|
|
275
|
+
// RFC 0148 §A: a pass with zero assertions is an unclassified return. If the
|
|
276
|
+
// file said why (softSkip / seamAbsent / behaviorGate), that is its
|
|
277
|
+
// disposition; if it said nothing, the runner resolves it to `blocked` with
|
|
278
|
+
// the marker detail — never to a pass. Floors still REJECT that row, so the
|
|
279
|
+
// honest bundle row and the pressure to say why both survive. The rule is
|
|
280
|
+
// `resolveFileRecord` (pinned by conformance-execution-witness.test.ts).
|
|
281
|
+
const { disposition, detail } = resolveFileRecord(states, gateReason, assertionCount, softSkipDisposition(file));
|
|
282
|
+
try {
|
|
283
|
+
recordRequirement(requirementIdForFile(file), disposition, detail, { assertionCount });
|
|
284
|
+
} catch {
|
|
285
|
+
/* a scenario that recorded its own file id first wins; never fail the file for bookkeeping */
|
|
286
|
+
}
|
|
287
|
+
_fileStates.delete(file);
|
|
288
|
+
_fileAssertions.delete(file);
|
|
289
|
+
_ledgerMarks.delete(file);
|
|
290
|
+
});
|
|
291
|
+
|