Skip to content

Commit 8e0367f

Browse files
committed
fix(conformance): align declaration runners
Exercise release and deterministic guests through the host checker, preserving typed JSON numbers and sorted keys. Admit the supported ore_block output in host validation. Require fresh producer artifacts without repeating full CI suites. Refs #1460
1 parent cc89cc0 commit 8e0367f

18 files changed

Lines changed: 396 additions & 75 deletions

File tree

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,18 @@ jobs:
250250
- name: Go binding
251251
env:
252252
STACK_ENCRYPT_TESTS_REQUIRE_LOCK: "1"
253+
STACK_CONFORMANCE_OUTPUT: ${{ runner.temp }}/stack-conformance-go.json
253254
run: |
254255
ulimit -l "$(ulimit -H -l)"
255256
echo "RLIMIT_MEMLOCK: $(ulimit -l) KiB"
256257
mise run go:test
257258
258-
- name: Shared Stack Encrypt conformance and fresh Go to Rust opening
259-
run: mise run test:conformance
259+
- name: Fresh Go scalar artifacts open in Rust
260+
env:
261+
STACK_CONFORMANCE_INPUT: ${{ runner.temp }}/stack-conformance-go.json
262+
run: |
263+
test -s "$STACK_CONFORMANCE_INPUT"
264+
mise x --env test -- cargo nextest run --locked -p stack-encrypt --all-features --test conformance
260265
261266
# The Rust suite opens the committed v4_rows.json and
262267
# v4_smallint_rows.json, whose ciphertexts the Go SDK sealed when each

‎conformance/stack-encrypt/README.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ Python and TypeScript runners are pending; their absence is not a passing result
3030

3131
The [declaration grammar](declaration-grammar.md) defines the engine's current
3232
record-plan syntax, defaults, context rules, kind/index table and null policies.
33-
`v1/declarations.json` contains 201 cases, including malformed frames and all
33+
`v1/declarations.json` contains cases, including malformed frames and all
3434
21 kinds against five scalar indexes. The resolver is explicitly `none`; EQL
3535
targets need their own profile.
3636

‎conformance/stack-encrypt/declaration-grammar.md‎

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -243,7 +243,10 @@ The corpus is `v1/declarations.json`. Its version, profile and resolver are
243243
required. Case IDs are unique and stable. Every case contains exactly one input:
244244

245245
- `{"json": value}` converts JSON objects in lexical key order. Nonnegative
246-
integers become uint64, negative integers int64, and fractional values float64.
246+
integer tokens become uint64; tokens beginning with `-` (including `-0`)
247+
become int64. Decimal/exponent tokens become finite float64. Integer tokens
248+
outside the corresponding 64-bit range and non-finite floats are invalid
249+
corpus inputs; runners must refuse them without rounding.
247250
Arrays preserve order. This is fixture conversion, not SDK policy.
248251
- `{"frame_hex": text}` supplies exact transport bytes, including typed contexts,
249252
original field order and malformed frames that JSON cannot represent.
@@ -265,7 +268,7 @@ no credentials and uses a refusing transport.
265268

266269
## Evidence and limits
267270

268-
The corpus covers 201 cases: 91 accepted and 110 refused, including the complete
271+
The corpus covers accepted and refused cases, including the complete
269272
scalar kind/index matrix. The native runner also checks selected normalization
270273
observations. Existing codec tests own exhaustive frame validity, depth limits,
271274
and integer-reader behavior. Existing EQL tests own resolver/catalog behavior.
@@ -274,3 +277,10 @@ Runtime operations and language-specific declarations remain separate layers.
274277
See [coverage.md](coverage.md) for the consolidation policy. New Python and
275278
TypeScript runners can adopt these inputs and expectations without copying the
276279
engine's validation rules.
280+
281+
The Go runner checks both the release and deterministic guests. Go-specific
282+
`v1/go-declarations.json` supplies explicit `record.Plan` inputs for the subset
283+
representable by that host model, keyed by the shared case ID. Those cases also
284+
run through production `Checker.Check` (Validate, Wire and the engine), with the
285+
same acceptance outcome. Raw malformed frames and shapes the Go plan cannot
286+
express remain engine-boundary checks.

