diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0c69649..f8cc488 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -174,7 +174,7 @@ jobs: run: | test -z "$(gofmt -l .)" go vet ./... - go test ./... + go test -count=1 ./... CGO_ENABLED=0 go build -o "${RUNNER_TEMP:-/tmp}/ink" ./cmd/ink CGO_ENABLED=0 go build -o "${RUNNER_TEMP:-/tmp}/ink-verify-server" ./cmd/ink-verify-server CGO_ENABLED=0 go build -o "${RUNNER_TEMP:-/tmp}/ink-witness-server" ./cmd/ink-witness-server @@ -222,9 +222,9 @@ jobs: nix develop --command bash -c ' set -euo pipefail cd go - go test ./ink/ + go test -count=1 ./ink/ log="${RUNNER_TEMP:-/tmp}/staged.log" - go test ./ink/ -run TestAgentCardSignaturePhaseC -v | tee "$log" + go test -count=1 ./ink/ -run TestAgentCardSignaturePhaseC -v | tee "$log" # A skip exits 0, so assert the staged test actually ran and passed. grep -q "^--- PASS: TestAgentCardSignaturePhaseC" "$log" ' diff --git a/.github/workflows/weekly-conformance.yml b/.github/workflows/weekly-conformance.yml index 16b8b85..f9ceef7 100644 --- a/.github/workflows/weekly-conformance.yml +++ b/.github/workflows/weekly-conformance.yml @@ -239,7 +239,7 @@ jobs: - name: Go conformance and manifest integrity working-directory: go - run: go test ./... + run: go test -count=1 ./... - name: Go verifier, staged vectors working-directory: go @@ -247,9 +247,9 @@ jobs: INK_STAGED_CONFORMANCE: "1" run: | set -euo pipefail - go test ./ink/ + go test -count=1 ./ink/ log="${RUNNER_TEMP:-/tmp}/staged.log" - go test ./ink/ -run TestAgentCardSignaturePhaseC -v > "$log" + go test -count=1 ./ink/ -run TestAgentCardSignaturePhaseC -v > "$log" cat "$log" grep -q '^--- PASS: TestAgentCardSignaturePhaseC' "$log" diff --git a/CHANGELOG.md b/CHANGELOG.md index 8578cad..c683754 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,20 @@ here. Pre-1.0 releases follow `0.Y.Z` semantics, see ### Changes +- Inclusion-receipt reject vectors name the check that must refuse them, and + both runners assert it. Every one of the 33 asserted a verdict and nothing + else, so a verifier that refused for an unrelated reason satisfied them. Go + gained a step alongside its verdict, the generator refuses to emit a reject + vector without one, and the corpus schema carries the member. +- Go conformance runs pass `-count=1`. The Go test cache does not track the + corpus files the tests read, so a vector edit could be validated against a + cached pass: mutating an expected step reported ok in 0.004 seconds and + caught nothing, while the same run with `-count=1` caught it. The rule is + gated, so an invocation without it fails the fact check. +- The Go conformance runner asserts a reject vector that its parser refused at + the boundary. Those cases used to skip every assertion, so a parser that + refused everything satisfied them. + - The card verifier rejects a present member of the wrong type wherever it reads one, rather than at the top level only. A non-string `currentSigningKeyId` was compared as a value by the reference and read as diff --git a/conformance/v1/generate.mjs b/conformance/v1/generate.mjs index a6b12c4..b4b4c17 100644 --- a/conformance/v1/generate.mjs +++ b/conformance/v1/generate.mjs @@ -182,6 +182,11 @@ function writeSchema() { properties: { result: { type: "string", enum: ["accept", "reject"] }, reason: { type: "string" }, + // The check that refused, where the surface has one. A vector + // pinning only the verdict is satisfied by a refusal from the + // wrong check, so a reject vector names the step wherever the + // implementations can express it. + step: { type: "string" }, auditEvent: { type: "string" }, canonicalPrincipal: { type: "string" }, keyStatus: { type: "string", enum: ["active", "retired", "revoked"] }, @@ -1890,8 +1895,51 @@ const validReceipt = await makeReceipt(); function rcptAccept(caseId, description, input) { return { caseId, description, input, expect: { result: "accept" } }; } +// The check that MUST refuse each rejected receipt, asserted by both runners. +// Coarser than a per-case reason, since thirteen cases share `structure`, but +// it is what both implementations can express today. +const RECEIPT_REJECT_STEP = { + "checkpoint-bad-root-rejects": "checkpoint", + "checkpoint-fork-rejects": "checkpoint", + "checkpoint-negative-size-rejects": "checkpoint", + "checkpoint-rollback-rejects": "checkpoint", + "empty-event-id-rejects": "structure", + "empty-signature-rejects": "structure", + "empty-timestamp-rejects": "structure", + "event-hash-bad-format-rejects": "proof", + "event-hash-not-in-tree-rejects": "proof", + "event-id-mismatch-rejects": "proof", + "event-leaf-not-at-index-rejects": "proof", + "event-missing-id-rejects": "proof", + "leaf-index-ge-tree-size-rejects": "structure", + "malformed-signature-rejects": "signature", + "negative-leaf-index-rejects": "structure", + "non-integer-leaf-index-rejects": "structure", + "null-event-rejects": "proof", + "proof-bad-element-rejects": "structure", + "proof-not-array-rejects": "structure", + "proof-too-long-rejects": "structure", + "receipt-not-object-rejects": "structure", + "short-root-hash-rejects": "structure", + "surrogate-in-event-id-rejects": "signature", + "surrogate-in-event-rejects": "proof", + "tampered-event-id-rejects": "signature", + "tampered-leaf-index-rejects": "signature", + "tampered-proof-rejects": "proof", + "tampered-root-hash-rejects": "signature", + "tampered-timestamp-rejects": "signature", + "tampered-tree-size-rejects": "signature", + "uppercase-root-hash-rejects": "structure", + "wrong-witness-key-rejects": "signature", + "zero-tree-size-rejects": "structure", +}; + function rcptReject(caseId, description, input) { - return { caseId, description, input, expect: { result: "reject" } }; + const step = RECEIPT_REJECT_STEP[caseId]; + // A new reject vector has to declare which check refuses it. Generating one + // without a step would quietly reintroduce the verdict-only assertion. + if (!step) throw new Error(`inclusion-receipt: no reject step declared for ${caseId}`); + return { caseId, description, input, expect: { result: "reject", step } }; } const wpk = { witnessPublicKeyHex: publicKeyHex }; diff --git a/conformance/v1/manifest.json b/conformance/v1/manifest.json index 0a9622b..f67be29 100644 --- a/conformance/v1/manifest.json +++ b/conformance/v1/manifest.json @@ -153,7 +153,7 @@ "spec": "specs/ink-inclusion-receipt.md", "summary": "Composite inclusion-receipt verification.", "caseCount": 39, - "sha256": "e8f41905b2b482ac82cac2262f92f99b14045ffe8060c69a94de29e99b431e2f" + "sha256": "2be8699b9d047b6692c329902882ae12af99a00e9a4c7fd40b32164de2336ec0" }, { "id": "jcs-number", diff --git a/conformance/v1/schema.json b/conformance/v1/schema.json index 6b43d63..307229e 100644 --- a/conformance/v1/schema.json +++ b/conformance/v1/schema.json @@ -119,6 +119,9 @@ "reason": { "type": "string" }, + "step": { + "type": "string" + }, "auditEvent": { "type": "string" }, diff --git a/conformance/v1/vectors/inclusion-receipt.json b/conformance/v1/vectors/inclusion-receipt.json index aafa1f1..6b96da8 100644 --- a/conformance/v1/vectors/inclusion-receipt.json +++ b/conformance/v1/vectors/inclusion-receipt.json @@ -32,7 +32,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -54,7 +55,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -76,7 +78,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -98,7 +101,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -120,7 +124,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -142,7 +147,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -164,7 +170,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -183,7 +190,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -268,7 +276,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -289,7 +298,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -311,7 +321,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -333,7 +344,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -355,7 +367,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -377,7 +390,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -399,7 +413,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -421,7 +436,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -443,7 +459,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -465,7 +482,8 @@ "witnessPublicKeyHex": "8dd3649d02ecde7b08636d86b15a923bc2ac3d301e82b58c18241661aeec4a2b" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -487,7 +505,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -541,7 +560,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -568,7 +588,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -594,7 +615,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -640,7 +662,8 @@ "eventHash": "not-a-hash" }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -663,7 +686,8 @@ "eventHash": "1a01d742673069afdd4ae9b6643939e94935869dcfb605bc71624469c2a54dd0" }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -690,7 +714,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -768,7 +793,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "checkpoint" } }, { @@ -794,7 +820,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "checkpoint" } }, { @@ -820,7 +847,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "checkpoint" } }, { @@ -846,7 +874,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "checkpoint" } }, { @@ -899,7 +928,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "signature" } }, { @@ -925,7 +955,8 @@ } }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } }, { @@ -947,7 +978,8 @@ "witnessPublicKeyHex": "22fec375ea0fe9d1b05996aac2485c17fafda30b7b6718c76e3169fa16c419c4" }, "expect": { - "result": "reject" + "result": "reject", + "step": "structure" } }, { @@ -970,7 +1002,8 @@ "event": null }, "expect": { - "result": "reject" + "result": "reject", + "step": "proof" } } ] diff --git a/go/ink/conformance_test.go b/go/ink/conformance_test.go index a53fe21..f617858 100644 --- a/go/ink/conformance_test.go +++ b/go/ink/conformance_test.go @@ -69,6 +69,7 @@ type conformanceCase struct { Expect struct { Result string `json:"result"` Reason string `json:"reason"` + Step string `json:"step"` AuditEvent string `json:"auditEvent"` CanonicalPrincipal string `json:"canonicalPrincipal"` KeyStatus string `json:"keyStatus"` @@ -603,6 +604,26 @@ func TestAuditQueryResponse(t *testing.T) { } } +// boundaryReject records that the receiver's parser refused the input before the +// verifier ran. That is a structural rejection, so the vector's step still has +// to match: skipping the assertion here let a reject vector pass without any +// check at all, which is the verdict-only weakness one layer lower. +func expectBoundaryStep(t *testing.T, caseID, wantStep, what string) { + t.Helper() + if wantStep == "" || wantStep == "structure" { + return + } + // These refuse here rather than at the step the vector names: this runner + // applies the raw-text checks the enforcement order requires before the + // parse, and the reference runner cannot, being handed a parsed object. + // Listed by name so the set cannot widen silently. + switch caseID { + case "surrogate-in-event-rejects", "null-event-rejects", "surrogate-in-event-id-rejects": + return + } + t.Errorf("%s: %s refused at the boundary (step structure), want step %q", caseID, what, wantStep) +} + func TestInclusionReceipt(t *testing.T) { vf := loadVectors(t, "inclusion-receipt") for _, c := range vf.Cases { @@ -624,6 +645,8 @@ func TestInclusionReceipt(t *testing.T) { if !ok { if want { t.Errorf("%s: receipt failed to parse but vector expects accept", c.CaseID) + } else { + expectBoundaryStep(t, c.CaseID, c.Expect.Step, "receipt") } continue } @@ -637,6 +660,8 @@ func TestInclusionReceipt(t *testing.T) { if err != nil { if want { t.Errorf("%s: event failed to parse but vector expects accept: %v", c.CaseID, err) + } else { + expectBoundaryStep(t, c.CaseID, c.Expect.Step, "event") } continue } @@ -644,6 +669,8 @@ func TestInclusionReceipt(t *testing.T) { if !isObj { if want { t.Errorf("%s: event is not an object but vector expects accept", c.CaseID) + } else { + expectBoundaryStep(t, c.CaseID, c.Expect.Step, "event") } continue } @@ -668,8 +695,14 @@ func TestInclusionReceipt(t *testing.T) { opts.LaterCheckpoint = &cp } - if got := VerifyInclusionReceipt(receipt, pub, opts); got != want { - t.Errorf("%s: VerifyInclusionReceipt = %v, want %v", c.CaseID, got, want) + got, step := VerifyInclusionReceiptStep(receipt, pub, opts) + if got != want { + t.Errorf("%s: VerifyInclusionReceipt = %v, want %v (step %s)", c.CaseID, got, want, step) + } + // The step is the reason for this surface. A vector pinning only the + // verdict is satisfied by a refusal from the wrong check. + if c.Expect.Step != "" && string(step) != c.Expect.Step { + t.Errorf("%s: step = %q, want %q", c.CaseID, step, c.Expect.Step) } } } diff --git a/go/ink/receipt.go b/go/ink/receipt.go index 2787abd..8d7c1b7 100644 --- a/go/ink/receipt.go +++ b/go/ink/receipt.go @@ -290,8 +290,28 @@ func verifyReceiptSignatureWith(r InclusionReceipt, s signerStrategy, artifactMs // optionally walks the inclusion proof (when Event or EventHash is given) and // cross-checks a later checkpoint (when LaterCheckpoint is given). It returns // false, never panics, on any failed step. +// ReceiptRejectStep names the check that refused a receipt, using the +// reference's step names, so a vector can pin why and not merely that. +type ReceiptRejectStep string + +const ( + ReceiptStepNone ReceiptRejectStep = "" + ReceiptStepStructure ReceiptRejectStep = "structure" + ReceiptStepSignature ReceiptRejectStep = "signature" + ReceiptStepProof ReceiptRejectStep = "proof" + ReceiptStepCheckpoint ReceiptRejectStep = "checkpoint" +) + func VerifyInclusionReceipt(receipt InclusionReceipt, witnessPublicKey []byte, opts ReceiptVerifyOptions) bool { - return verifyInclusionReceiptWith(receipt, fixedKey(witnessPublicKey), opts).Verified + ok, _ := VerifyInclusionReceiptStep(receipt, witnessPublicKey, opts) + return ok +} + +// VerifyInclusionReceiptStep is VerifyInclusionReceipt plus the step that +// refused. On acceptance the step is ReceiptStepNone. +func VerifyInclusionReceiptStep(receipt InclusionReceipt, witnessPublicKey []byte, opts ReceiptVerifyOptions) (bool, ReceiptRejectStep) { + result, step := verifyInclusionReceiptWith(receipt, fixedKey(witnessPublicKey), opts) + return result.Verified, step } // VerifyInclusionReceiptWithKeys verifies an INK inclusion receipt (INK @@ -312,24 +332,25 @@ func VerifyInclusionReceipt(receipt InclusionReceipt, witnessPublicKey []byte, o // matching the "no key attribution for a rejection" rule // VerifyInkSignatureForLiveAuth documents elsewhere in this package. func VerifyInclusionReceiptWithKeys(receipt InclusionReceipt, keys []CandidateKey, hintKeyID string, opts ReceiptVerifyOptions) MultiKeyResult { - return verifyInclusionReceiptWith(receipt, candidateKeys(keys, hintKeyID), opts) + result, _ := verifyInclusionReceiptWith(receipt, candidateKeys(keys, hintKeyID), opts) + return result } -func verifyInclusionReceiptWith(receipt InclusionReceipt, s signerStrategy, opts ReceiptVerifyOptions) MultiKeyResult { +func verifyInclusionReceiptWith(receipt InclusionReceipt, s signerStrategy, opts ReceiptVerifyOptions) (MultiKeyResult, ReceiptRejectStep) { if !checkReceiptShape(receipt) { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepStructure } var artifactMs int64 if s.needsClock { ms, ok := ParseInkTimestampMs(receipt.Timestamp) if !ok { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepSignature } artifactMs = ms } result := verifyReceiptSignatureWith(receipt, s, artifactMs) if !result.Verified { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepSignature } var leafHash string @@ -337,33 +358,33 @@ func verifyInclusionReceiptWith(receipt InclusionReceipt, s signerStrategy, opts if opts.Event != nil { id, ok := opts.Event["id"].(string) if !ok || id != receipt.EventID { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepProof } h, ok := ComputeAuditMerkleLeafHash(opts.Event) if !ok { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepProof } leafHash, hasLeaf = h, true } else if opts.EventHash != "" { if !isMerkleHashHex(opts.EventHash) { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepProof } leafHash, hasLeaf = opts.EventHash, true } if hasLeaf && !VerifyInclusionProof(leafHash, receipt.InclusionProof, receipt.LeafIndex, receipt.TreeSize, receipt.RootHash) { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepProof } if cp := opts.LaterCheckpoint; cp != nil { if cp.TreeSize < 0 || !isMerkleHashHex(cp.RootHash) { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepCheckpoint } if cp.TreeSize < receipt.TreeSize { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepCheckpoint } if cp.TreeSize == receipt.TreeSize && cp.RootHash != receipt.RootHash { - return MultiKeyResult{} + return MultiKeyResult{}, ReceiptStepCheckpoint } } - return result + return result, ReceiptStepNone } diff --git a/governance/releases/1.0-readiness-evidence.md b/governance/releases/1.0-readiness-evidence.md index ffa0293..2b1667f 100644 --- a/governance/releases/1.0-readiness-evidence.md +++ b/governance/releases/1.0-readiness-evidence.md @@ -263,7 +263,7 @@ only category added since the `v0.15.0` freeze. - Manifest format: `ink.conformance.manifest.v1` - Coverage: **32 categories, 897 vectors**[^ck] - Manifest integrity anchor (SHA-256 of `conformance/v1/manifest.json`): - `247d445fbd5a14572bf4597b06c57494e32c2e16f6904ca8085f72c41e165d2a`[^ck] + `42acbd42c1c95a9a2554cdc8d38a69aab45d426315402aac74019f4099b5228f`[^ck] Per profile: diff --git a/scripts/check-doc-facts.ts b/scripts/check-doc-facts.ts index 98f2a7d..4b60286 100644 --- a/scripts/check-doc-facts.ts +++ b/scripts/check-doc-facts.ts @@ -184,6 +184,17 @@ if ( `so the release under evaluation has one source.`, ); } +// A conformance run that the Go test cache can serve is not a run. The cache +// does not track the corpus files the tests read, so a vector change validates +// against a stale pass unless every invocation asks for a fresh one. +for (const workflow of [".github/workflows/ci.yml", ".github/workflows/weekly-conformance.yml"]) { + for (const line of read(workflow).split("\n")) { + const trimmed = line.trim(); + if (!trimmed.startsWith("go test ") || trimmed.includes("-count=1")) continue; + errors.push(`${workflow}: "${trimmed}" must pass -count=1, or the corpus can be checked against a cached result.`); + } +} + const INTEROP_WORKFLOW = ".github/workflows/interop-lab.yml"; const cancelOnlyForPullRequests = /cancel-in-progress: \$\{\{ github\.event_name == 'pull_request' \}\}/; // A group holds one pending run, so a second push while the first is queued diff --git a/test/conformance.test.ts b/test/conformance.test.ts index caabc83..9109154 100644 --- a/test/conformance.test.ts +++ b/test/conformance.test.ts @@ -86,10 +86,10 @@ interface VectorCase { description: string; optionalBehavior?: OptionalBehavior; input: Record; - expect: { result: "accept" | "reject"; reason?: string; auditEvent?: string; canonicalPrincipal?: string; keyStatus?: string; keyId?: string; signature?: string; epochMs?: number; canonicalString?: string; leafHash?: string; derivedGrantId?: string }; + expect: { result: "accept" | "reject"; reason?: string; step?: string; auditEvent?: string; canonicalPrincipal?: string; keyStatus?: string; keyId?: string; signature?: string; epochMs?: number; canonicalString?: string; leafHash?: string; derivedGrantId?: string }; } -type Outcome = { result: "accept" | "reject"; reason?: string; auditEvents?: string[]; canonicalPrincipal?: string; keyStatus?: string; keyId?: string; signature?: string; epochMs?: number; canonicalString?: string; leafHash?: string; derivedGrantId?: string }; +type Outcome = { result: "accept" | "reject"; reason?: string; step?: string; auditEvents?: string[]; canonicalPrincipal?: string; keyStatus?: string; keyId?: string; signature?: string; epochMs?: number; canonicalString?: string; leafHash?: string; derivedGrantId?: string }; async function evaluate(category: string, input: Record): Promise { switch (category) { @@ -495,7 +495,10 @@ async function evaluate(category: string, input: Record): Promi eventHash, laterCheckpoint, }); - return { result: r.valid ? "accept" : "reject" }; + // The failing step is the reason for this surface. A vector that pins + // only the verdict is satisfied by refusing for the wrong reason. + const failed = r.steps.find((step) => !step.pass); + return { result: r.valid ? "accept" : "reject", step: failed?.name }; } case "payload-encryption": { const { envelope, recipientPrivateKeyHex, recipientDid } = input as { @@ -642,6 +645,9 @@ describe("ink/1 conformance vectors", () => { if (c.expect.reason !== undefined) { expect(actual.reason, c.caseId).toBe(c.expect.reason); } + if (c.expect.step !== undefined) { + expect(actual.step, c.caseId).toBe(c.expect.step); + } if (c.expect.auditEvent !== undefined) { expect(actual.auditEvents ?? [], c.caseId).toContain(c.expect.auditEvent); }