@openwop/openwop-conformance 1.163.0 → 2.0.0-rc.1
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 +11 -2
- package/dist/cli.js +262 -14
- package/dist/lib/certification-bundle-v3.js +142 -0
- package/dist/lib/paths.js +25 -3
- package/dist/lib/requirement-ids.js +57 -4
- package/dist/lib/requirement-ledger.js +1 -0
- package/dist/lib/scenario-disposition.js +6 -1
- package/dist/lib/spec-coherence.js +13 -32
- package/dist/spec-artifacts.lock.json +5 -0
- package/package.json +11 -33
- package/requirements.json +22892 -11701
- package/scenario-majors.json +1493 -0
- package/schemas/CORPUS-STAMP.json +137 -5
- package/src/cli.ts +236 -19
- package/src/global-setup.ts +3 -2
- package/src/lib/certification-bundle-v3.ts +175 -0
- package/src/lib/corpus-stamp.ts +29 -1
- package/src/lib/driver.ts +9 -1
- package/src/lib/era2-seed.ts +197 -0
- package/src/lib/paths.ts +25 -3
- package/src/lib/requirement-ids.ts +54 -4
- package/src/lib/requirement-ledger.ts +7 -1
- package/src/lib/scenario-disposition.ts +6 -1
- package/src/lib/seams.ts +45 -0
- package/src/lib/spec-coherence.ts +15 -32
- package/src/lib/v2.ts +62 -0
- package/src/scenarios/a2a-1-0-agent-card.test.ts +17 -16
- package/src/scenarios/a2a-1-0-task-roundtrip.test.ts +11 -10
- package/src/scenarios/a2a-card-runtime-consistency.test.ts +18 -17
- package/src/scenarios/a2a-peer-authority.test.ts +7 -6
- package/src/scenarios/a2a-task-roundtrip.test.ts +30 -29
- package/src/scenarios/a2a-version-negotiation.test.ts +16 -14
- package/src/scenarios/a2ui-surface-degrades.test.ts +5 -3
- package/src/scenarios/a2ui-surface-delta-transport.test.ts +35 -33
- package/src/scenarios/a2ui-surface-replay.test.ts +6 -4
- package/src/scenarios/a2ui-surface-shape.test.ts +11 -10
- package/src/scenarios/a2ui-surface-version-refusal.test.ts +6 -4
- package/src/scenarios/a2ui-untrusted-blocks-approval.test.ts +5 -3
- package/src/scenarios/agent-capability-degraded-projection.test.ts +12 -11
- package/src/scenarios/agent-channel-dispatch.test.ts +18 -16
- package/src/scenarios/agent-deployment-lifecycle.test.ts +20 -19
- package/src/scenarios/agent-deployment-shape.test.ts +22 -24
- package/src/scenarios/agent-eval-run.test.ts +19 -18
- package/src/scenarios/agent-eval-suite-shape.test.ts +20 -22
- package/src/scenarios/agent-live-allowlist-enforced.test.ts +6 -5
- package/src/scenarios/agent-live-invocation-bracket.test.ts +13 -12
- package/src/scenarios/agent-live-runtime-shape.test.ts +10 -12
- package/src/scenarios/agent-live-structured-output.test.ts +7 -6
- package/src/scenarios/agent-loop-iteration-monotonic.test.ts +3 -3
- package/src/scenarios/agent-loop-stateful-resume.test.ts +2 -2
- package/src/scenarios/agent-loop-version5-shape.test.ts +4 -4
- package/src/scenarios/agent-loop-workspace-snapshot.test.ts +3 -3
- package/src/scenarios/agent-manifest-role-profile.test.ts +9 -9
- package/src/scenarios/agent-manifest-runtime.test.ts +13 -12
- package/src/scenarios/agent-org-chart-scoping.test.ts +14 -13
- package/src/scenarios/agent-org-chart-shape.test.ts +11 -13
- package/src/scenarios/agent-platform-aggregate-evidence.test.ts +5 -4
- package/src/scenarios/agent-platform-profile.test.ts +18 -19
- package/src/scenarios/agent-requires-capabilities-shape.test.ts +4 -4
- package/src/scenarios/agent-roster-attribution.test.ts +18 -17
- package/src/scenarios/agent-roster-shape.test.ts +17 -19
- package/src/scenarios/agent-verifier-shape.test.ts +11 -11
- package/src/scenarios/agentConfidenceEscalation.test.ts +2 -1
- package/src/scenarios/agentMemoryCrossTenantIsolation.test.ts +6 -5
- package/src/scenarios/agentMemoryRedactionContract.test.ts +2 -1
- package/src/scenarios/agentMemoryRoundTrip.test.ts +2 -1
- package/src/scenarios/agentMemoryTtlExpiry.test.ts +4 -3
- package/src/scenarios/agentMessageReducer.test.ts +2 -1
- package/src/scenarios/agentMetadata.test.ts +2 -1
- package/src/scenarios/agentPackCatalog.test.ts +23 -21
- package/src/scenarios/agentPackExport.test.ts +4 -2
- package/src/scenarios/agentPackHandoffSchemaValidation.test.ts +5 -4
- package/src/scenarios/agentPackInstall.test.ts +4 -2
- package/src/scenarios/agentPackProvenance.test.ts +2 -1
- package/src/scenarios/agentReasoningEvents.test.ts +3 -2
- package/src/scenarios/agentReasoningStreaming.test.ts +11 -10
- package/src/scenarios/agents-run-tool-allowlist.test.ts +14 -12
- package/src/scenarios/ai-envelope-shape.test.ts +31 -29
- package/src/scenarios/aiEnvelope.capBreached.test.ts +26 -24
- package/src/scenarios/aiEnvelope.contractRefusal.test.ts +36 -34
- package/src/scenarios/aiEnvelope.correlationReplay.test.ts +17 -15
- package/src/scenarios/aiEnvelope.redaction.test.ts +22 -20
- package/src/scenarios/aiEnvelope.schemaDrift.test.ts +18 -16
- package/src/scenarios/aiEnvelope.trustBoundaryPropagation.test.ts +38 -36
- package/src/scenarios/aiEnvelope.universalKinds.test.ts +31 -29
- package/src/scenarios/aiproviders-input-shape.test.ts +5 -5
- package/src/scenarios/aiproviders-realtimevoice-shape.test.ts +14 -16
- package/src/scenarios/aiproviders-selfhosted-honesty.test.ts +6 -4
- package/src/scenarios/aiproviders-selfhosted-shape.test.ts +15 -17
- package/src/scenarios/aiproviders-speechsynth-shape.test.ts +6 -8
- package/src/scenarios/aiproviders-subscription-scope.test.ts +7 -8
- package/src/scenarios/anonymous-actor-audit-opaque.test.ts +9 -8
- package/src/scenarios/anonymous-actor-default-deny.test.ts +10 -9
- package/src/scenarios/anonymous-actor-egress-guarded.test.ts +6 -5
- package/src/scenarios/anonymous-actor-no-secret-reach.test.ts +7 -6
- package/src/scenarios/anonymous-actor-shape.test.ts +17 -19
- package/src/scenarios/anonymous-actor-write-gated.test.ts +9 -8
- package/src/scenarios/append-ordering.test.ts +7 -5
- package/src/scenarios/approval-gate-events.test.ts +4 -3
- package/src/scenarios/approval-gate-flow.test.ts +4 -3
- package/src/scenarios/approval-payload.test.ts +5 -4
- package/src/scenarios/artifact-auth.test.ts +7 -6
- package/src/scenarios/artifact-type-pack-install.test.ts +6 -6
- package/src/scenarios/artifact-type-pack-manifest-validation.test.ts +9 -8
- package/src/scenarios/artifact-type-registration-source.test.ts +20 -20
- package/src/scenarios/artifact-type-store-emission.test.ts +15 -18
- package/src/scenarios/artifact-type-store-without-render.test.ts +4 -4
- package/src/scenarios/audit-log-integrity.test.ts +8 -6
- package/src/scenarios/auth-api-key-rotation.test.ts +11 -9
- package/src/scenarios/auth-mtls.test.ts +22 -20
- package/src/scenarios/auth-oauth2-client-credentials.test.ts +16 -14
- package/src/scenarios/auth-oidc-user-bearer.test.ts +26 -24
- package/src/scenarios/auth-saml-profile.test.ts +14 -12
- package/src/scenarios/auth-scim-profile.test.ts +3 -2
- package/src/scenarios/auth-subject-link-key-class.test.ts +8 -7
- package/src/scenarios/auth-subject-link.test.ts +11 -10
- package/src/scenarios/auth.test.ts +5 -4
- package/src/scenarios/authorization-fail-closed.test.ts +3 -2
- package/src/scenarios/authorization-roles-shape.test.ts +9 -7
- package/src/scenarios/blob-cross-tenant-isolation.test.ts +8 -6
- package/src/scenarios/blob-presign-expiry.test.ts +12 -10
- package/src/scenarios/blob-roundtrip.test.ts +0 -0
- package/src/scenarios/budget-enforcement.test.ts +15 -14
- package/src/scenarios/budget-policy-shape.test.ts +18 -19
- package/src/scenarios/bulk-cancel.test.ts +9 -8
- package/src/scenarios/byok-auth-modes.test.ts +13 -14
- package/src/scenarios/byok-roundtrip.test.ts +12 -10
- package/src/scenarios/cache-cross-tenant-isolation.test.ts +8 -6
- package/src/scenarios/cache-ttl-expiry.test.ts +7 -5
- package/src/scenarios/callai-multimodal.test.ts +5 -3
- package/src/scenarios/cancellation.test.ts +6 -5
- package/src/scenarios/cap-breach.test.ts +15 -14
- package/src/scenarios/capability-arm-witness.test.ts +5 -3
- package/src/scenarios/certification-bundle-non-vacuous.test.ts +20 -19
- package/src/scenarios/certification-bundle-redaction.test.ts +11 -10
- package/src/scenarios/certification-bundle-v2.test.ts +11 -10
- package/src/scenarios/chain-compensation-expansion.test.ts +14 -13
- package/src/scenarios/chain-produced-var-roundtrip.test.ts +13 -14
- package/src/scenarios/chain-subchain-cycle-rejected.test.ts +9 -10
- package/src/scenarios/chain-subchain-fanout.test.ts +8 -9
- package/src/scenarios/chain-subchain-sibling.test.ts +14 -16
- package/src/scenarios/channel-presence-behavioral.test.ts +9 -7
- package/src/scenarios/channel-presence-shape.test.ts +11 -12
- package/src/scenarios/channel-ttl.test.ts +6 -5
- package/src/scenarios/chat-card-pack-execution.test.ts +4 -4
- package/src/scenarios/chat-card-pack-manifest-validation.test.ts +9 -8
- package/src/scenarios/commitment-fired.test.ts +6 -5
- package/src/scenarios/compensation-behavior.test.ts +24 -23
- package/src/scenarios/compensation-recovery.test.ts +28 -27
- package/src/scenarios/configurable-schema.test.ts +8 -6
- package/src/scenarios/conformance-advertised-seam-required.test.ts +6 -5
- package/src/scenarios/conformance-execution-witness.test.ts +9 -8
- package/src/scenarios/connection-pack-apihosts.test.ts +19 -17
- package/src/scenarios/connection-pack-manifest-valid.test.ts +12 -11
- package/src/scenarios/connection-pack-no-credential-material.test.ts +8 -6
- package/src/scenarios/connection-pack-reach-exclusive.test.ts +5 -4
- package/src/scenarios/connection-pack-write-reconsent.test.ts +6 -4
- package/src/scenarios/connection-provider-resolution.test.ts +18 -16
- package/src/scenarios/connector-manifest-validity.test.ts +8 -7
- package/src/scenarios/context-budget-transcript-bound.test.ts +17 -15
- package/src/scenarios/context-summarization-replay.test.ts +14 -12
- package/src/scenarios/contract-provenance.test.ts +18 -18
- package/src/scenarios/conversation-turn-model-provenance-shape.test.ts +14 -15
- package/src/scenarios/conversationCapabilityNegotiation.test.ts +2 -1
- package/src/scenarios/conversationLifecycle.test.ts +2 -1
- package/src/scenarios/conversationReplayDeterminism.test.ts +4 -2
- package/src/scenarios/conversationVsLegacySuspend.test.ts +2 -1
- package/src/scenarios/core-standard-profile.test.ts +8 -9
- package/src/scenarios/cost-attribution.test.ts +23 -21
- package/src/scenarios/credential-payload-redaction.test.ts +4 -3
- package/src/scenarios/credentials-capability-shape.test.ts +5 -4
- package/src/scenarios/cross-engine-append-behavior.test.ts +11 -10
- package/src/scenarios/cross-engine-append-ordering.test.ts +4 -3
- package/src/scenarios/cross-host-ancestry-endpoint.test.ts +11 -10
- package/src/scenarios/cross-host-causation-shape.test.ts +7 -6
- package/src/scenarios/cross-workspace-isolation.test.ts +4 -3
- package/src/scenarios/data-residency-admission.test.ts +15 -14
- package/src/scenarios/deadletter-capability-shape.test.ts +6 -4
- package/src/scenarios/deadletter-retry-exhaustion.test.ts +4 -3
- package/src/scenarios/debug-bundle-truncation.test.ts +9 -7
- package/src/scenarios/debugBundle.test.ts +25 -23
- package/src/scenarios/discovery-families-view.test.ts +5 -4
- package/src/scenarios/discovery.test.ts +30 -29
- package/src/scenarios/dispatch-cross-worker-handoff.test.ts +10 -8
- package/src/scenarios/dispatch-fanout-parallel.test.ts +24 -22
- package/src/scenarios/dispatch-input-mapping.test.ts +12 -10
- package/src/scenarios/dispatch-output-mapping.test.ts +9 -7
- package/src/scenarios/dispatch-per-item-input.test.ts +22 -20
- package/src/scenarios/dispatchLoop.test.ts +3 -2
- package/src/scenarios/distillation-index-roundtrip.test.ts +3 -3
- package/src/scenarios/distillation-secret-carryforward.test.ts +2 -2
- package/src/scenarios/distillation-shape.test.ts +4 -4
- package/src/scenarios/distillation-stable-archive.test.ts +6 -6
- package/src/scenarios/distillation-token-budget.test.ts +5 -5
- package/src/scenarios/durability-poison-exhaustion.test.ts +7 -6
- package/src/scenarios/egress-audience-binding.test.ts +11 -10
- package/src/scenarios/egress-decision-content-free.test.ts +6 -5
- package/src/scenarios/egress-provenance-shape.test.ts +17 -18
- package/src/scenarios/envelope-completion-distinguishes-truncation.test.ts +27 -25
- package/src/scenarios/envelope-nl-to-format-engaged.test.ts +19 -17
- package/src/scenarios/envelope-reasoning-secret-redaction.test.ts +28 -26
- package/src/scenarios/envelope-reasoning-shape.test.ts +19 -17
- package/src/scenarios/envelope-recovery-applied.test.ts +24 -22
- package/src/scenarios/envelope-refusal-shape.test.ts +31 -29
- package/src/scenarios/envelope-rendering-hint.test.ts +5 -4
- package/src/scenarios/envelope-retry-attempted.test.ts +33 -31
- package/src/scenarios/envelope-retry-exhausted.test.ts +22 -20
- package/src/scenarios/envelope-tier-one-subset-static.test.ts +9 -7
- package/src/scenarios/envelope-truncated.test.ts +19 -17
- package/src/scenarios/envelope-truncation-cap-exhaustion.test.ts +19 -17
- package/src/scenarios/envelope-variant-discriminator-static.test.ts +4 -3
- package/src/scenarios/errors.test.ts +20 -19
- package/src/scenarios/eventOrdering.test.ts +16 -14
- package/src/scenarios/exec-not-protocol-tier.test.ts +5 -7
- package/src/scenarios/experimental-tier-shape.test.ts +17 -15
- package/src/scenarios/export-bundle-portability.test.ts +17 -17
- package/src/scenarios/failure-path.test.ts +7 -6
- package/src/scenarios/feedback-capability-shape.test.ts +4 -3
- package/src/scenarios/feedback-correction-redaction.test.ts +2 -1
- package/src/scenarios/feedback-cross-tenant-isolation.test.ts +2 -1
- package/src/scenarios/feedback-fork-not-copied.test.ts +3 -2
- package/src/scenarios/feedback-on-terminal-run.test.ts +3 -2
- package/src/scenarios/feedback-record-and-list.test.ts +2 -1
- package/src/scenarios/feedback-unsupported-501.test.ts +2 -1
- package/src/scenarios/fixtures-gating.test.ts +29 -28
- package/src/scenarios/fixtures-valid.test.ts +16 -15
- package/src/scenarios/form-content-instantiation.test.ts +15 -14
- package/src/scenarios/frontend-plugin-packs.test.ts +29 -27
- package/src/scenarios/fs-path-traversal.test.ts +14 -12
- package/src/scenarios/goal-standing-continuation.test.ts +21 -21
- package/src/scenarios/grpc-transport.test.ts +9 -7
- package/src/scenarios/heartbeat-capability-shape.test.ts +3 -3
- package/src/scenarios/heartbeat-fires-once-per-tick.test.ts +2 -2
- package/src/scenarios/heartbeat-idempotent-no-spam.test.ts +3 -3
- package/src/scenarios/heartbeat-runtime-bound.test.ts +2 -2
- package/src/scenarios/highConcurrency.test.ts +14 -13
- package/src/scenarios/host-callback-declaration.test.ts +8 -7
- package/src/scenarios/http-client-ssrf.test.ts +7 -5
- package/src/scenarios/i18n-negotiation.test.ts +14 -12
- package/src/scenarios/idempotency-concurrent-claim.test.ts +14 -13
- package/src/scenarios/idempotency-key-determinism.test.ts +20 -18
- package/src/scenarios/idempotency.test.ts +8 -7
- package/src/scenarios/idempotencyRetry.test.ts +13 -11
- package/src/scenarios/identity-owner-shape.test.ts +6 -5
- package/src/scenarios/identity-passthrough.test.ts +4 -3
- package/src/scenarios/interrupt-approval.test.ts +6 -5
- package/src/scenarios/interrupt-approver-routing.test.ts +15 -15
- package/src/scenarios/interrupt-auth-required-resume.test.ts +5 -3
- package/src/scenarios/interrupt-clarification.test.ts +4 -3
- package/src/scenarios/interrupt-external-event-correlation.test.ts +6 -5
- package/src/scenarios/interrupt-parent-child-cascade.test.ts +4 -3
- package/src/scenarios/interrupt-quorum-resolution.test.ts +5 -4
- package/src/scenarios/interrupt-token-matrix.test.ts +8 -6
- package/src/scenarios/interruptRace.test.ts +12 -10
- package/src/scenarios/kv-atomic-increment.test.ts +11 -9
- package/src/scenarios/kv-cas.test.ts +14 -12
- package/src/scenarios/kv-cross-tenant-isolation.test.ts +12 -10
- package/src/scenarios/kv-ttl-expiry.test.ts +7 -5
- package/src/scenarios/localized-content-delivery.test.ts +24 -26
- package/src/scenarios/maliciousManifest.test.ts +10 -8
- package/src/scenarios/mcp-2026-07-28-discover.test.ts +15 -14
- package/src/scenarios/mcp-cache-tenant-scope.test.ts +6 -5
- package/src/scenarios/mcp-current-auth-boundary.test.ts +7 -6
- package/src/scenarios/mcp-discoverability.test.ts +9 -7
- package/src/scenarios/mcp-extension-opacity.test.ts +7 -6
- package/src/scenarios/mcp-mrtr-roundtrip.test.ts +18 -17
- package/src/scenarios/mcp-server-elicitation-bridge.test.ts +13 -12
- package/src/scenarios/mcp-server-prompt-roundtrip.test.ts +11 -10
- package/src/scenarios/mcp-server-resource-roundtrip.test.ts +11 -10
- package/src/scenarios/mcp-server-sampling-bridge.test.ts +12 -11
- package/src/scenarios/mcp-server-tool-roundtrip.test.ts +14 -13
- package/src/scenarios/mcp-server-untrusted-args.test.ts +14 -13
- package/src/scenarios/mcp-stateless-request.test.ts +11 -10
- package/src/scenarios/mcp-tool-roundtrip.test.ts +4 -3
- package/src/scenarios/mcp-toolcall-redaction.test.ts +4 -3
- package/src/scenarios/mcp-version-negotiation.test.ts +15 -14
- package/src/scenarios/media-url-inline-cap.test.ts +21 -19
- package/src/scenarios/memory-attribution-emits-on-write.test.ts +5 -5
- package/src/scenarios/memory-attribution-no-content.test.ts +4 -4
- package/src/scenarios/memory-attribution-replay-stable.test.ts +4 -3
- package/src/scenarios/memory-attribution-shape.test.ts +4 -3
- package/src/scenarios/memory-attribution-tenant-scoped.test.ts +3 -3
- package/src/scenarios/memory-capability-model-shape.test.ts +14 -15
- package/src/scenarios/memory-compaction-event-emitted.test.ts +7 -6
- package/src/scenarios/memory-compaction-provenance-tag.test.ts +3 -2
- package/src/scenarios/memory-compaction-sr1-carry-forward.test.ts +7 -6
- package/src/scenarios/memory-consolidation-idempotent.test.ts +5 -4
- package/src/scenarios/memory-consolidation-shape.test.ts +9 -11
- package/src/scenarios/memory-degraded-projection.test.ts +10 -9
- package/src/scenarios/memory-injection-budget.test.ts +15 -14
- package/src/scenarios/metric-emission.test.ts +4 -3
- package/src/scenarios/model-capability-insufficient.test.ts +19 -17
- package/src/scenarios/model-capability-substituted.test.ts +20 -18
- package/src/scenarios/multi-agent-confidence-escalation.test.ts +12 -11
- package/src/scenarios/multi-agent-handoff-state-machine.test.ts +10 -9
- package/src/scenarios/multi-agent-memory-lifecycle.test.ts +11 -10
- package/src/scenarios/multi-node-ordering.test.ts +4 -3
- package/src/scenarios/multi-party-conversation-behavioral.test.ts +6 -6
- package/src/scenarios/multi-party-conversation-shape.test.ts +21 -23
- package/src/scenarios/multi-region-idempotency-behavior.test.ts +16 -15
- package/src/scenarios/multi-region-idempotency.test.ts +6 -5
- package/src/scenarios/node-failed-payload-shape.test.ts +5 -4
- package/src/scenarios/node-module-required-capabilities-shape.test.ts +10 -8
- package/src/scenarios/oauth-authorization-code-roundtrip.test.ts +5 -4
- package/src/scenarios/oauth-capability-shape.test.ts +5 -4
- package/src/scenarios/oauth-connector-redaction.test.ts +4 -3
- package/src/scenarios/openapi-resolved-paths.test.ts +6 -5
- package/src/scenarios/orchestratorConservativePath.test.ts +2 -1
- package/src/scenarios/orchestratorDispatch.test.ts +2 -1
- package/src/scenarios/orchestratorTermination.test.ts +2 -1
- package/src/scenarios/org-position-no-authority-escalation.test.ts +2 -2
- package/src/scenarios/otel-collector-canary-inspection.test.ts +8 -7
- package/src/scenarios/otel-emission-grpc.test.ts +3 -2
- package/src/scenarios/otel-emission.test.ts +6 -5
- package/src/scenarios/otel-trace-propagation-subworkflow.test.ts +7 -6
- package/src/scenarios/otel-trace-propagation.test.ts +2 -1
- package/src/scenarios/owner-subject-echo.test.ts +13 -12
- package/src/scenarios/owner-subject-shape.test.ts +11 -10
- package/src/scenarios/pack-manifest-extension-opacity.test.ts +8 -8
- package/src/scenarios/pack-registry-isolation.test.ts +4 -3
- package/src/scenarios/pack-registry-publish.test.ts +65 -63
- package/src/scenarios/pack-registry.test.ts +38 -36
- package/src/scenarios/pause-resume.test.ts +18 -16
- package/src/scenarios/policies.test.ts +20 -18
- package/src/scenarios/production-backpressure.test.ts +18 -16
- package/src/scenarios/production-retention-expiry.test.ts +8 -6
- package/src/scenarios/profile-discovery-core-alias.test.ts +13 -11
- package/src/scenarios/profileDerivation.test.ts +34 -33
- package/src/scenarios/prompt-all-four-kinds-events.test.ts +11 -9
- package/src/scenarios/prompt-composed-secret-redaction.test.ts +11 -9
- package/src/scenarios/prompt-composed-trust-marker.test.ts +10 -8
- package/src/scenarios/prompt-end-to-end-events.test.ts +17 -15
- package/src/scenarios/prompt-list-and-fetch.test.ts +16 -14
- package/src/scenarios/prompt-mutable-lifecycle.test.ts +13 -11
- package/src/scenarios/prompt-mutation-workspace-membership-enforced.test.ts +6 -4
- package/src/scenarios/prompt-pack-install.test.ts +14 -12
- package/src/scenarios/prompt-prefix-cache.test.ts +19 -17
- package/src/scenarios/prompt-read-workspace-membership-enforced.test.ts +11 -9
- package/src/scenarios/prompt-render-deterministic.test.ts +19 -17
- package/src/scenarios/prompt-resolution-chain-agent-intrinsic.test.ts +7 -5
- package/src/scenarios/prompt-resolution-chain-event.test.ts +12 -10
- package/src/scenarios/prompt-resolution-chain-fallback-cascade.test.ts +10 -8
- package/src/scenarios/prompt-resolution-chain-node-wins.test.ts +8 -6
- package/src/scenarios/prompt-template-shape.test.ts +30 -28
- package/src/scenarios/proposal-reviewable-learning.test.ts +19 -19
- package/src/scenarios/protocol-versions-array.test.ts +10 -9
- package/src/scenarios/provider-usage.test.ts +17 -15
- package/src/scenarios/providerPolicyEnforcement.test.ts +16 -14
- package/src/scenarios/purpose-propagation.test.ts +23 -21
- package/src/scenarios/queue-ack-nack-dlq.test.ts +12 -10
- package/src/scenarios/queue-cross-tenant-isolation.test.ts +8 -6
- package/src/scenarios/queue-publish-consume-roundtrip.test.ts +10 -8
- package/src/scenarios/rate-limit-envelope.test.ts +6 -5
- package/src/scenarios/redaction.test.ts +13 -11
- package/src/scenarios/redactionAdversarial.test.ts +11 -9
- package/src/scenarios/registry-public.test.ts +12 -11
- package/src/scenarios/replay-divergence-at-refusal.test.ts +16 -15
- package/src/scenarios/replay-fanout-suppression.test.ts +16 -15
- package/src/scenarios/replay-fork-arbitrary.test.ts +24 -23
- package/src/scenarios/replay-fork.test.ts +20 -19
- package/src/scenarios/replay-llm-cache-key-portable.test.ts +11 -9
- package/src/scenarios/replay-llm-cache-key.test.ts +22 -21
- package/src/scenarios/replay-observable-sequence-determinism.test.ts +4 -3
- package/src/scenarios/replay-retention-expiry.test.ts +11 -9
- package/src/scenarios/replay-side-effect-suppression.test.ts +21 -19
- package/src/scenarios/replayDeterminism.test.ts +13 -12
- package/src/scenarios/requirement-ledger.test.ts +15 -14
- package/src/scenarios/restart-during-run.test.ts +3 -2
- package/src/scenarios/route-coverage.test.ts +20 -19
- package/src/scenarios/run-diff.test.ts +22 -20
- package/src/scenarios/run-execution-bounds-shape.test.ts +14 -12
- package/src/scenarios/run-transport-economy.test.ts +13 -11
- package/src/scenarios/runner-ledger.test.ts +17 -16
- package/src/scenarios/runs-lifecycle.test.ts +7 -6
- package/src/scenarios/runtime-capabilities.test.ts +12 -10
- package/src/scenarios/runtime-requires-install-gate.test.ts +8 -8
- package/src/scenarios/runtime-requires-shape.test.ts +7 -6
- package/src/scenarios/safefetch-behavior.test.ts +22 -21
- package/src/scenarios/safefetch-live-audit.test.ts +14 -13
- package/src/scenarios/sandbox-memory-cap.test.ts +2 -1
- package/src/scenarios/sandbox-mvp-behavior.test.ts +24 -23
- package/src/scenarios/sandbox-no-host-fs-escape.test.ts +3 -2
- package/src/scenarios/sandbox-timeout-cap.test.ts +2 -1
- package/src/scenarios/sandbox-wasm-isolation.test.ts +14 -15
- package/src/scenarios/sandbox-wasm-timeout.test.ts +5 -6
- package/src/scenarios/scheduling-capability-shape.test.ts +8 -6
- package/src/scenarios/scheduling-cron-fires-once.test.ts +8 -6
- package/src/scenarios/search-bm25-roundtrip.test.ts +8 -6
- package/src/scenarios/secret-leakage-otel-attribute.test.ts +22 -20
- package/src/scenarios/self-hosted-runner.test.ts +26 -24
- package/src/scenarios/semantic-digest-vectors.test.ts +8 -7
- package/src/scenarios/speech-synthesis-roundtrip.test.ts +9 -7
- package/src/scenarios/speech-synthesis-unadvertised.test.ts +5 -3
- package/src/scenarios/sql-injection-rejection.test.ts +8 -6
- package/src/scenarios/sql-transaction-atomicity.test.ts +10 -8
- package/src/scenarios/staleClaim.test.ts +5 -4
- package/src/scenarios/stream-modes-buffer.test.ts +8 -7
- package/src/scenarios/stream-modes-mixed.test.ts +10 -9
- package/src/scenarios/stream-modes.test.ts +12 -11
- package/src/scenarios/stream-subscribe-from-beginning.test.ts +11 -9
- package/src/scenarios/stream-text-fixture.test.ts +14 -13
- package/src/scenarios/streamReconnect.test.ts +13 -11
- package/src/scenarios/strict-behavior-gate.test.ts +9 -8
- package/src/scenarios/subrun-approval-fail-closed.test.ts +2 -2
- package/src/scenarios/subrun-approval-gate.test.ts +3 -3
- package/src/scenarios/subrun-attestation-shape.test.ts +2 -2
- package/src/scenarios/subrun-checksum-stable.test.ts +4 -4
- package/src/scenarios/subworkflow-input-mapping.test.ts +16 -14
- package/src/scenarios/subworkflow.test.ts +10 -9
- package/src/scenarios/table-cross-tenant-isolation.test.ts +8 -6
- package/src/scenarios/table-cursor-pagination.test.ts +8 -6
- package/src/scenarios/table-schema-enforcement.test.ts +7 -5
- package/src/scenarios/test-seam-unauthenticated.test.ts +2 -2
- package/src/scenarios/tool-catalog-compact-projection.test.ts +10 -8
- package/src/scenarios/tool-catalog-projection.test.ts +11 -9
- package/src/scenarios/tool-descriptor-shape.test.ts +14 -15
- package/src/scenarios/tool-hooks-authorization-fail-closed.test.ts +3 -3
- package/src/scenarios/tool-hooks-content-free.test.ts +4 -4
- package/src/scenarios/tool-hooks-failure-honesty.test.ts +4 -4
- package/src/scenarios/tool-hooks-rate-limit.test.ts +2 -2
- package/src/scenarios/tool-hooks-secret-redaction.test.ts +2 -2
- package/src/scenarios/tool-hooks-shape.test.ts +3 -3
- package/src/scenarios/tool-session-lifecycle.test.ts +14 -13
- package/src/scenarios/trigger-bridge-delivery.test.ts +14 -14
- package/src/scenarios/trigger-bridge-shape.test.ts +19 -20
- package/src/scenarios/trigger-ingestion.test.ts +19 -17
- package/src/scenarios/trigger-stream-cdc-sources.test.ts +16 -14
- package/src/scenarios/v2-approver-enforced.test.ts +97 -0
- package/src/scenarios/v2-assurance-downgrade-audited.test.ts +75 -0
- package/src/scenarios/v2-bundle-v3-signed.test.ts +96 -0
- package/src/scenarios/v2-capabilities-root-closed.test.ts +63 -0
- package/src/scenarios/v2-capability-record-shape.test.ts +104 -0
- package/src/scenarios/v2-chain-pin-exact.test.ts +118 -0
- package/src/scenarios/v2-coherence-not-in-bundle.test.ts +98 -0
- package/src/scenarios/v2-compensation-read-projection.test.ts +85 -0
- package/src/scenarios/v2-configurable-closed.test.ts +67 -0
- package/src/scenarios/v2-dual-stack-negotiation.test.ts +118 -0
- package/src/scenarios/v2-effect-identity-business-key.test.ts +132 -0
- package/src/scenarios/v2-effect-seam-manifest.test.ts +163 -0
- package/src/scenarios/v2-enum-growth-rule.test.ts +69 -0
- package/src/scenarios/v2-era-key.test.ts +86 -0
- package/src/scenarios/v2-error-registry.test.ts +89 -0
- package/src/scenarios/v2-event-type-closed.test.ts +97 -0
- package/src/scenarios/v2-fork-a-v1-run.test.ts +109 -0
- package/src/scenarios/v2-form-when-reuses-edge-conditions.test.ts +70 -0
- package/src/scenarios/v2-header-scheme.test.ts +67 -0
- package/src/scenarios/v2-id-grammar.test.ts +77 -0
- package/src/scenarios/v2-idempotency-key-grammar.test.ts +65 -0
- package/src/scenarios/v2-interrupt-token-scheme.test.ts +55 -0
- package/src/scenarios/v2-lane-issuer-advertised.test.ts +62 -0
- package/src/scenarios/v2-legacy-profiles-absent.test.ts +78 -0
- package/src/scenarios/v2-manifest-ceiling-refused.test.ts +130 -0
- package/src/scenarios/v2-manifest-hatch-carried.test.ts +120 -0
- package/src/scenarios/v2-min-client-version.test.ts +48 -0
- package/src/scenarios/v2-minimum-version-refused.test.ts +141 -0
- package/src/scenarios/v2-mrtr-rounds-ceiling.test.ts +95 -0
- package/src/scenarios/v2-negotiation-authenticated.test.ts +125 -0
- package/src/scenarios/v2-negotiation-decided-emitted.test.ts +149 -0
- package/src/scenarios/v2-no-transport-advertisement.test.ts +57 -0
- package/src/scenarios/v2-owner-subject-required.test.ts +72 -0
- package/src/scenarios/v2-pack-isolation.test.ts +147 -0
- package/src/scenarios/v2-payload-registry-closed.test.ts +110 -0
- package/src/scenarios/v2-peer-dependency-declared.test.ts +132 -0
- package/src/scenarios/v2-pinned-run-disposition.test.ts +124 -0
- package/src/scenarios/v2-poll-cursor-v2.test.ts +114 -0
- package/src/scenarios/v2-preferred-version-default.test.ts +83 -0
- package/src/scenarios/v2-profiles-derived-only.test.ts +70 -0
- package/src/scenarios/v2-provider-conflict.test.ts +115 -0
- package/src/scenarios/v2-refresh-sla.test.ts +78 -0
- package/src/scenarios/v2-relaxation-recorded.test.ts +134 -0
- package/src/scenarios/v2-revocation-honored.test.ts +66 -0
- package/src/scenarios/v2-subject-link-record.test.ts +102 -0
- package/src/scenarios/v2-unmapped-type-refused.test.ts +95 -0
- package/src/scenarios/v2-v1-events-translated.test.ts +105 -0
- package/src/scenarios/v2-v1-signed-webhook-accepted.test.ts +109 -0
- package/src/scenarios/v2-webhook-durable-delivery.test.ts +238 -0
- package/src/scenarios/v2-well-known-one-resource.test.ts +129 -0
- package/src/scenarios/vector-knn-roundtrip.test.ts +9 -7
- package/src/scenarios/verifier-gating.test.ts +8 -6
- package/src/scenarios/version-fold.test.ts +9 -8
- package/src/scenarios/version-negotiation.test.ts +12 -11
- package/src/scenarios/voice-bargein-no-partial-leak.test.ts +8 -6
- package/src/scenarios/voice-event-payloads-shape.test.ts +10 -11
- package/src/scenarios/voice-interim-not-durable.test.ts +7 -5
- package/src/scenarios/voice-streamref-tenant-bound.test.ts +5 -3
- package/src/scenarios/voice-synthesis-streaming.test.ts +11 -9
- package/src/scenarios/voice-transcription-streaming.test.ts +8 -6
- package/src/scenarios/voice-transcription-unadvertised.test.ts +5 -3
- package/src/scenarios/wasm-pack-abi-version-rejection.test.ts +3 -2
- 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 +5 -4
- package/src/scenarios/wasm-pack-replay-determinism.test.ts +2 -1
- package/src/scenarios/webhook-negative.test.ts +11 -9
- package/src/scenarios/webhook-receiver-adversarial.test.ts +8 -7
- package/src/scenarios/webhook-sig-algorithm.test.ts +5 -3
- package/src/scenarios/webhook-signed-delivery.test.ts +16 -15
- package/src/scenarios/webhook-tenant-isolation.test.ts +16 -14
- package/src/scenarios/workflow-chain-deferred-parameters.test.ts +28 -30
- package/src/scenarios/workflow-chain-expansion.test.ts +21 -20
- package/src/scenarios/workflow-chain-host-expansion.test.ts +21 -20
- package/src/scenarios/workflow-chain-pack-manifest-validation.test.ts +14 -12
- package/src/scenarios/workflow-chain-pack-signature-verification.test.ts +5 -4
- package/src/scenarios/workflow-chain-unresolvable-typeid.test.ts +9 -8
- package/src/scenarios/workflow-primary-output-annotation.test.ts +7 -6
- package/src/scenarios/workflow-variable-format.test.ts +24 -24
- package/src/scenarios/workload-identity-behavior.test.ts +15 -13
- package/src/scenarios/workload-identity-chain-bounds.test.ts +18 -17
- package/src/scenarios/workspace-behavior.test.ts +19 -18
- package/src/scenarios/workspace-capability-shape.test.ts +3 -2
- package/src/scenarios/workspace-cross-tenant-isolation-blackbox.test.ts +6 -5
- package/src/scenarios/workspace-cross-tenant-isolation.test.ts +7 -6
- package/src/scenarios/x-openwop-form-pack-manifest.test.ts +8 -7
- package/src/setup.ts +44 -6
- package/vitest.config.ts +5 -1
- package/api/.redocly.lint-ignore.yaml +0 -22
- package/api/asyncapi.yaml +0 -834
- package/api/grpc/openwop.proto +0 -251
- package/api/openapi.yaml +0 -2693
- package/api/redocly.yaml +0 -23
- package/schemas/README.md +0 -144
- package/schemas/a2a-task-state.schema.json +0 -78
- package/schemas/a2ui-surface-delta-frame.schema.json +0 -48
- package/schemas/agent-deployment-transition.schema.json +0 -49
- package/schemas/agent-deployment.schema.json +0 -54
- package/schemas/agent-eval-suite.schema.json +0 -140
- package/schemas/agent-inventory-response.schema.json +0 -115
- package/schemas/agent-manifest.schema.json +0 -156
- package/schemas/agent-org-chart.schema.json +0 -82
- package/schemas/agent-ref.schema.json +0 -63
- package/schemas/agent-roster-entry.schema.json +0 -81
- package/schemas/agent-roster-response.schema.json +0 -21
- package/schemas/ai-envelope.schema.json +0 -134
- package/schemas/annotation-create.schema.json +0 -37
- package/schemas/annotation.schema.json +0 -56
- package/schemas/artifact-type-pack-manifest.schema.json +0 -244
- package/schemas/audit-verify-result.schema.json +0 -90
- package/schemas/budget-policy.schema.json +0 -18
- package/schemas/capabilities.schema.json +0 -4160
- package/schemas/certification-bundle-v2.schema.json +0 -118
- package/schemas/channel-presence-payload.schema.json +0 -41
- package/schemas/channel-written-payload.schema.json +0 -55
- package/schemas/chat-card-pack-manifest.schema.json +0 -253
- package/schemas/compact-tool-descriptor.schema.json +0 -51
- package/schemas/compensation-policy.schema.json +0 -64
- package/schemas/conformance-certification-bundle.schema.json +0 -86
- package/schemas/connection-pack-manifest.schema.json +0 -353
- package/schemas/conversation-event.schema.json +0 -168
- package/schemas/conversation-turn.schema.json +0 -117
- package/schemas/core-conformance-mock-agent-config.schema.json +0 -152
- package/schemas/credential-provenance.schema.json +0 -18
- package/schemas/credential-reference.schema.json +0 -21
- package/schemas/debug-bundle.schema.json +0 -196
- package/schemas/dispatch-config.schema.json +0 -112
- package/schemas/envelopes/clarification.request.schema.json +0 -52
- package/schemas/envelopes/error.schema.json +0 -30
- package/schemas/envelopes/media.audio.schema.json +0 -38
- package/schemas/envelopes/media.file.schema.json +0 -37
- package/schemas/envelopes/media.image.schema.json +0 -33
- package/schemas/envelopes/schema.request.schema.json +0 -26
- package/schemas/envelopes/schema.response.schema.json +0 -22
- package/schemas/envelopes/ui.a2ui-surface.schema.json +0 -154
- package/schemas/error-envelope.schema.json +0 -25
- package/schemas/eval-summary.schema.json +0 -92
- package/schemas/export-bundle.schema.json +0 -66
- package/schemas/form-content-pack-manifest.schema.json +0 -243
- package/schemas/frontend-plugin-manifest.schema.json +0 -109
- package/schemas/goal.schema.json +0 -104
- package/schemas/heartbeat-evaluated.schema.json +0 -14
- package/schemas/heartbeat-state-changed.schema.json +0 -14
- package/schemas/localized-content-language-settings.schema.json +0 -26
- package/schemas/localized-content-page-response.schema.json +0 -60
- package/schemas/localized-content-page.schema.json +0 -62
- package/schemas/localized-content-section.schema.json +0 -51
- package/schemas/memory-entry.schema.json +0 -36
- package/schemas/memory-list-options.schema.json +0 -37
- package/schemas/node-pack-manifest.schema.json +0 -654
- package/schemas/orchestrator-decision.schema.json +0 -90
- package/schemas/org-chart-responsibility-view.schema.json +0 -26
- package/schemas/pack-lockfile.schema.json +0 -108
- package/schemas/prompt-kind.schema.json +0 -8
- package/schemas/prompt-pack-manifest.schema.json +0 -117
- package/schemas/prompt-ref.schema.json +0 -40
- package/schemas/prompt-template.schema.json +0 -149
- package/schemas/proposal.schema.json +0 -84
- package/schemas/registry-version-manifest.schema.json +0 -208
- package/schemas/residency.schema.json +0 -16
- package/schemas/run-ancestry-response.schema.json +0 -54
- package/schemas/run-diff-response.schema.json +0 -64
- package/schemas/run-event-payloads.schema.json +0 -4452
- package/schemas/run-event.schema.json +0 -211
- package/schemas/run-options.schema.json +0 -80
- package/schemas/run-orchestrator-decided-event.schema.json +0 -20
- package/schemas/run-snapshot.schema.json +0 -148
- package/schemas/security-advisory.schema.json +0 -109
- package/schemas/self-hosted-runner-dispatch-frame.schema.json +0 -79
- package/schemas/self-hosted-runner-registration.schema.json +0 -53
- package/schemas/self-hosted-runner-result-frame.schema.json +0 -38
- package/schemas/subject.schema.json +0 -56
- package/schemas/suspend-request.schema.json +0 -207
- package/schemas/tool-descriptor.schema.json +0 -63
- package/schemas/trigger-event.schema.json +0 -222
- package/schemas/trigger-subscription-registration.schema.json +0 -67
- package/schemas/trigger-subscription.schema.json +0 -26
- package/schemas/ui-plugin-message.schema.json +0 -90
- package/schemas/workflow-chain-pack-manifest.schema.json +0 -615
- package/schemas/workflow-definition.schema.json +0 -850
- package/schemas/workload-identity.schema.json +0 -79
- package/schemas/workspace-file-create.schema.json +0 -20
- package/schemas/workspace-file.schema.json +0 -39
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0173 §B — `effect-identity-business-key` (suite 2.0.0, target major 2; gated on `idempotency`).
|
|
3
|
+
*
|
|
4
|
+
* Layer-2 effect identity is a core obligation keyed on business identity: a
|
|
5
|
+
* host that advertises `idempotency` assigns a logical effect id once per
|
|
6
|
+
* effect, stable across transport retries, injects it as the provider's
|
|
7
|
+
* idempotency key, and serves `GET /runs/{runId}/effects`
|
|
8
|
+
* (`schemas/v2/effect-ledger-projection.schema.json`) — each row
|
|
9
|
+
* `{ effectId, nodeId, attempt, keying: business-identity | activity-recipe,
|
|
10
|
+
* state, at }`, content-free of provider payloads (RFC 0150 §B; RFC 0173 §B row
|
|
11
|
+
* C6.7; `spec/v2/core/security-defaults.md` §Layer-2 effect identity).
|
|
12
|
+
*
|
|
13
|
+
* Legs:
|
|
14
|
+
* 1. the ledger read validates on a run of the noop fixture and every row's
|
|
15
|
+
* `keying` is one of the two documented modes;
|
|
16
|
+
* 2. the "same provider key across two transport retries" leg needs the
|
|
17
|
+
* suite's fixture provider (RFC 0173 §D.2 G4 — a provider that rejects a
|
|
18
|
+
* changed key) driven through the seams profile; no such seam is
|
|
19
|
+
* catalogued, so that leg records `blocked` naming it.
|
|
20
|
+
*
|
|
21
|
+
* @see spec/v2/core/security-defaults.md §Layer-2 effect identity
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { describe, it, expect } from 'vitest';
|
|
25
|
+
import { driver } from '../lib/driver.js';
|
|
26
|
+
import { v2Discovery, gateFamily, v2Validator } from '../lib/v2.js';
|
|
27
|
+
import { seamsProfileAdvertised, SEAMS_PREFIX } from '../lib/seams.js';
|
|
28
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
29
|
+
import { req } from '../lib/requirement-ids.js';
|
|
30
|
+
|
|
31
|
+
const FIXTURE = 'conformance-noop';
|
|
32
|
+
const KEYING = ['business-identity', 'activity-recipe'];
|
|
33
|
+
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
34
|
+
|
|
35
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
36
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function waitTerminal(runId: string, timeoutMs: number): Promise<void> {
|
|
40
|
+
const deadline = Date.now() + timeoutMs;
|
|
41
|
+
while (Date.now() < deadline) {
|
|
42
|
+
const res = await driver.get(`/runs/${encodeURIComponent(runId)}`);
|
|
43
|
+
if (res.status === 200 && TERMINAL.has(String((res.json as { status?: unknown } | null)?.status))) return;
|
|
44
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
describe('RFC 0173 §B — effect-identity-business-key (gated on idempotency)', () => {
|
|
49
|
+
it('GET /runs/{runId}/effects validates and every row is keyed on business identity or the activity recipe', async () => {
|
|
50
|
+
const doc = await discovery();
|
|
51
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
52
|
+
if (!(await gateFamily('idempotency'))) return softSkip('inapplicable', 'idempotency family not advertised — no Layer-2 obligation (gate recorded under openwop.family.idempotency)');
|
|
53
|
+
const fixtures = Array.isArray(doc['fixtures']) ? (doc['fixtures'] as unknown[]) : [];
|
|
54
|
+
if (!fixtures.includes(FIXTURE)) return softSkip('inapplicable', `${FIXTURE} fixture not advertised — no run to read`);
|
|
55
|
+
|
|
56
|
+
const create = await driver.post('/runs', { workflowId: FIXTURE });
|
|
57
|
+
expect(create.status, req('openwop.requirement.0173.effect-identity-business-key', 'runs.md §Create', 'POST /runs MUST answer 201 for the noop fixture')).toBe(201);
|
|
58
|
+
const runId = (create.json as { runId: string }).runId;
|
|
59
|
+
await waitTerminal(runId, 10_000);
|
|
60
|
+
|
|
61
|
+
const res = await driver.get(`/runs/${encodeURIComponent(runId)}/effects`);
|
|
62
|
+
expect(
|
|
63
|
+
res.status,
|
|
64
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'security-defaults.md §Layer-2 effect identity', 'a host advertising `idempotency` MUST serve GET /runs/{runId}/effects with 200 (RFC 0173 §B)'),
|
|
65
|
+
).toBe(200);
|
|
66
|
+
const check = v2Validator('effect-ledger-projection')(res.json);
|
|
67
|
+
expect(
|
|
68
|
+
check.ok,
|
|
69
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json', `the ledger projection MUST validate: ${check.errors}`),
|
|
70
|
+
).toBe(true);
|
|
71
|
+
const body = res.json as { runId?: unknown; effects?: Array<{ effectId?: unknown; keying?: unknown; providerKey?: unknown }> };
|
|
72
|
+
expect(body.runId, req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json runId', 'runId MUST echo the run read')).toBe(runId);
|
|
73
|
+
const effects = body.effects ?? [];
|
|
74
|
+
const ids = new Set<string>();
|
|
75
|
+
for (const e of effects) {
|
|
76
|
+
expect(
|
|
77
|
+
KEYING,
|
|
78
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'security-defaults.md §Layer-2 effect identity', `keying MUST be business-identity or activity-recipe (the documented fallback) — effect ${String(e.effectId)} declares ${String(e.keying)}`),
|
|
79
|
+
).toContain(e.keying);
|
|
80
|
+
// One logical effect id per effect: a duplicate row is a re-assignment.
|
|
81
|
+
expect(
|
|
82
|
+
ids.has(String(e.effectId)),
|
|
83
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'RFC 0150 §B', `effectId ${String(e.effectId)} MUST be assigned once per effect (duplicate ledger row)`),
|
|
84
|
+
).toBe(false);
|
|
85
|
+
ids.add(String(e.effectId));
|
|
86
|
+
if (typeof e.providerKey === 'string') {
|
|
87
|
+
expect(
|
|
88
|
+
/(secret|bearer |sk-[a-z0-9]{8,})/i.test(e.providerKey),
|
|
89
|
+
req('openwop.requirement.0173.effect-identity-business-key', 'effect-ledger-projection.schema.json providerKey', 'providerKey is a redaction-safe identity, never credential material'),
|
|
90
|
+
).toBe(false);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
if (effects.length === 0) softSkip('inapplicable', 'the noop fixture issued no external effect — the per-row keying leg had no rows (an effect-issuing fixture would exercise it)');
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
it('the same provider key is presented across two transport retries', async () => {
|
|
97
|
+
const doc = await discovery();
|
|
98
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
99
|
+
if (!(await gateFamily('idempotency'))) return softSkip('inapplicable', 'idempotency family not advertised — no Layer-2 obligation (gate recorded under openwop.family.idempotency)');
|
|
100
|
+
if (!seamsProfileAdvertised(doc)) return softSkip('blocked', 'the retry leg is driven through the suite fixture provider (RFC 0173 §D.2 G4) under the seams profile — seams profile not advertised');
|
|
101
|
+
// The seam is catalogued (api/seams-v2.yaml `forceEffectTransportRetry`). An
|
|
102
|
+
// unreachable providerUrl forces the transport retry; the witness is the
|
|
103
|
+
// host's own Layer-2 ledger (RFC 0173 §C.2), where every attempt of one
|
|
104
|
+
// effect MUST carry the same business-identity key.
|
|
105
|
+
const fired = await driver.post(`${SEAMS_PREFIX}/sample/test/idempotency/effect-retry`, { providerUrl: 'http://127.0.0.1:1/' });
|
|
106
|
+
if (fired.status === 404 || fired.status === 403 || fired.status === 405) {
|
|
107
|
+
return softSkip('blocked', `the host advertises the seams profile but does not serve ${SEAMS_PREFIX}/sample/test/idempotency/effect-retry (answered ${fired.status}) — the cross-retry keying leg cannot be driven`);
|
|
108
|
+
}
|
|
109
|
+
const body = fired.json as { runId?: unknown; effectId?: unknown } | null;
|
|
110
|
+
if (fired.status !== 201 || typeof body?.runId !== 'string' || typeof body?.effectId !== 'string') {
|
|
111
|
+
return softSkip('blocked', `${SEAMS_PREFIX}/sample/test/idempotency/effect-retry answered ${fired.status} without { runId, effectId } — the seam contract in api/seams-v2.yaml is 201 { runId, effectId }`);
|
|
112
|
+
}
|
|
113
|
+
const ledger = await driver.get(`/runs/${body.runId}/effects`);
|
|
114
|
+
if (ledger.status !== 200) return softSkip('blocked', `GET /runs/{runId}/effects answered ${ledger.status} — the ledger is the witness for cross-retry keying`);
|
|
115
|
+
const all = ((ledger.json as { effects?: unknown } | null)?.effects ?? []) as Array<Record<string, unknown>>;
|
|
116
|
+
const attempts = all.filter((e) => e['effectId'] === body.effectId);
|
|
117
|
+
if (attempts.length < 2) {
|
|
118
|
+
return softSkip('blocked', `the seam produced ${attempts.length} ledger row(s) for effect ${String(body.effectId)} — a cross-retry assertion needs at least two attempts`);
|
|
119
|
+
}
|
|
120
|
+
const keys = new Set(attempts.map((e) => String(e['providerKey'] ?? '')));
|
|
121
|
+
expect(
|
|
122
|
+
keys.size,
|
|
123
|
+
req('openwop.requirement.0173.effect-identity-business-key.retry', 'spec/v2/core/replay.md §Effect identity', `every attempt of one effect MUST present the same provider key across a transport retry — ${attempts.length} attempt(s) presented ${keys.size} distinct key(s)`),
|
|
124
|
+
).toBe(1);
|
|
125
|
+
for (const a of attempts) {
|
|
126
|
+
expect(
|
|
127
|
+
a['keying'],
|
|
128
|
+
req('openwop.requirement.0173.effect-identity-business-key.retry', 'spec/v2/core/replay.md §Effect identity', `a Layer-2 host keys a retried effect on business identity, not the activity recipe (attempt ${String(a['attempt'])})`),
|
|
129
|
+
).toBe('business-identity');
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0173 §C.1 — `effect-seam-manifest` (suite 2.0.0, target major 2; gated on `replay`).
|
|
3
|
+
*
|
|
4
|
+
* A v2 host that advertises `replay` MUST publish the effect-seam manifest at
|
|
5
|
+
* `GET /host/effect-seams` (`api/v2/openapi.yaml` `getEffectSeamManifest`,
|
|
6
|
+
* `schemas/v2/effect-seam-manifest.schema.json`): one row per outbound effect
|
|
7
|
+
* path the node runtime can reach, every row `guarded: true` with a
|
|
8
|
+
* `guardedBy`. The `replay` facet names the address as the constant
|
|
9
|
+
* `effectSeamsManifest: "/host/effect-seams"` (`spec/v2/facets/replay.schema.json`).
|
|
10
|
+
*
|
|
11
|
+
* Legs:
|
|
12
|
+
* 1. the manifest validates; every row is guarded; at least one row exists
|
|
13
|
+
* for every `kind` the host declares (the manifest is the host's own
|
|
14
|
+
* enumeration — RFC 0140 G7 answered by making the host list it);
|
|
15
|
+
* 2. the facet constant equals the served address;
|
|
16
|
+
* 3. driving one seam of each kind through the suite's receiver and
|
|
17
|
+
* observing no re-fire needs a seam that fires a named manifest row inside
|
|
18
|
+
* a run — none is catalogued (`host-sample-test-seams.md`,
|
|
19
|
+
* `api/seams-v2.yaml`), so that leg records `blocked` naming it.
|
|
20
|
+
*
|
|
21
|
+
* Completeness of the manifest (a seam outside it) is negative-existence and is
|
|
22
|
+
* not asserted here (RFC 0173 falsifiability table, §C.1 row).
|
|
23
|
+
*
|
|
24
|
+
* @see spec/v2/core/replay.md §The effect-seam manifest
|
|
25
|
+
* @see spec/v2/core/security-defaults.md §Replay suppression
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { describe, it, expect } from 'vitest';
|
|
29
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
30
|
+
import { v2Discovery, gateFamily, v2Validator } from '../lib/v2.js';
|
|
31
|
+
import { seamsProfileAdvertised, SEAMS_PREFIX } from '../lib/seams.js';
|
|
32
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
33
|
+
import { req } from '../lib/requirement-ids.js';
|
|
34
|
+
|
|
35
|
+
const MANIFEST_PATH = '/host/effect-seams';
|
|
36
|
+
const KINDS = ['http', 'queue', 'storage', 'provider-sdk', 'webhook-fanout'] as const;
|
|
37
|
+
|
|
38
|
+
interface SeamRow { seam?: unknown; kind?: unknown; guarded?: unknown; guardedBy?: unknown; branchReFires?: unknown }
|
|
39
|
+
interface Manifest { manifestVersion?: unknown; seams?: SeamRow[] }
|
|
40
|
+
|
|
41
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
42
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function manifest(): Promise<{ status: number; body: Manifest | null }> {
|
|
46
|
+
const res = await driver.get(MANIFEST_PATH);
|
|
47
|
+
return { status: res.status, body: res.status === 200 && res.json && typeof res.json === 'object' ? (res.json as Manifest) : null };
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
describe('RFC 0173 §C.1 — effect-seam-manifest (gated on replay)', () => {
|
|
51
|
+
it('GET /host/effect-seams validates and every row is guarded', async () => {
|
|
52
|
+
const doc = await discovery();
|
|
53
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
54
|
+
const replay = await gateFamily('replay');
|
|
55
|
+
if (!replay) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay)');
|
|
56
|
+
|
|
57
|
+
const { status, body } = await manifest();
|
|
58
|
+
expect(
|
|
59
|
+
status,
|
|
60
|
+
req('openwop.requirement.0173.effect-seam-manifest', 'replay.md §The effect-seam manifest', 'a host advertising `replay` MUST serve GET /host/effect-seams with 200 (RFC 0173 §C.1)'),
|
|
61
|
+
).toBe(200);
|
|
62
|
+
const check = v2Validator('effect-seam-manifest')(body);
|
|
63
|
+
expect(
|
|
64
|
+
check.ok,
|
|
65
|
+
req('openwop.requirement.0173.effect-seam-manifest', 'effect-seam-manifest.schema.json', `the manifest MUST validate against schemas/v2/effect-seam-manifest.schema.json: ${check.errors}`),
|
|
66
|
+
).toBe(true);
|
|
67
|
+
const rows = body?.seams ?? [];
|
|
68
|
+
expect(
|
|
69
|
+
rows.length,
|
|
70
|
+
req('openwop.requirement.0173.effect-seam-manifest', 'replay.md §The effect-seam manifest', 'the manifest MUST enumerate at least one outbound effect seam — a node runtime with no effect path does not need `replay`'),
|
|
71
|
+
).toBeGreaterThan(0);
|
|
72
|
+
for (const row of rows) {
|
|
73
|
+
expect(
|
|
74
|
+
row.guarded,
|
|
75
|
+
req('openwop.requirement.0173.effect-seam-manifest', 'RFC 0173 §C.1', `every manifest row MUST be guarded: true — seam ${String(row.seam)} (kind ${String(row.kind)})`),
|
|
76
|
+
).toBe(true);
|
|
77
|
+
expect(
|
|
78
|
+
typeof row.guardedBy === 'string' && row.guardedBy.length > 0,
|
|
79
|
+
req('openwop.requirement.0173.effect-seam-manifest', 'RFC 0173 §C.1', `every row MUST name the mechanism that records the outcome instead of re-firing (guardedBy) — seam ${String(row.seam)}`),
|
|
80
|
+
).toBe(true);
|
|
81
|
+
expect(
|
|
82
|
+
(KINDS as readonly string[]).includes(String(row.kind)),
|
|
83
|
+
req('openwop.requirement.0173.effect-seam-manifest', 'effect-seam-manifest.schema.json seams[].kind', `kind MUST be one of ${KINDS.join(' | ')} — seam ${String(row.seam)} declares ${String(row.kind)}`),
|
|
84
|
+
).toBe(true);
|
|
85
|
+
}
|
|
86
|
+
// One row per kind the host declares: the set of kinds the manifest names is
|
|
87
|
+
// the host's declaration, and each named kind is backed by at least one row.
|
|
88
|
+
const declared = new Set(rows.map((r) => String(r.kind)));
|
|
89
|
+
for (const kind of declared) {
|
|
90
|
+
expect(
|
|
91
|
+
rows.filter((r) => r.kind === kind).length,
|
|
92
|
+
req('openwop.requirement.0173.effect-seam-manifest', 'RFC 0173 §C.1', `at least one manifest row per declared kind (${kind})`),
|
|
93
|
+
).toBeGreaterThanOrEqual(1);
|
|
94
|
+
}
|
|
95
|
+
// RFC 0140 G6: a `branch` re-fire is stated as a permission, and a permission
|
|
96
|
+
// for a mode the host does not offer is a claim about nothing.
|
|
97
|
+
const modes = Array.isArray(replay['modes']) ? (replay['modes'] as unknown[]) : [];
|
|
98
|
+
for (const row of rows.filter((r) => r.branchReFires === true)) {
|
|
99
|
+
expect(
|
|
100
|
+
modes,
|
|
101
|
+
req('openwop.requirement.0173.effect-seam-manifest', 'effect-seam-manifest.schema.json seams[].branchReFires', `seam ${String(row.seam)} states branchReFires: true, so the replay facet MUST offer the branch mode it refers to`),
|
|
102
|
+
).toContain('branch');
|
|
103
|
+
}
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('the replay facet names the manifest address as the constant /host/effect-seams', async () => {
|
|
107
|
+
const doc = await discovery();
|
|
108
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
109
|
+
const replay = await gateFamily('replay');
|
|
110
|
+
if (!replay) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay)');
|
|
111
|
+
expect(
|
|
112
|
+
replay['effectSeamsManifest'],
|
|
113
|
+
req('openwop.requirement.0173.effect-seam-manifest.facet', 'facets/replay.schema.json effectSeamsManifest', 'the facet MUST carry the constant "/host/effect-seams" (replay.md §The surface)'),
|
|
114
|
+
).toBe(MANIFEST_PATH);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it('driving one seam of each kind observes no re-fire on replay', async () => {
|
|
118
|
+
const doc = await discovery();
|
|
119
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
120
|
+
const replay = await gateFamily('replay');
|
|
121
|
+
if (!replay) return softSkip('inapplicable', 'replay family not advertised (gate recorded under openwop.family.replay)');
|
|
122
|
+
if (!seamsProfileAdvertised(doc)) {
|
|
123
|
+
return softSkip('blocked', `the no-re-fire leg is seam-gated (RFC 0173 falsifiability §B replay row is witnessable-gated only through a seam that fires a named manifest row inside a run) — seams profile not advertised (conformance.seamsProfile)`);
|
|
124
|
+
}
|
|
125
|
+
// The seam is catalogued (api/seams-v2.yaml `fireEffectSeam`). The witness is
|
|
126
|
+
// the host's own Layer-2 ledger, not an externally reachable receiver: fire a
|
|
127
|
+
// guarded row that states `branchReFires: false`, fork the run in `replay`
|
|
128
|
+
// mode, and read GET /runs/{runId}/effects on the fork. Suppression means the
|
|
129
|
+
// fork issues no further attempt for that seam, so its effect ledger cannot
|
|
130
|
+
// grow past the parent's.
|
|
131
|
+
const manifest = await driver.get(MANIFEST_PATH);
|
|
132
|
+
const seamRows: SeamRow[] = manifest.status === 200 && manifest.json && typeof manifest.json === 'object'
|
|
133
|
+
? ((manifest.json as Manifest).seams ?? []) : [];
|
|
134
|
+
const target = seamRows.find((r) => r.guarded === true && r.branchReFires === false);
|
|
135
|
+
if (!target) return softSkip('inapplicable', `no manifest row is both guarded and branchReFires: false — nothing to witness suppression on (${seamRows.length} row(s) at ${MANIFEST_PATH})`);
|
|
136
|
+
const fired = await driver.post(`${SEAMS_PREFIX}/sample/effect-seams/fire`, { seam: String(target.seam) });
|
|
137
|
+
if (fired.status === 404 || fired.status === 403 || fired.status === 405) {
|
|
138
|
+
return softSkip('blocked', `the host advertises the seams profile but does not serve ${SEAMS_PREFIX}/sample/effect-seams/fire (answered ${fired.status}) — the no-re-fire leg cannot be driven`);
|
|
139
|
+
}
|
|
140
|
+
const firedBody = fired.json as { runId?: unknown } | null;
|
|
141
|
+
if (fired.status !== 201 || typeof firedBody?.runId !== 'string') {
|
|
142
|
+
return softSkip('blocked', `${SEAMS_PREFIX}/sample/effect-seams/fire answered ${fired.status} without { runId } — the seam contract in api/seams-v2.yaml is 201 { runId }`);
|
|
143
|
+
}
|
|
144
|
+
const parentId = firedBody.runId;
|
|
145
|
+
const parentEffects = await driver.get(`/runs/${parentId}/effects`);
|
|
146
|
+
if (parentEffects.status !== 200) return softSkip('blocked', `GET /runs/{runId}/effects answered ${parentEffects.status} on the fired run — the ledger is the witness for suppression (RFC 0173 §C.2)`);
|
|
147
|
+
const countOf = (r: OpenWOPResponse): number => {
|
|
148
|
+
const b = r.json as { effects?: unknown } | null;
|
|
149
|
+
return Array.isArray(b?.effects) ? (b.effects as unknown[]).length : 0;
|
|
150
|
+
};
|
|
151
|
+
const forked = await driver.post(`/runs/${parentId}:fork`, { mode: 'replay' });
|
|
152
|
+
const forkBody = forked.json as { runId?: unknown } | null;
|
|
153
|
+
if (forked.status !== 201 || typeof forkBody?.runId !== 'string') {
|
|
154
|
+
return softSkip('blocked', `POST /runs/{runId}:fork mode replay answered ${forked.status} on the fired run — suppression is witnessed on the fork`);
|
|
155
|
+
}
|
|
156
|
+
const forkEffects = await driver.get(`/runs/${String(forkBody.runId)}/effects`);
|
|
157
|
+
if (forkEffects.status !== 200) return softSkip('blocked', `GET /runs/{runId}/effects answered ${forkEffects.status} on the replay fork`);
|
|
158
|
+
expect(
|
|
159
|
+
countOf(forkEffects),
|
|
160
|
+
req('openwop.requirement.0173.effect-seam-manifest.no-re-fire', 'spec/v2/core/replay.md §The effect-seam manifest', `seam ${String(target.seam)} states branchReFires: false, so a replay fork MUST NOT issue a further attempt through it — the fork's effect ledger (${countOf(forkEffects)}) cannot exceed the parent's (${countOf(parentEffects)})`),
|
|
161
|
+
).toBeLessThanOrEqual(countOf(parentEffects));
|
|
162
|
+
});
|
|
163
|
+
});
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — `enum-growth-rule` (suite 2.0.0; RFC 0171 §A.1, §A.5;
|
|
3
|
+
* `spec/v2/core/events.md` §"Types" — Growth; `overview.md` §0).
|
|
4
|
+
*
|
|
5
|
+
* Witness class: witnessable — unaided. A registry-backed enum grows by adding
|
|
6
|
+
* a row and regenerating; a consumer MUST accept an unknown registered member
|
|
7
|
+
* and MUST NOT act on it, and a producer MUST NOT emit an unregistered member.
|
|
8
|
+
* The schema half is server-free: `run-event.schema.json`'s `oneOf` accepts a
|
|
9
|
+
* vendor type `<org>.thing` (the org read from the host's `extensions`, `acme`
|
|
10
|
+
* when the host is unreachable or advertises none) that is unknown to this
|
|
11
|
+
* suite, and rejects the reserved `openwop.` prefix. The reader half is the
|
|
12
|
+
* suite's own fold: `event-type-closed` and `payload-registry-closed` ignore a
|
|
13
|
+
* vendor event's payload rather than acting on it, which is what the growth
|
|
14
|
+
* rule requires of a consumer. The producer half is `event-type-closed`.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
import { describe, it, expect } from 'vitest';
|
|
18
|
+
import { readFileSync } from 'node:fs';
|
|
19
|
+
import { join } from 'node:path';
|
|
20
|
+
import { v2Discovery, v2Validator } from '../lib/v2.js';
|
|
21
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
22
|
+
import { req } from '../lib/requirement-ids.js';
|
|
23
|
+
|
|
24
|
+
const DOC = 'spec/v2/core/events.md §Types (Growth)';
|
|
25
|
+
|
|
26
|
+
async function org(): Promise<string> {
|
|
27
|
+
let doc: Record<string, unknown> | null;
|
|
28
|
+
try { doc = await v2Discovery(); } catch { doc = null; }
|
|
29
|
+
const ext = doc?.['extensions'];
|
|
30
|
+
const first = ext !== null && typeof ext === 'object' ? Object.keys(ext as Record<string, unknown>)[0] : undefined;
|
|
31
|
+
const o = first?.split('.')[0];
|
|
32
|
+
return o !== undefined && o.length > 0 ? o : 'acme';
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function protocolTypes(): Set<string> {
|
|
36
|
+
const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'run-event.schema.json'), 'utf8')) as { properties: { type: { oneOf: Array<{ enum?: string[] }> } } };
|
|
37
|
+
return new Set(schema.properties.type.oneOf.flatMap((b) => b.enum ?? []));
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function event(type: string): Record<string, unknown> {
|
|
41
|
+
return {
|
|
42
|
+
eventId: 'conformanceevent0123456789',
|
|
43
|
+
runId: 'openwop-conformance-tenant/conformancerun0123456789',
|
|
44
|
+
type,
|
|
45
|
+
payload: { anything: 'the suite does not act on it' },
|
|
46
|
+
timestamp: '2026-09-03T00:00:00Z',
|
|
47
|
+
sequence: 0,
|
|
48
|
+
schemaVersion: 1,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
describe('v2 enum-growth-rule (RFC 0171 §A.5)', () => {
|
|
53
|
+
it('the envelope accepts a registered-but-unknown-to-this-suite vendor member', async () => {
|
|
54
|
+
const validate = v2Validator('run-event');
|
|
55
|
+
const type = `${await org()}.thing`;
|
|
56
|
+
expect(protocolTypes().has(type), req('openwop.requirement.0171.enum-growth-rule.unknown-member-accepted', DOC, `${type} MUST NOT be a member of the suite's closed protocol enum — it is the unknown member the rule is about`)).toBe(false);
|
|
57
|
+
const r = validate(event(type));
|
|
58
|
+
expect(r.ok, req('openwop.requirement.0171.enum-growth-rule.unknown-member-accepted', DOC, `a consumer MUST accept an unknown registered member: the schema's oneOf vendor branch MUST admit ${type} (${r.errors})`)).toBe(true);
|
|
59
|
+
const three = validate(event(`${await org()}.thing.happened`));
|
|
60
|
+
expect(three.ok, req('openwop.requirement.0171.enum-growth-rule.unknown-member-accepted', DOC, 'the vendor branch admits an optional third segment (<org>.<domain>.<name>)')).toBe(true);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('the reserved prefix and a malformed member are rejected', () => {
|
|
64
|
+
const validate = v2Validator('run-event');
|
|
65
|
+
expect(validate(event('openwop.thing')).ok, req('openwop.requirement.0171.enum-growth-rule.reserved-rejected', DOC, 'openwop. is the only reserved prefix; a type under it that is not in the enum MUST fail')).toBe(false);
|
|
66
|
+
expect(validate(event('Acme.Thing')).ok, req('openwop.requirement.0171.enum-growth-rule.reserved-rejected', DOC, 'a member outside the positive pattern (upper-case) MUST fail')).toBe(false);
|
|
67
|
+
expect(validate(event('thing')).ok, req('openwop.requirement.0171.enum-growth-rule.reserved-rejected', DOC, 'a single-segment member MUST fail — every type is <domain>.<name> or <org>.<name>')).toBe(false);
|
|
68
|
+
});
|
|
69
|
+
});
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RFC 0176 §A.2 — `era-key` (suite 2.0.0, target major 2; unaided).
|
|
3
|
+
*
|
|
4
|
+
* `eventLogSchemaVersion` is the era key: a v2 host MUST stamp `3` on every run
|
|
5
|
+
* it creates, the field is REQUIRED on every run snapshot
|
|
6
|
+
* (`schemas/v2/run-snapshot.schema.json`), and discovery advertises the value
|
|
7
|
+
* the host writes for new runs and nothing else (`spec/v2/core/persistence.md`
|
|
8
|
+
* §The era key; versioning.md §2.2). The absent-⇒-`2` half of the rule is a
|
|
9
|
+
* property of a store a v1 host has written and is witnessed by the seam-gated
|
|
10
|
+
* `v2-v1-events-translated` scenario, not here.
|
|
11
|
+
*
|
|
12
|
+
* Legs:
|
|
13
|
+
* 1. a run created now (`POST /runs`, the `conformance-noop` fixture) reads
|
|
14
|
+
* back with `eventLogSchemaVersion: 3`;
|
|
15
|
+
* 2. discovery's `eventLogSchemaVersion` equals the value the host wrote.
|
|
16
|
+
*
|
|
17
|
+
* @see spec/v2/core/persistence.md §The era key
|
|
18
|
+
* @see spec/v2/core/runs.md §getRun
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { describe, it, expect } from 'vitest';
|
|
22
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
23
|
+
import { v2Discovery } from '../lib/v2.js';
|
|
24
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
25
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
26
|
+
import { req } from '../lib/requirement-ids.js';
|
|
27
|
+
|
|
28
|
+
const DOC = 'spec/v2/core/persistence.md §The era key';
|
|
29
|
+
const NOOP_WORKFLOW_ID = 'conformance-noop';
|
|
30
|
+
const V2_ERA = 3;
|
|
31
|
+
|
|
32
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
33
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
|
|
36
|
+
try { return await fn(); } catch { return null; }
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
async function freshSnapshot(): Promise<{ snapshot: Record<string, unknown> } | { reason: string }> {
|
|
40
|
+
const created = await http(() => driver.post('/runs', { workflowId: NOOP_WORKFLOW_ID }));
|
|
41
|
+
if (created === null) return { reason: 'POST /runs unreachable (fetch failed)' };
|
|
42
|
+
const runId = (created.json as { runId?: unknown } | undefined)?.runId;
|
|
43
|
+
if (created.status !== 201 || typeof runId !== 'string') {
|
|
44
|
+
return { reason: `POST /runs {workflowId: ${NOOP_WORKFLOW_ID}} answered ${created.status} ${readErrorCode(created.json) ?? ''} — the smallest valid create was refused (fixture not seeded?)`.trim() };
|
|
45
|
+
}
|
|
46
|
+
const snap = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`));
|
|
47
|
+
if (snap === null || snap.status !== 200 || !snap.json || typeof snap.json !== 'object') return { reason: `GET /runs/{runId} answered ${snap?.status ?? 'no response'} for the run just created` };
|
|
48
|
+
return { snapshot: snap.json as Record<string, unknown> };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('RFC 0176 §A.2 — era-key (unaided)', () => {
|
|
52
|
+
it('a run created now carries eventLogSchemaVersion 3 on its snapshot', async () => {
|
|
53
|
+
const doc = await discovery();
|
|
54
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
55
|
+
const s = await freshSnapshot();
|
|
56
|
+
if ('reason' in s) return softSkip('blocked', s.reason);
|
|
57
|
+
expect(
|
|
58
|
+
s.snapshot['eventLogSchemaVersion'],
|
|
59
|
+
req('openwop.requirement.0176.era-key', DOC, 'eventLogSchemaVersion is REQUIRED on every run snapshot (run-snapshot.schema.json) — the era key is never absent on a run a v2 host created'),
|
|
60
|
+
).toBeDefined();
|
|
61
|
+
expect(
|
|
62
|
+
s.snapshot['eventLogSchemaVersion'],
|
|
63
|
+
req('openwop.requirement.0176.era-key', DOC, `a v2 host MUST stamp ${V2_ERA} on every run it creates (RFC 0176 §A.2; versioning.md §2.2) — got ${String(s.snapshot['eventLogSchemaVersion'])}`),
|
|
64
|
+
).toBe(V2_ERA);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('discovery advertises the value the host writes for new runs and nothing else', async () => {
|
|
68
|
+
const doc = await discovery();
|
|
69
|
+
if (!doc) return softSkip('blocked', 'discovery unreachable');
|
|
70
|
+
const advertised = doc['eventLogSchemaVersion'];
|
|
71
|
+
expect(
|
|
72
|
+
Number.isInteger(advertised),
|
|
73
|
+
req('openwop.requirement.0176.era-key.discovery', 'spec/v2/core/versioning.md §2.2', `discovery MUST advertise eventLogSchemaVersion as an integer (metadata key, capabilities.md §Metadata) — got ${String(advertised)}`),
|
|
74
|
+
).toBe(true);
|
|
75
|
+
expect(
|
|
76
|
+
advertised,
|
|
77
|
+
req('openwop.requirement.0176.era-key.discovery', 'spec/v2/core/versioning.md §2.2', `the advertised value is the one the host writes for NEW runs: ${V2_ERA} on a v2 host (a host holds one constant for this axis, RFC 0167 §F)`),
|
|
78
|
+
).toBe(V2_ERA);
|
|
79
|
+
const s = await freshSnapshot();
|
|
80
|
+
if ('reason' in s) return softSkip('blocked', `${s.reason} — the advertised value was checked, its agreement with a written run was not`);
|
|
81
|
+
expect(
|
|
82
|
+
s.snapshot['eventLogSchemaVersion'],
|
|
83
|
+
req('openwop.requirement.0176.era-key.discovery', DOC, 'the value stamped on a run created now MUST equal the value discovery advertises'),
|
|
84
|
+
).toBe(advertised);
|
|
85
|
+
});
|
|
86
|
+
});
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — `error-registry` (suite 2.0.0; RFC 0171 §B.1–§B.2;
|
|
3
|
+
* `spec/v2/core/errors.md` §"The envelope", §"Retry timing").
|
|
4
|
+
*
|
|
5
|
+
* Witness class: witnessable — unaided (the `Retry-After` leg is gated on a
|
|
6
|
+
* rate-limited response being observed). Every error response MUST be the
|
|
7
|
+
* closed envelope `{ error, message, details? }` with a registered (or vendor)
|
|
8
|
+
* code answered at its registered HTTP status; `details.retryAfter`,
|
|
9
|
+
* `retryAfterMs` and `retryAfterSeconds` MUST NOT be emitted — retry timing
|
|
10
|
+
* lives in `Retry-After` only, which a `429 rate_limited` MUST set. The
|
|
11
|
+
* deliberately bad request is `GET /runs/does-not-exist` (outside the runId
|
|
12
|
+
* grammar, so a host may answer `400 validation_error` or `404 not_found`).
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { describe, it, expect } from 'vitest';
|
|
16
|
+
import { existsSync, readFileSync } from 'node:fs';
|
|
17
|
+
import { join } from 'node:path';
|
|
18
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
19
|
+
import { v2Discovery, v2Validator } from '../lib/v2.js';
|
|
20
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
21
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
22
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
23
|
+
import { req } from '../lib/requirement-ids.js';
|
|
24
|
+
|
|
25
|
+
const DOC = 'spec/v2/core/errors.md §The envelope';
|
|
26
|
+
const ERRORS_PATH = join(SCHEMAS_DIR, '..', 'spec', 'v2', 'errors.json');
|
|
27
|
+
const RETRY_SPELLINGS = ['retryAfter', 'retryAfterMs', 'retryAfterSeconds'];
|
|
28
|
+
|
|
29
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
30
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
31
|
+
}
|
|
32
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
|
|
33
|
+
try { return await fn(); } catch { return null; }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** code → registered httpStatus, when the registry ships in this layout. */
|
|
37
|
+
function registryStatus(): Map<string, number> | null {
|
|
38
|
+
if (!existsSync(ERRORS_PATH)) return null;
|
|
39
|
+
const rows = (JSON.parse(readFileSync(ERRORS_PATH, 'utf8')) as { rows?: Array<{ code: string; httpStatus: number }> }).rows ?? [];
|
|
40
|
+
return new Map(rows.map((r) => [r.code, r.httpStatus]));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async function badRequest(): Promise<OpenWOPResponse | { reason: string }> {
|
|
44
|
+
if (!(await discovery())) return { reason: 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0' };
|
|
45
|
+
const res = await http(() => driver.get('/runs/does-not-exist'));
|
|
46
|
+
if (res === null) return { reason: 'GET /runs/does-not-exist unreachable (fetch failed)' };
|
|
47
|
+
if (res.status < 400) return { reason: `GET /runs/does-not-exist answered ${res.status} — no error envelope to observe` };
|
|
48
|
+
return res;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
describe('v2 error-registry (RFC 0171 §B.1–§B.2)', () => {
|
|
52
|
+
it('an error response is the closed envelope with a registered code at its registered status', async () => {
|
|
53
|
+
const res = await badRequest();
|
|
54
|
+
if ('reason' in res) return softSkip('blocked', res.reason);
|
|
55
|
+
const r = v2Validator('error-envelope')(res.json);
|
|
56
|
+
expect(r.ok, req('openwop.requirement.0171.error-registry.envelope', DOC, `every error body MUST be { error, message, details? } and nothing else, with error a registered or vendor code (${r.errors})`)).toBe(true);
|
|
57
|
+
const code = readErrorCode(res.json);
|
|
58
|
+
const registry = registryStatus();
|
|
59
|
+
if (registry !== null && code !== undefined && registry.has(code)) {
|
|
60
|
+
expect(res.status, req('openwop.requirement.0171.error-registry.envelope', DOC, `a host MUST answer ${code} with its registered status ${String(registry.get(code))}`)).toBe(registry.get(code));
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it('retry timing is never spelled inside details', async () => {
|
|
65
|
+
const res = await badRequest();
|
|
66
|
+
if ('reason' in res) return softSkip('blocked', res.reason);
|
|
67
|
+
const details = (res.json as { details?: unknown } | undefined)?.details;
|
|
68
|
+
const spelled = details !== null && typeof details === 'object' ? RETRY_SPELLINGS.filter((k) => k in (details as Record<string, unknown>)) : [];
|
|
69
|
+
expect(spelled, req('openwop.requirement.0171.error-registry.no-retry-details', 'spec/v2/core/errors.md §Retry timing', 'details.retryAfter / retryAfterMs / retryAfterSeconds are not part of v2 and a host MUST NOT emit them — Retry-After is the only retry timing')).toEqual([]);
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('a 429 rate_limited response carries Retry-After', async () => {
|
|
73
|
+
if (!(await discovery())) return softSkip('blocked', 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0');
|
|
74
|
+
// The suite does not manufacture load; it inspects whatever the probes it
|
|
75
|
+
// already makes return. A host that never rate-limits the suite records the
|
|
76
|
+
// leg as inapplicable (witnessable — gated on a host advertising limits).
|
|
77
|
+
const seen: OpenWOPResponse[] = [];
|
|
78
|
+
for (let i = 0; i < 3; i++) {
|
|
79
|
+
const res = await http(() => driver.get('/runs/does-not-exist'));
|
|
80
|
+
if (res !== null) seen.push(res);
|
|
81
|
+
}
|
|
82
|
+
const limited = seen.filter((r) => r.status === 429);
|
|
83
|
+
if (limited.length === 0) return softSkip('inapplicable', 'no 429 rate_limited response was observed during the suite\'s probes — the Retry-After leg is gated on a rate-limited response');
|
|
84
|
+
for (const r of limited) {
|
|
85
|
+
expect(readErrorCode(r.json), req('openwop.requirement.0171.error-registry.retry-after', 'spec/v2/core/errors.md §Retry timing', 'a 429 MUST carry the registered code rate_limited')).toBe('rate_limited');
|
|
86
|
+
expect(r.headers.get('retry-after'), req('openwop.requirement.0171.error-registry.retry-after', 'spec/v2/core/errors.md §Retry timing', 'a 429 rate_limited response MUST set Retry-After')).not.toBeNull();
|
|
87
|
+
}
|
|
88
|
+
});
|
|
89
|
+
});
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* v2 — `event-type-closed` (suite 2.0.0; RFC 0171 §A.1, §A.5;
|
|
3
|
+
* `spec/v2/core/events.md` §"The envelope", §"Types").
|
|
4
|
+
*
|
|
5
|
+
* Witness class: witnessable — unaided. Every event a host emits on a run's
|
|
6
|
+
* poll response carries a `type` that is EITHER a member of the closed enum in
|
|
7
|
+
* `schemas/v2/run-event.schema.json` OR a vendor type matching the positive
|
|
8
|
+
* `<org>.<name>` pattern whose org the host itself advertises under
|
|
9
|
+
* `extensions` — a producer MUST NOT emit an unregistered member. Each event is
|
|
10
|
+
* validated as a whole `RunEventDoc`. The run is the `conformance-noop`
|
|
11
|
+
* fixture, awaited to a terminal status so the log holds the run transitions.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { describe, it, expect } from 'vitest';
|
|
15
|
+
import { readFileSync } from 'node:fs';
|
|
16
|
+
import { join } from 'node:path';
|
|
17
|
+
import { driver, type OpenWOPResponse } from '../lib/driver.js';
|
|
18
|
+
import { v2Discovery, v2Validator } from '../lib/v2.js';
|
|
19
|
+
import { SCHEMAS_DIR } from '../lib/paths.js';
|
|
20
|
+
import { readErrorCode } from '../lib/error-envelope.js';
|
|
21
|
+
import { softSkip } from '../lib/soft-skip.js';
|
|
22
|
+
import { req } from '../lib/requirement-ids.js';
|
|
23
|
+
|
|
24
|
+
const DOC = 'spec/v2/core/events.md §Types';
|
|
25
|
+
const NOOP_WORKFLOW_ID = 'conformance-noop';
|
|
26
|
+
const VENDOR = /^(?!openwop\.)[a-z][a-z0-9]*(-[a-z0-9]+)*\.[a-z][a-z0-9]*(-[a-z0-9]+)*(\.[a-z][a-z0-9]*(-[a-z0-9]+)*)?$/;
|
|
27
|
+
const TERMINAL = new Set(['completed', 'failed', 'cancelled']);
|
|
28
|
+
const TIMEOUT_MS = Number(process.env['OPENWOP_LIFECYCLE_TIMEOUT_MS'] ?? 10_000);
|
|
29
|
+
|
|
30
|
+
async function discovery(): Promise<Record<string, unknown> | null> {
|
|
31
|
+
try { return await v2Discovery(); } catch { return null; }
|
|
32
|
+
}
|
|
33
|
+
async function http(fn: () => Promise<OpenWOPResponse>): Promise<OpenWOPResponse | null> {
|
|
34
|
+
try { return await fn(); } catch { return null; }
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The closed protocol enum, read from the generated schema. */
|
|
38
|
+
function protocolTypes(): Set<string> {
|
|
39
|
+
const schema = JSON.parse(readFileSync(join(SCHEMAS_DIR, 'v2', 'run-event.schema.json'), 'utf8')) as { properties: { type: { oneOf: Array<{ enum?: string[] }> } } };
|
|
40
|
+
return new Set(schema.properties.type.oneOf.flatMap((b) => b.enum ?? []));
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Orgs the host registers under `extensions.<org>.<name>`. */
|
|
44
|
+
function advertisedOrgs(doc: Record<string, unknown>): Set<string> {
|
|
45
|
+
const ext = doc['extensions'];
|
|
46
|
+
const keys = ext !== null && typeof ext === 'object' ? Object.keys(ext as Record<string, unknown>) : [];
|
|
47
|
+
return new Set(keys.map((k) => k.split('.')[0] ?? ''));
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function terminalEvents(): Promise<{ events: unknown[]; doc: Record<string, unknown> } | { reason: string }> {
|
|
51
|
+
const doc = await discovery();
|
|
52
|
+
if (!doc) return { reason: 'v2 discovery unreachable — /.well-known/openwop did not answer 200 with a JSON body under OpenWOP-Version: 2.0' };
|
|
53
|
+
const created = await http(() => driver.post('/runs', { workflowId: NOOP_WORKFLOW_ID }));
|
|
54
|
+
if (created === null) return { reason: 'POST /runs unreachable (fetch failed)' };
|
|
55
|
+
const runId = (created.json as { runId?: unknown } | undefined)?.runId;
|
|
56
|
+
if (created.status !== 201 || typeof runId !== 'string') return { reason: `POST /runs {workflowId: ${NOOP_WORKFLOW_ID}} answered ${created.status} ${readErrorCode(created.json) ?? ''} — the smallest valid create was refused (fixture not seeded?)`.trim() };
|
|
57
|
+
const deadline = Date.now() + TIMEOUT_MS;
|
|
58
|
+
while (Date.now() < deadline) {
|
|
59
|
+
const snap = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}`));
|
|
60
|
+
if (snap !== null && TERMINAL.has(String((snap.json as { status?: unknown } | undefined)?.status))) break;
|
|
61
|
+
await new Promise((r) => setTimeout(r, 250));
|
|
62
|
+
}
|
|
63
|
+
const poll = await http(() => driver.get(`/runs/${encodeURIComponent(runId)}/events/poll?timeout=1`));
|
|
64
|
+
if (poll === null || poll.status !== 200) return { reason: `GET /runs/{runId}/events/poll answered ${poll?.status ?? 'no response'}` };
|
|
65
|
+
const events = (poll.json as { events?: unknown } | undefined)?.events;
|
|
66
|
+
if (!Array.isArray(events) || events.length === 0) return { reason: 'the poll returned no events for a run driven to terminal status — the log is not observable' };
|
|
67
|
+
return { events, doc };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
describe('v2 event-type-closed (RFC 0171 §A.1, §A.5)', () => {
|
|
71
|
+
it('every emitted type is a registered protocol type or a vendor type under an advertised org', async () => {
|
|
72
|
+
const t = await terminalEvents();
|
|
73
|
+
if ('reason' in t) return softSkip('blocked', t.reason);
|
|
74
|
+
const enumSet = protocolTypes();
|
|
75
|
+
const orgs = advertisedOrgs(t.doc);
|
|
76
|
+
for (const ev of t.events) {
|
|
77
|
+
const type = String((ev as { type?: unknown }).type);
|
|
78
|
+
const registered = enumSet.has(type);
|
|
79
|
+
const vendor = !registered && VENDOR.test(type) && orgs.has(type.split('.')[0] ?? '');
|
|
80
|
+
expect(registered || vendor, req('openwop.requirement.0171.event-type-closed.type', DOC, `event type ${type} MUST be a member of the closed enum or a vendor type <org>.<name> whose org the host advertises under extensions (a producer MUST NOT emit an unregistered member; openwop. is reserved)`)).toBe(true);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it('every emitted event validates as a RunEventDoc', async () => {
|
|
85
|
+
const t = await terminalEvents();
|
|
86
|
+
if ('reason' in t) return softSkip('blocked', t.reason);
|
|
87
|
+
const validate = v2Validator('run-event');
|
|
88
|
+
let previous = -1;
|
|
89
|
+
for (const ev of t.events) {
|
|
90
|
+
const r = validate(ev);
|
|
91
|
+
expect(r.ok, req('openwop.requirement.0171.event-type-closed.envelope', 'spec/v2/core/events.md §The envelope', `every event MUST validate against schemas/v2/run-event.schema.json — eventId, runId, type, payload, timestamp, sequence, schemaVersion REQUIRED; closed (${r.errors})`)).toBe(true);
|
|
92
|
+
const seq = (ev as { sequence?: unknown }).sequence;
|
|
93
|
+
expect(typeof seq === 'number' && seq > previous, req('openwop.requirement.0171.event-type-closed.envelope', 'spec/v2/core/events.md §The envelope', 'sequence is the one ordering field: integer ≥ 0, strictly increasing per run')).toBe(true);
|
|
94
|
+
previous = typeof seq === 'number' ? seq : previous;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
});
|