‎conformance/stack-encrypt/v1/declarations.json‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3273,6 +3273,8 @@
32733273
}
32743274
]
32753275
}
3276-
}
3276+
},
3277+
{"id": "json-negative-zero","input": {"json": {"value": {"context": [["users","value"],-0],"outputs": ["c"]}}},"accepted": true,"normalized": {"label": ["users"],"context_field": null,"extension": [{"type": "int64","text": "0"}],"fields": [{"name": "value","label": ["users","value"],"type": null,"null": null,"outputs": ["c"]}]}},
3278+
{"id": "json-lexical-field-order","input": {"json": {"zeta": {"context": ["users","zeta"],"outputs": ["c"]},"alpha": {"context": ["users","alpha"],"outputs": ["c"]}}},"accepted": true,"normalized": {"label": ["users"],"context_field": null,"extension": [],"fields": [{"name": "alpha","label": ["users","alpha"],"type": null,"null": null,"outputs": ["c"]},{"name": "zeta","label": ["users","zeta"],"type": null,"null": null,"outputs": ["c"]}]}}
32773279
]
32783280
}

‎conformance/stack-encrypt/v1/go-declarations.json‎

Lines changed: 129 additions & 0 deletions
Large diffs are not rendered by default.

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

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -77,7 +77,7 @@ The coverage map there identifies existing evidence and remaining gaps.
7777
Implemented on `feat/stack-encrypt-conformance`, based directly on #1404.
7878
The corpus has eight positive cases and three query refusals. Local validation
7979
passes for native Rust, Go WASI, and fresh Go-to-Rust artifact exchange through
80-
`mise run test:conformance`. Rust clippy, the enabled Go linters, and 33 CI wiring
80+
`mise run test:conformance`. Rust clippy, the enabled Go linters, and the CI wiring
8181
checks also pass. The work is in draft [PR #1460](https://github.com/cipherstash/stack/pull/1460),
8282
targeting #1404's branch. Local validation is distinct from hosted CI status.
8383

@@ -91,16 +91,16 @@ SQL requirements and Python/TypeScript runners remain open.
9191
The [declaration grammar](../../conformance/stack-encrypt/declaration-grammar.md)
9292
records the implemented syntax and semantic rules, including accepted enum
9393
aliases, defaults, context composition, all 21 kinds against five scalar indexes,
94-
null policies and target-capability refusal. Its 201 cases run in native Rust
94+
null policies and target-capability refusal. Its cases run in native Rust
9595
and Go WASI without a cipher. Native Rust checks selected normalized plan
9696
observations; Go's existing checker exposes acceptance and diagnostics only.
9797

9898
The profile fixes `NoTargets`. EQL resolver/catalog behavior and full JSON/SQL
9999
operation semantics remain later profiles. Malformed frames explicitly distinguish
100100
the native parser's diagnostic from the WASI boundary's earlier refusal.
101101

102-
Local validation passes for both profiles (212 shared cases), fresh ciphertext
103-
exchange, Rust clippy/formatting, Go lint, Biome and 34 CI wiring checks.
102+
Local validation passes for both profiles, fresh ciphertext
103+
exchange, Rust clippy/formatting, Go lint, Biome and the CI wiring checks.
104104

105105
The [coverage map](../../conformance/stack-encrypt/coverage.md#consolidation-policy)
106106
records consolidation candidates. Shared case tables can replace duplicated

‎languages/golang/encrypt/checker.go‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,12 @@ func (k *Checker) engineCheck(ctx context.Context, plan *record.Plan) error {
5757
if err != nil {
5858
return fmt.Errorf("%w: %v", ErrEncoding, err)
5959
}
60-
_, err = k.c.call(ctx, func(ctx context.Context, inst *instance) ([]byte, error) {
60+
return k.checkFrame(ctx, encoded)
61+
}
62+
63+
// checkFrame is the common engine boundary for lowered plans and raw-frame tests.
64+
func (k *Checker) checkFrame(ctx context.Context, encoded []byte) error {
65+
_, err := k.c.call(ctx, func(ctx context.Context, inst *instance) ([]byte, error) {
6166
return inst.call(ctx, inst.planCheck, buf(encoded))
6267
})
6368
return err

‎languages/golang/encrypt/conformance_test.go‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,7 @@ func readConformanceJSON(t *testing.T, path string, out any) {
6666
}
6767
d := json.NewDecoder(bytes.NewReader(data))
6868
d.DisallowUnknownFields()
69+
d.UseNumber()
6970
if err := d.Decode(out); err != nil {
7071
t.Fatal(err)
7172
}
@@ -222,7 +223,7 @@ func TestStackConformance(t *testing.T) {
222223
if _, err := cipher.Open(ctx, &wrong, frozen); !errors.Is(err, encrypt.ErrForbidden) {
223224
t.Fatalf("wrong context: expected forbidden key retrieval, got %v", err)
224225
}
225-
output.Cases[c.ID] = conformanceArtifact{Frame: want.Frame, Ciphertext: hex.EncodeToString(fresh[0][corpus.Field].Ciphertext), Equality: hex.EncodeToString(query.Terms[0])}
226+
output.Cases[c.ID] = conformanceArtifact{Frame: hex.EncodeToString(encoded), Ciphertext: hex.EncodeToString(fresh[0][corpus.Field].Ciphertext), Equality: hex.EncodeToString(query.Terms[0])}
226227
})
227228
}
228229
if t.Failed() {

‎languages/golang/encrypt/declaration_conformance_test.go‎

Lines changed: 92 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -8,13 +8,13 @@ import (
88
"errors"
99
"os"
1010
"path/filepath"
11-
"reflect"
1211
"sort"
1312
"strconv"
1413
"strings"
1514
"testing"
1615

1716
"github.com/cipherstash/stack/languages/golang/encrypt"
17+
"github.com/cipherstash/stack/languages/golang/internal/record"
1818
"github.com/cipherstash/vitaminc/bindings/go/vcffi"
1919
"github.com/cipherstash/vitaminc/bindings/go/vcvalue"
2020
)
@@ -61,17 +61,7 @@ func declarationValue(t *testing.T, value any) any {
6161
}
6262
return v
6363
case json.Number:
64-
text := v.String()
65-
var value any
66-
var err error
67-
switch {
68-
case strings.ContainsAny(text, ".eE"):
69-
value, err = strconv.ParseFloat(text, 64)
70-
case strings.HasPrefix(text, "-"):
71-
value, err = strconv.ParseInt(text, 10, 64)
72-
default:
73-
value, err = strconv.ParseUint(text, 10, 64)
74-
}
64+
value, err := declarationNumber(v.String())
7565
if err != nil {
7666
t.Fatal(err)
7767
}
@@ -92,21 +82,44 @@ func TestDeclarationConformance(t *testing.T) {
9282
if corpus.Version != 1 || corpus.Profile != "record-declarations-v1" || corpus.Resolver != "none" || len(corpus.Cases) == 0 {
9383
t.Fatal("unsupported or empty declaration corpus")
9484
}
95-
wasm, err := os.ReadFile("testdata/stack_encrypt_guest_deterministic.wasm")
96-
if errors.Is(err, os.ErrNotExist) && os.Getenv("STACK_CONFORMANCE_REQUIRED") != "1" {
97-
t.Skip(err)
98-
}
99-
if err != nil {
100-
t.Fatal(err)
85+
var hostPlans map[string]record.Plan
86+
readConformanceJSON(t, filepath.Join("..", "..", "..", "conformance", "stack-encrypt", "v1", "go-declarations.json"), &hostPlans)
87+
for name, load := range map[string]func() ([]byte, error){"release": encrypt.PlainGuest, "deterministic": encrypt.DeterministicGuest} {
88+
t.Run(name, func(t *testing.T) {
89+
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+
}
93+
if err != nil {
94+
t.Fatal(err)
95+
}
96+
checker, err := encrypt.NewCheckerOver(context.Background(), wasm)
97+
if err != nil {
98+
t.Fatal(err)
99+
}
100+
t.Cleanup(func() { _ = checker.Close() })
101+
checkDeclarationCases(t, checker, corpus.Cases, hostPlans)
102+
})
101103
}
104+
}
105+
106+
func checkDeclarationCases(t *testing.T, checker *encrypt.Checker, cases []declarationCase, hostPlans map[string]record.Plan) {
107+
t.Helper()
102108
ctx := context.Background()
103-
checker, err := encrypt.NewCheckerOver(ctx, wasm)
104-
if err != nil {
105-
t.Fatal(err)
109+
for id := range hostPlans {
110+
found := false
111+
for _, c := range cases {
112+
if c.ID == id {
113+
found = true
114+
break
115+
}
116+
}
117+
if !found {
118+
t.Fatalf("host plan has no shared case: %s", id)
119+
}
106120
}
107-
t.Cleanup(func() { _ = checker.Close() })
108121
seen := map[string]bool{}
109-
for _, c := range corpus.Cases {
122+
for _, c := range cases {
110123
if c.ID == "" || seen[c.ID] {
111124
t.Fatalf("empty or duplicate case ID %q", c.ID)
112125
}
@@ -138,6 +151,15 @@ func TestDeclarationConformance(t *testing.T) {
138151
if err != nil {
139152
t.Fatal(err)
140153
}
154+
if plan, ok := hostPlans[c.ID]; ok {
155+
hostErr := checker.Check(ctx, &plan)
156+
if (hostErr == nil) != accepted {
157+
t.Fatalf("host Check: %v; accepted=%v", hostErr, accepted)
158+
}
159+
if hostErr != nil && !errors.Is(hostErr, encrypt.ErrEncoding) {
160+
t.Fatalf("host Check: expected ErrEncoding, got %v", hostErr)
161+
}
162+
}
141163
err = checker.CheckFrameForTest(ctx, frame)
142164
expected := c.ExpectedError
143165
if c.BoundaryError != nil {
@@ -159,9 +181,55 @@ func TestDeclarationConformance(t *testing.T) {
159181
if !errors.Is(err, encrypt.ErrEncoding) || !errors.As(err, &diagnostic) {
160182
t.Fatalf("expected encoding diagnostic, got %v", err)
161183
}
162-
if diagnostic.Code != expected.Code || !reflect.DeepEqual(diagnostic.Fields, expected.Fields) {
184+
if diagnostic.Code != expected.Code || !sameDiagnosticFields(t, diagnostic.Fields, expected.Fields) {
163185
t.Fatalf("got %s %#v; expected %s %#v", diagnostic.Code, diagnostic.Fields, expected.Code, expected.Fields)
164186
}
165187
})
166188
}
167189
}
190+
191+
// Compare JSON payload values without float64 rounding or int64/uint64 type
192+
// accidents. UseNumber in readConformanceJSON preserves expected integers.
193+
func sameDiagnosticFields(t *testing.T, actual, expected map[string]any) bool {
194+
t.Helper()
195+
a, err := json.Marshal(actual)
196+
if err != nil {
197+
t.Fatal(err)
198+
}
199+
b, err := json.Marshal(expected)
200+
if err != nil {
201+
t.Fatal(err)
202+
}
203+
return bytes.Equal(a, b)
204+
}
205+
206+
func TestDiagnosticNumericFields(t *testing.T) {
207+
if !sameDiagnosticFields(t, map[string]any{"n": uint64(18446744073709551615), "m": int64(-1)}, map[string]any{"n": json.Number("18446744073709551615"), "m": json.Number("-1")}) {
208+
t.Fatal("integer payload changed")
209+
}
210+
if sameDiagnosticFields(t, map[string]any{"n": uint64(18446744073709551615)}, map[string]any{"n": json.Number("18446744073709551614")}) {
211+
t.Fatal("integer precision lost")
212+
}
213+
}
214+
215+
func declarationNumber(text string) (any, error) {
216+
switch {
217+
case strings.ContainsAny(text, ".eE"):
218+
return strconv.ParseFloat(text, 64)
219+
case strings.HasPrefix(text, "-"):
220+
return strconv.ParseInt(text, 10, 64)
221+
default:
222+
return strconv.ParseUint(text, 10, 64)
223+
}
224+
}
225+
func TestDeclarationNumberProjection(t *testing.T) {
226+
zero, err := declarationNumber("-0")
227+
if err != nil || zero != int64(0) {
228+
t.Fatalf("-0: %v %v", zero, err)
229+
}
230+
for _, invalid := range []string{"18446744073709551616", "-9223372036854775809", "1e400"} {
231+
if _, err := declarationNumber(invalid); err == nil {
232+
t.Fatalf("accepted %s", invalid)
233+
}
234+
}
235+
}

‎languages/golang/encrypt/export_test.go‎

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -81,9 +81,9 @@ var ErrDeterministicGuestNotBuilt = errors.New("encrypt: deterministic guest not
8181
// name the build they run against (NewDeterministicEQLClient is the other),
8282
// so each build keeps its own coverage.
8383
func NewDeterministicClient(ctx context.Context, seed [32]byte) (*Client, error) {
84-
wasm, err := os.ReadFile(deterministicGuestPath)
84+
wasm, err := DeterministicGuest()
8585
if err != nil {
86-
return nil, ErrDeterministicGuestNotBuilt
86+
return nil, err
8787
}
8888
return deterministicClient(ctx, wasm, seed)
8989
}
@@ -188,8 +188,14 @@ var (
188188
// lowering, so the shared grammar can include malformed declarations. The
189189
// checker has no cipher, credentials or usable transport.
190190
func (k *Checker) CheckFrameForTest(ctx context.Context, encoded []byte) error {
191-
_, err := k.c.call(ctx, func(ctx context.Context, inst *instance) ([]byte, error) {
192-
return inst.call(ctx, inst.planCheck, buf(encoded))
193-
})
194-
return err
191+
return k.checkFrame(ctx, encoded)
192+
}
193+
194+
// DeterministicGuest reads the named test build without embedding it.
195+
func DeterministicGuest() ([]byte, error) {
196+
wasm, err := os.ReadFile(deterministicGuestPath)
197+
if errors.Is(err, os.ErrNotExist) {
198+
return nil, ErrDeterministicGuestNotBuilt
199+
}
200+
return wasm, err
195201
}

0 commit comments

Comments
 (0)