Skip to content

Commit 52d96d9

Browse files
committed
test(conformance): enforce fixture enrollment
1 parent 4d6fe81 commit 52d96d9

18 files changed

Lines changed: 294 additions & 62 deletions

‎.github/workflows/tests-golang.yml‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ on:
5959
- scripts/check-wasm-imports.py
6060
- scripts/go-binding-test.sh
6161
- scripts/stack-conformance.sh
62+
- scripts/stack-conformance-selection.mjs
6263
- Cargo.toml
6364
- Cargo.lock
6465
- .cargo/**
@@ -97,6 +98,7 @@ on:
9798
- scripts/check-wasm-imports.py
9899
- scripts/go-binding-test.sh
99100
- scripts/stack-conformance.sh
101+
- scripts/stack-conformance-selection.mjs
100102
- Cargo.toml
101103
- Cargo.lock
102104
- .cargo/**

‎conformance/stack-encrypt/fixture-contract.md‎

Lines changed: 19 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,12 @@ pages or fetch limits are copied into this directory.
1818
[`v1/fixture-profiles.json`](v1/fixture-profiles.json) maps stable requirement IDs
1919
to the existing Rust test functions and Go tests that provide their evidence.
2020
It is an enrollment index, not another input corpus or a runtime capability
21-
negotiation format. The CI wiring test checks that its fixtures/tests exist,
22-
that each requirement is documented here, and that the conformance command
23-
selects every listed test. Both language implementations are mandatory for
24-
these profiles; Python and TypeScript remain pending.
21+
negotiation format. The CI wiring test checks fixture paths, source declarations,
22+
requirement documentation and exact command selection. Rust also checks each
23+
binary’s compiled test inventory and rejects missing or ignored enrolled tests.
24+
The Go missing-guest regression checks each indexed test’s actual failure/skip
25+
outcome, so a test excluded from compilation cannot silently pass enrollment.
26+
Both language implementations are mandatory for these profiles; Python and TypeScript remain pending.
2527

2628
## Value-kind requirements
2729

@@ -66,21 +68,29 @@ which the record-interface test covers. Its existing SQL parameter conversion,
6668
empty-page, zero-query, inverted-range and floating-point ordering assertions
6769
remain in those selected tests. Wider operator refusal and secret-diagnostic
6870
tests remain in the full suites; this slice does not turn them into shared
69-
negative cases.
71+
negative cases. In Go, `TestAQueryTheFieldDoesNotAnswerIsRefused` and
72+
`TestAMatchesQueryNeverPrintsItsText` share the fixture client, so they also
73+
fail on a missing guest in required mode. They remain outside the aggregate
74+
selection and the shared requirements; the missing-guest regression covers them.
7075

7176
## Running and maintaining the profiles
7277

7378
`mise run test:conformance` selects the existing `value_kinds` and
7479
`query_operators` Rust binaries and the Go tests listed in the enrollment index,
75-
alongside the earlier profiles. Each existing test runs once per invocation;
76-
no wrapper test invokes another test a second time. The ordinary Rust and Go
80+
alongside the earlier profiles. The Go selection is generated from the index;
81+
adding an enrolled Go test does not require editing a shell regular expression.
82+
Fixture assertions stay in their owning tests; the separate missing-guest probes
83+
stop at client creation. The ordinary Rust and Go
7784
CI suites already run these tests, so enrollment adds no duplicate CI suite.
7885

7986
When `STACK_CONFORMANCE_REQUIRED=1`, a missing deterministic guest fails the
8087
enrolled Go tests instead of skipping them. The aggregate command rejects both
8188
`STACK_CONFORMANCE_UPDATE` and `STACK_ENCRYPT_UPDATE_FIXTURES`, even when set to
82-
an empty value. The enrolled Rust fixture writers independently reject updates
83-
in required mode. Deliberate fixture maintenance remains available outside
89+
an empty value. All four Rust fixture writers, including the two record-lowering
90+
writers,
91+
independently reject updates in required mode. A subprocess regression exercises
92+
the guard with absent, empty and nonempty update flags without writing fixtures.
93+
Deliberate fixture maintenance remains available outside
8494
required mode through the existing documented regeneration commands.
8595

8696
Keep requirement IDs stable. Add a new profile version for incompatible

‎docs/plans/2026-10-31-stack-encrypt-conformance.md‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -152,8 +152,7 @@ Python and TypeScript consumers remain pending.
152152

153153
## Value-kind and query-operator checkpoint
154154

155-
The next slice is on `feat/stack-encrypt-value-query-conformance`, stacked on
156-
[PR #1462](https://github.com/cipherstash/stack/pull/1462). The
155+
Building on the read-compatibility profile, the
157156
[fixture contract](../../conformance/stack-encrypt/fixture-contract.md) and
158157
[enrollment index](../../conformance/stack-encrypt/v1/fixture-profiles.json)
159158
assign stable requirement IDs to the existing `value_kinds.json` and
@@ -162,13 +161,14 @@ existing Rust and Go tests. Typed Rust comparisons, generated Go methods,
162161
match verification and fetch sizing remain intact.
163162

164163
The fixture files stay in place with no duplicated expectations. CI guards
165-
the requirement-to-test mapping and test selection. Required Go runs fail if
164+
the requirement-to-test mapping, compiled test availability and exact test
165+
selection. The Go selection is generated from the enrollment index. Required Go runs fail if
166166
the guest is missing; required checks cannot regenerate frozen fixtures.
167167
This is consolidation of inventory and execution, not removal of assertions.
168168

169169
Local validation passes for the expanded conformance command, fresh scalar
170170
artifact exchange, missing-guest regressions, Rust clippy/formatting, Go lint,
171-
Biome and CI wiring checks. Both enrolled Rust writers refuse regeneration in
171+
Biome and CI wiring checks. All four Rust fixture writers refuse regeneration in
172172
required mode, and the frozen fixture files remain unchanged.
173173

174174
The coverage is representative, not a complete kind/operator boundary matrix.

‎languages/golang/encrypt/conformance_test.go‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,7 @@ func TestStackConformance(t *testing.T) {
160160
}
161161
ctx := context.Background()
162162
client, err := encrypt.NewDeterministicClient(ctx, [32]byte(seed))
163-
if errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
164-
t.Skip(err)
165-
}
163+
skipMissingConformanceGuest(t, err)
166164
if err != nil {
167165
t.Fatal(err)
168166
}

‎languages/golang/encrypt/declaration_conformance_test.go‎

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/hex"
77
"encoding/json"
88
"errors"
9-
"os"
109
"path/filepath"
1110
"sort"
1211
"strconv"
@@ -87,9 +86,7 @@ func TestDeclarationConformance(t *testing.T) {
8786
for name, load := range map[string]func() ([]byte, error){"release": encrypt.PlainGuest, "deterministic": encrypt.DeterministicGuest} {
8887
t.Run(name, func(t *testing.T) {
8988
wasm, err := load()
90-
if (errors.Is(err, encrypt.ErrGuestNotBuilt) || errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt)) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
91-
t.Skip(err)
92-
}
89+
skipMissingConformanceGuest(t, err)
9390
if err != nil {
9491
t.Fatal(err)
9592
}

‎languages/golang/encrypt/fixture_conformance_test.go‎

Lines changed: 57 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,31 @@
11
package encrypt_test
22

33
import (
4+
"encoding/json"
45
"errors"
56
"os"
67
"os/exec"
78
"path/filepath"
9+
"regexp"
10+
"slices"
811
"strings"
912
"testing"
13+
14+
"github.com/cipherstash/stack/languages/golang/encrypt"
1015
)
1116

17+
// All conformance runners use the same missing-guest policy. Other errors
18+
// remain failures in the caller, including errors wrapping neither sentinel.
19+
func skipMissingConformanceGuest(t *testing.T, err error) {
20+
t.Helper()
21+
if (errors.Is(err, encrypt.ErrGuestNotBuilt) || errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt)) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
22+
t.Skip(err)
23+
}
24+
}
25+
1226
// Exercise the real fixture entry points in an isolated tree with their frozen
13-
// inputs but no guest. A missing guest must never make required conformance green.
27+
// inputs but no guest. Check every indexed test's outcome, so a missing compiled
28+
// test also fails this guard. A missing guest must never make conformance green.
1429
func TestFixtureConformanceRequiresGuest(t *testing.T) {
1530
binary, err := os.Executable()
1631
if err != nil {
@@ -24,20 +39,56 @@ func TestFixtureConformanceRequiresGuest(t *testing.T) {
2439
t.Fatal(err)
2540
}
2641
}
27-
for _, name := range []string{"value_kinds.json", "query_operators.json"} {
28-
data, err := os.ReadFile(filepath.Join("..", "..", "..", fixtures, name))
42+
var index struct {
43+
Profiles []struct {
44+
Fixture string `json:"fixture"`
45+
Requirements []struct {
46+
GoTests []string `json:"go_tests"`
47+
} `json:"requirements"`
48+
} `json:"profiles"`
49+
}
50+
data, err := os.ReadFile(filepath.Join("..", "..", "..", "conformance", "stack-encrypt", "v1", "fixture-profiles.json"))
51+
if err != nil {
52+
t.Fatal(err)
53+
}
54+
if err := json.Unmarshal(data, &index); err != nil {
55+
t.Fatal(err)
56+
}
57+
names := make(map[string]bool)
58+
for _, profile := range index.Profiles {
59+
data, err := os.ReadFile(filepath.Join("..", "..", "..", profile.Fixture))
2960
if err != nil {
3061
t.Fatal(err)
3162
}
32-
if err := os.WriteFile(filepath.Join(root, fixtures, name), data, 0o600); err != nil {
63+
if err := os.WriteFile(filepath.Join(root, profile.Fixture), data, 0o600); err != nil {
3364
t.Fatal(err)
3465
}
66+
for _, requirement := range profile.Requirements {
67+
for _, name := range requirement.GoTests {
68+
names[name] = true
69+
}
70+
}
71+
}
72+
if len(names) == 0 {
73+
t.Fatal("fixture index selects no Go tests")
74+
}
75+
// These full-suite tests also share queryFixtureClient's required-mode policy;
76+
// they remain outside the shared fixture requirements and aggregate selection.
77+
names["TestAQueryTheFieldDoesNotAnswerIsRefused"] = true
78+
names["TestAMatchesQueryNeverPrintsItsText"] = true
79+
tests := make([]string, 0, len(names))
80+
for name := range names {
81+
tests = append(tests, name)
82+
}
83+
slices.Sort(tests)
84+
patterns := make([]string, len(tests))
85+
for i, name := range tests {
86+
patterns[i] = regexp.QuoteMeta(name)
3587
}
36-
tests := []string{"TestGeneratedCodeOpensTheRustValueKindsFixture", "TestQueryTermsAreTheRustFixtures"}
3788
for _, required := range []string{"0", "1"} {
3889
t.Run("required="+required, func(t *testing.T) {
3990
t.Setenv("STACK_CONFORMANCE_REQUIRED", required)
40-
cmd := exec.CommandContext(t.Context(), binary, "-test.v", "-test.timeout=30s", "-test.run=^("+strings.Join(tests, "|")+")$")
91+
cmd := exec.CommandContext(t.Context(), binary, "-test.v", "-test.timeout=30s", "-test.run=^("+strings.Join(patterns, "|")+")$")
4192
cmd.Dir = workdir
4293
out, err := cmd.CombinedOutput()
4394
marker := "--- SKIP: "

‎languages/golang/encrypt/query_test.go‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -129,9 +129,7 @@ func queryFixtureClient(t *testing.T, f queryFixture) *encrypt.Client {
129129
t.Fatalf("seed: %v (%d bytes)", err, len(seed))
130130
}
131131
c, err := encrypt.NewDeterministicClient(context.Background(), [32]byte(seed))
132-
if (errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt) || errors.Is(err, encrypt.ErrGuestNotBuilt)) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
133-
t.Skip(err)
134-
}
132+
skipMissingConformanceGuest(t, err)
135133
if err != nil {
136134
t.Fatal(err)
137135
}

‎languages/golang/encrypt/read_conformance_test.go‎

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ import (
66
"encoding/json"
77
"errors"
88
"fmt"
9-
"os"
109
"path/filepath"
1110
"reflect"
1211
"testing"
@@ -149,9 +148,7 @@ func TestReadConformance(t *testing.T) {
149148
}
150149
ctx := context.Background()
151150
client, err := encrypt.NewDeterministicClient(ctx, [32]byte{})
152-
if errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
153-
t.Skip(err)
154-
}
151+
skipMissingConformanceGuest(t, err)
155152
if err != nil {
156153
t.Fatal(err)
157154
}
@@ -416,9 +413,7 @@ func TestReadCorpusSchema(t *testing.T) {
416413
func TestReadTreePreservesCipherScope(t *testing.T) {
417414
ctx := context.Background()
418415
client, err := encrypt.NewDeterministicClient(ctx, [32]byte{})
419-
if errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
420-
t.Skip(err)
421-
}
416+
skipMissingConformanceGuest(t, err)
422417
if err != nil {
423418
t.Fatal(err)
424419
}

‎languages/golang/encrypt/runtime_conformance_test.go‎

Lines changed: 1 addition & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package encrypt_test
33
import (
44
"bytes"
55
"context"
6-
"errors"
7-
"os"
86
"path/filepath"
97
"reflect"
108
"testing"
@@ -59,9 +57,7 @@ func TestRuntimeConformance(t *testing.T) {
5957
}
6058
ctx := context.Background()
6159
client, err := encrypt.NewDeterministicClient(ctx, [32]byte{})
62-
if errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
63-
t.Skip(err)
64-
}
60+
skipMissingConformanceGuest(t, err)
6561
if err != nil {
6662
t.Fatal(err)
6763
}

‎languages/golang/encrypt/value_kinds_test.go‎

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -152,9 +152,7 @@ func TestGeneratedCodeOpensTheRustValueKindsFixture(t *testing.T) {
152152
t.Fatalf("seed: %v (%d bytes)", err, len(seedBytes))
153153
}
154154
c, err := encrypt.NewDeterministicClient(context.Background(), [32]byte(seedBytes))
155-
if (errors.Is(err, encrypt.ErrDeterministicGuestNotBuilt) || errors.Is(err, encrypt.ErrGuestNotBuilt)) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
156-
t.Skip(err)
157-
}
155+
skipMissingConformanceGuest(t, err)
158156
if err != nil {
159157
t.Fatal(err)
160158
}

0 commit comments

Comments
 (0)