Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/tests-crates.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ on:
- packages/stack-profile/**
- packages/stack-kms/**
- packages/stack-encrypt/**
- conformance/stack-encrypt/**
- packages/stack-encrypt-derive/**
- packages/stack-guest-abi/**
- languages/typescript/packages/auth/**
Expand All @@ -44,6 +45,7 @@ on:
- packages/stack-profile/**
- packages/stack-kms/**
- packages/stack-encrypt/**
- conformance/stack-encrypt/**
- packages/stack-encrypt-derive/**
- packages/stack-guest-abi/**
- languages/typescript/packages/auth/**
Expand Down
8 changes: 8 additions & 0 deletions .github/workflows/tests-golang.yml
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ on:
- packages/stack-profile/**
- packages/stack-kms/**
- packages/stack-encrypt/**
- conformance/stack-encrypt/**
- packages/stack-encrypt-derive/**
- packages/stack-guest-abi/**
# The eql guest build links eql-bindings.
Expand All @@ -57,6 +58,7 @@ on:
- languages/golang/**
- scripts/check-wasm-imports.py
- scripts/go-binding-test.sh
- scripts/stack-conformance.sh
- Cargo.toml
- Cargo.lock
- .cargo/**
Expand All @@ -74,6 +76,7 @@ on:
- packages/stack-profile/**
- packages/stack-kms/**
- packages/stack-encrypt/**
- conformance/stack-encrypt/**
- packages/stack-encrypt-derive/**
- packages/stack-guest-abi/**
# The eql guest build links eql-bindings.
Expand All @@ -93,6 +96,7 @@ on:
- languages/golang/**
- scripts/check-wasm-imports.py
- scripts/go-binding-test.sh
- scripts/stack-conformance.sh
- Cargo.toml
- Cargo.lock
- .cargo/**
Expand All @@ -119,6 +123,7 @@ env:
RUSTFLAGS: "-D warnings"
RUST_BACKTRACE: full
CARGO_TERM_COLOR: always
STACK_CONFORMANCE_REQUIRED: "1"
CARGO_NET_GIT_FETCH_WITH_CLI: true

jobs:
Expand Down Expand Up @@ -250,6 +255,9 @@ jobs:
echo "RLIMIT_MEMLOCK: $(ulimit -l) KiB"
mise run go:test

- name: Shared Stack Encrypt conformance and fresh Go to Rust opening
run: mise run test:conformance

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Efficiency: this repeats work. mise run go:test in the previous step already runs TestStackConformance and TestDeclarationConformance (amd64 and 386, with STACK_CONFORMANCE_REQUIRED=1). tests-crates.yml already runs the Rust conformance and declarations tests. This step runs both Go tests again, plus a Rust host build and run of both suites, before reaching the one new check (the Rust STACK_CONFORMANCE_INPUT consumer). Consider exporting STACK_CONFORMANCE_OUTPUT from the go:test step and running only the Rust consumer here.


# The Rust suite opens the committed v4_rows.json and
# v4_smallint_rows.json, whose ciphertexts the Go SDK sealed when each
# file was last regenerated. This also hands it the rows the Go SDK
Expand Down
119 changes: 119 additions & 0 deletions conformance/stack-encrypt/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
# Stack Encrypt conformance

This directory holds a shared test contract for Stack Encrypt bindings. The
[implementation plan](../../docs/plans/2026-10-31-stack-encrypt-conformance.md)
sets out the wider work; [coverage.md](coverage.md) maps existing evidence and
gaps. These profiles are **experimental**, not certification of an entire SDK.

## Run

From the repository root, with the pinned mise tools installed:

```sh
mise run wasm:guest:build:deterministic
mise run test:conformance
```

No credentials or database are required. The task refuses a missing test guest.
It runs native Rust against frozen vectors, Go against the same vectors, then
native Rust against the ciphertexts Go has just produced. It never regenerates
expectations. CI also runs the Go cases on its other supported host platforms.
The same task runs the shared declaration acceptance/rejection corpus in both
languages, with native observations of normalization.

The Rust runner is `packages/stack-encrypt/tests/conformance.rs`; the Go runner
is `languages/golang/encrypt/conformance_test.go`. Go calls the record interface
used by generated code. Testing public SDK declarations is a separate layer.
Python and TypeScript runners are pending; their absence is not a passing result.

## `record-declarations-v1` contract

The [declaration grammar](declaration-grammar.md) defines the engine's current
record-plan syntax, defaults, context rules, kind/index table and null policies.
`v1/declarations.json` contains 201 cases, including malformed frames and all

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Case counts are hard-coded prose in several places: 201 here, "91 accepted and 110 refused" in declaration-grammar.md, and 212 and "33/34 CI wiring checks" in the plan. Nothing asserts them, so they go stale on the next case added. AGENTS.md has several examples of this kind of drift. Either drop the numbers or assert them, for example in stack-conformance-ci.test.mjs.

21 kinds against five scalar indexes. The resolver is explicitly `none`; EQL
targets need their own profile.

The runners are `packages/stack-encrypt/tests/declarations.rs` and
`languages/golang/encrypt/declaration_conformance_test.go`. Both call the engine
without a cipher. Valid-frame declaration diagnostics agree exactly; malformed
frames document the different native-parser and WASI-boundary classifications.
Native Rust also checks normalization observations. Go's checker does not expose
the parsed plan, so it asserts acceptance and diagnostics.

## `record-scalars-v1` contract

`v1/cases.json` has a corpus format `version`, a semantic `profile`, a public
deterministic-provider `seed`, a record `context`, a `field`, an exact list of
`required_capabilities`, and uniquely identified `cases`. Unknown fields,
versions, profiles, capabilities and input types fail. Every case must run.

The profile fixes a one-field record with ciphertext (`c`) and equality (`eq`)
outputs. Its field context is the record context followed by the field name.
Each case declares the field's `kind` and a typed `input`. `input.text` is the
literal string value, canonical base-10 integer, `true`/`false`, or empty text
for null, as determined by `input.type`. Runners construct real typed host
values, preserving integer width. JSON numbers are deliberately not used for
values: future TypeScript runners must not pass 64-bit integers through Number.

The initial positives cover int32 and int64 bounds, uint64 max, an integer
above JavaScript's safe-integer limit, Unicode text and empty text. For each:

1. The encoded typed frame must equal the frozen frame, including its type.
2. Record encryption and opening must preserve both value and type.
3. An equality query must produce one exact `eq` operand, encoded as `bytes`,
without an EQL domain. Its bytes must equal the frozen equality term and
the term stored with the record.
4. The frozen native owned-scalar ciphertext must open through the record
path. Native Rust additionally opens it through the owned scalar API.
5. Fresh Go ciphertext must open in Rust through both paths. Ciphertext bytes
themselves are not compared: encryption is randomized.
6. Opening with `wrong` appended to the record context (before the field name)
must refuse key retrieval as forbidden.
This expectation uses `stack_kms::DeterministicSource`, whose descriptor
binding models ZeroKMS's refusal. It is distinct from an AEAD failure and
does not imply every provider returns the same error shape.

Negative cases have `expected_error` and exercise the equality query only.
They require the exact engine diagnostic `code`, `reason`, and field name;
Go must also expose `ErrEncoding`. Native Rust verifies rejection occurs
before data-key generation or retrieval. String/bool inputs for an integer
field and a null query are currently covered. Error prose is not contractual.

Boolean equality is not admitted by the current engine. Boolean scalar
encryption, nullable records, other numeric kinds, bytes, JSON, EQL targets,
other operators, declarations, cancellation and provider failures need later
profiles or cases. The full SQL behavior is not part of this initial profile.

## Artifacts and evolution

`v1/native.json` is a frozen artifact set produced by the Rust owned scalar API.
It records `version`, `profile`, `producer`, and a case-ID map containing hex
`frame`, `ciphertext` and `equality` bytes. The map must contain exactly every
positive case; missing or extra cases fail. Every key and plaintext here is
public test material. The provider seed is not a production key mechanism.

The runners accept `STACK_CONFORMANCE_OUTPUT` to export fresh artifacts; Rust
accepts `STACK_CONFORMANCE_INPUT` to validate another producer. The task isolates
these variables and sets `STACK_CONFORMANCE_REQUIRED=1`, preventing a missing
Go test guest from turning conformance into a skipped test. Ordinary Go test
runs may skip this suite when the guest has not been built.

To intentionally create or update native artifacts:

```sh
STACK_CONFORMANCE_UPDATE=1 mise x --env test -- cargo nextest run --locked \
-p stack-encrypt --all-features --test conformance
mise run test:conformance
```

Review the case and artifact diff together. Never regenerate artifacts merely
to make a regression pass. Existing IDs retain their meaning; incompatible
expectations require a new profile. Corpus version 1 is a test format, not a
production wire version. Ciphertext/storage versions remain owned by their
existing codecs.

Both runners currently share the Rust engine. This checks host value conversion,
native/WASI interoperability and regressions against frozen evidence; it cannot
detect every bug shared by both paths. Independent SQL oracles, broader negative
cases, release-version matrices and richer version reports remain in the plan.
51 changes: 51 additions & 0 deletions conformance/stack-encrypt/coverage.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# Coverage map

Snapshot: PR #1404 at `b684a217e`, including the JSON matching work through #1452.
This is an inventory of evidence, not a claim that every suite was rerun here.
Paths are relative to the repository root. New profile cases run from `v1/`.

| Requirement | Existing evidence | Remaining work |
|---|---|---|
| Scalar value widths and lossless transport | `packages/stack-encrypt/tests/value_kinds.rs`; `languages/golang/encrypt/value_kinds_test.go`; initial shared scalar cases | Expand shared cases to decimals, dates, timestamps, floats, bytes and all widths; Python/TS mappings |
| Owned opaque ciphertext and storage envelope | `packages/stack-encrypt/tests/scalar.rs`, `frozen_bytes.rs`; initial cross-opening cases | Blob profile; encoded targets; supported version matrix |
| Record declaration and lowering | [Declaration grammar](declaration-grammar.md); `v1/declarations.json` shared by native Rust and Go; native normalization observations; existing record lowering suites | EQL-resolver profile; SDK-to-declaration mappings; runtime source/read shapes |
| Context binding and composition | `packages/stack-encrypt/tests/target.rs`, `descriptor.rs`, `record_lowering.rs`; `languages/golang/encrypt/context_field_test.go`; initial wrong-context cases | Shared composition vectors; expected-context and required-input matrix |
| Query terms and diagnostics | `packages/stack-encrypt/tests/query_operators.rs`; `languages/golang/encrypt/query_test.go`; initial equality/query-error cases | Common rejection corpus for every operator, capability and input shape |
| Missing, SQL NULL and sealed null | `packages/stack-encrypt/src/dynamic/record.rs` null tests; `languages/golang/encrypt/null_test.go` | Explicit language mappings and common missing/null fixtures |
| Fresh cross-language storage | `.github/workflows/tests-golang.yml` exports fresh Go EQL rows for Rust; initial scalar artifact exchange | Every producer/consumer/backend pair and supported released versions |
| JSON queries, extraction and tampering | `packages/stack-encrypt/tests/json_tree_vectors.rs`; `packages/eql/tests/encryption/tests/json_tree.rs`; `languages/golang/encrypt/json_tree_test.go`, `json_postgres_test.go` | Formal operation profile; JSON vectors consumed by all runners |
| Element and predicate matching | Go `json_elements_test.go`, `json_match_test.go` and corresponding PostgreSQL tests; Rust/SQL predicate suites | Map exact supported grammar and documented semantic differences to case IDs |
| Generated SDK declarations | Go stashgen and generated-code fixture tests | Python and TypeScript public-API runners; compiler/type-checker cases |
| Validation before key I/O and batching | Rust `tests/common::CountingSource` and record/query tests; guest preflight tests | Requirement-by-requirement coverage; common provider observations |
| Binding lifecycle and cleanup | `packages/stack-guest-abi`; Go `cancel_test.go`, `step_test.go`, guest memory tests | Native Python/TS and WASM backend-specific matrices; no blanket host-erasure claim |
| Version/capability negotiation | Existing ABI status table and target/operator discovery | Normative supported-version rules; reject unknown required capabilities |

Python and future TypeScript runners have no evidence in this checkout. Their
absence is a delivery gap, not a test skip that counts toward certification.

## Consolidation policy

Shared semantic expectations should be maintained once and executed by each
language runner. Conformance can replace duplicate case tables and fixture
maintenance. It does not eliminate the need to execute the Rust and Go paths.
No existing test has been deleted in these first two slices.

| Existing evidence | Relationship to conformance | Consolidation action |
|---|---|---|
| Rust `dynamic/record.rs` tests `a_malformed_plan`, `a_null_is_refused_as_its_key`, `a_missing_or_null_index_payload_is_the_default` | Many inputs and diagnostic outcomes are now shared declaration cases | Move the remaining cases and retain any unique display/low-level assertions before removing duplicate rows |
| Rust kind/index admission tests | The corpus has the complete 21-kind/five-index acceptance matrix | Share case data; retain direct tests that compare declaration admission with actual term execution |
| Rust `value_kinds.rs`, Go `value_kinds_test.go` | Already share `value_kinds.json`; cover more types and generated SDK code than the initial scalar profile | Enroll that existing fixture under named requirements instead of copying it; keep generated-code assertions |
| Rust `query_operators.rs`, Go `query_test.go` | Already share operator vectors, including verification and target limits | Reuse their fixture and cases when adding an operator profile; preserve host/generated query mapping tests |
| Go checker and host validation tests in `guest_test.go` | Exercise host rejection and diagnostic forwarding through public checker methods | Keep adapter-specific assertions; raw-frame conformance bypasses host validation deliberately |
| Native scalar/record round trips and cross-opening | Some behavior overlaps, but existing tests cover more types and API paths | Replace only exact duplicates after matching requirements and execution paths |

Keep Rust compile-fail, ownership/zeroization, batching, fuzz/property, and
implementation regression tests. Keep Go generation, host type conversion,
error wrapping, cancellation and guest lifecycle tests. Keep independent
PostgreSQL oracles and live-provider tests. These observe guarantees a shared
engine corpus cannot prove by itself.

For each removal, link the old assertion to stable case IDs, compare coverage
and failure behavior, and run both runners in CI. Preserve unrelated assertions
from mixed tests. Enroll Python and TypeScript against the existing corpus so
adding a language does not create another copy of the expected behavior.
Loading
Loading