Skip to content
Merged
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
19 changes: 16 additions & 3 deletions conformance/stack-encrypt/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ From the repository root, with the pinned mise tools installed:

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

Expand All @@ -20,21 +21,31 @@ 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. It also runs the shared
runtime cases described below.
runtime and read cases described below.

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-reads-v1` contract

The [read contract](read-contract.md) defines cases in `v1/reads.json` for
malformed stored records, authenticated read-type failures and context preflight.
The native runner observes retrieval stages. Go exercises the same cases through
an uninitialised guest and a configured cipher, plus the normal record adapter
for representable cases. The contract records the static-context combination
that the host refuses before calling the guest. The runners are `tests/read_conformance.rs` in the Rust
crate and `encrypt/read_conformance_test.go` in the Go module.

## `record-runtime-v1` contract

The [runtime contract](runtime-contract.md) defines cases in `v1/runtime.json`:
missing and explicit null values, SQL-null omission, sealed-null storage and
equality, typed batch round trips, write validation and lossless query conversions.
The runners are `packages/stack-encrypt/tests/runtime_conformance.rs` and
`languages/golang/encrypt/runtime_conformance_test.go`. Native Rust also checks
data-key call counts; Go records where its host adapter refuses inputs earlier.
data-key call counts; Go host source validation preserves shared structured diagnostics.

## `record-declarations-v1` contract

Expand All @@ -49,7 +60,9 @@ The runners are `packages/stack-encrypt/tests/declarations.rs` and
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.
the parsed plan, so it asserts acceptance and diagnostics against both release
and deterministic guests. Representable cases also exercise `Checker.Check`
through explicit host plan fixtures in `v1/go-declarations.json`.

## `record-scalars-v1` contract

Expand Down
22 changes: 20 additions & 2 deletions conformance/stack-encrypt/coverage.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,20 +39,38 @@ batches; Go provider-call instrumentation remains open.
| Native source validation tests | `late-wrong-type`, `late-missing`, `unknown-field` | Repeated keys, unsupported frame shapes and more output kinds |
| Existing shared value-kind/operator fixtures | Runtime query cases cover selected conversion edges | Full type/operator matrices, generated SDK calls and verification |

## Read checkpoint

`v1/reads.json` adds cases for stored-shape validation, authenticated types
and context preflight. The [read contract](read-contract.md) distinguishes native
shape/pending stages and the guest's earlier `check_record` diagnostics. Native
data-key counters and Go's uninitialised guest verify preflight boundaries.

| Existing evidence | Shared case IDs | Retained assertions |
|---|---|---|
| Native `a_stored_record_that_does_not_fit` | `missing-field`, `missing-ciphertext`, `field-not-map`, `plaintext-under-c`, `row-not-map`, `late-missing-field` | Repeated keys, other passthrough fields and output kinds |
| Native typed read/null tests | `authenticated-int32-as-*`, `authenticated-null-as-required`, `authenticated-null-as-sql`, `sql-absent-*` | Wider types and declaration combinations |
| Go `context_field_test.go`, native context tests | `context-unrestricted`, `context-expected`, `context-mismatch`, `context-late-mismatch`, `context-drop-tenant`, `context-tenant-*` | Generated APIs, queries, extension composition and stored-context tampering |
| Native and guest context-source validation | `static-context-with-expectation` | Other source combinations and empty-batch behavior |

These are overlap mappings, not grounds for deleting whole mixed-purpose tests.
Repeated-key trees cannot be emitted by Go's map-based tree encoder; a future
raw-byte profile must cover that boundary explicitly.

## 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 three slices.
No existing test has been deleted in these first four 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 |
| Go checker and host validation tests in `guest_test.go` | Exercise host rejection and diagnostic forwarding through public checker methods | Keep adapter-specific assertions; representable declaration cases also use `Checker.Check`, while raw frames observe the guest boundary |
| 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
Expand Down
126 changes: 126 additions & 0 deletions conformance/stack-encrypt/read-contract.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,126 @@
# Record read profile

`record-reads-v1` adds shared cases in `v1/reads.json` for stored record
shapes, authenticated type checks and context preflight. It fixes the resolver
to `NoTargets`. It is experimental and covers native Rust and Go WASI.

## Inputs and observations

Each case has separate `write` and `read` declarations, each naming the `kind`
and `null` policy of `value`. Outputs are ciphertext and equality. `rows` uses
the runtime profile's typed scalar representation. Runners first encrypt these
rows successfully, then apply the named `mutation` to its zero-based `row`.
This supplies real ciphertext, including for negative reads.

Without `context_field`, the context is `["conformance", "value"]`. With it,
`tenant` is a string passthrough supplying the row context; `value` is its
field identity. An optional `expected_context` constrains the stored context.
Omit it for an unrestricted read; when present it must be a nonempty plain
label string. Explicit JSON null is invalid, not another spelling of omission.

`expected` describes the native opener's stage, complete diagnostic code and
payload on refusal, typed ordered rows on success, and data-key retrieval
call count. Runners must reject unknown stages, mutations, capabilities,
profiles and input types. Cases have unique, nonempty IDs.

| Stage | Native observation | Retrieval calls |
|---|---|---|
| `shape` | `record::decrypt` refuses before returning a pending operation | 0 |
| `preflight` | The pending opener refuses before requesting keys | 0 |
| `authenticated` | Shape checking passes; opening the authenticated value finds a type mismatch | 1 |
| `success` | Opened rows match all expected values, types and ordering | 1 when ciphertext is present; 0 for the all-absent SQL-null case |

Counters are reset after fixture encryption. Reads must never generate data
keys. These counts observe native data-key calls, not index-key initialization
or Go's logical provider calls.

`check_error` independently describes `record::check_record`, which needs no
cipher. An absent `check_error` requires acceptance. The Go guest runs this
check before looking up its cipher state. Both an uninitialised guest and a
configured cipher execute each case (subject to the host context refusal
described below): guest preflight errors must have the specified
diagnostic in either; accepted shapes must reach `ErrState` without a cipher.
Consequently, a concealed type mismatch must pass preflight and fail only when
the active guest opens it.

The Go runner uses a test-only entry point to the existing `se_decrypt_record`
export. This permits shapes the host's `record.Sealed` type cannot express,
including a passthrough directly under `c`. It uses the binding's existing
tree codec, Cipher validation/context/extension, production decoding and
transport-buffer cleanup. Only the uninitialised checker deliberately bypasses
host plan checks to observe the guest boundary. Cases marked `adapter` also
run the normal `Cipher.Open` path; they retain their original stored shape.
This does not certify generated SDK mappings.

## Required behavior

- Missing required fields, missing ciphertext outputs, non-map rows or field
outputs, and plaintext passthroughs under ciphertext are refused before keys.
- A malformed later row rejects the batch before retrieval.
- Reads project the fields and outputs the plan opens. Extra stored fields,
extra outputs and missing equality outputs do not prevent reading ciphertext.
Writes still reject unknown source fields, as the runtime profile requires.
- Reading int32 ciphertext as int64, uint64 or string fails after retrieval.
Reads do not apply the query path's lossless numeric conversion.
- A sealed null cannot open through a required declaration. It can open through
a SQL-null declaration, with retrieval because ciphertext is present.
- An absent SQL-null field opens as null without retrieval. A required or
sealed-null read declaration refuses the same missing field.
- Context-field reads work without an expected label or with a matching label.
A mismatch, including one in a later row, refuses the whole batch before keys.
- Missing context fields, missing passthrough payloads and non-string context
values fail before retrieval.
- A static-context plan cannot also take a call context. Native `check_record`
and the guest report `dynamic_record` with reason `refused`; the native pending
opener reports `plan_two_context_sources`, naming the plan and the call.
The corpus records both engine diagnostics explicitly. Go's configured
`Cipher.Context(...).Open` refuses this combination earlier in `Cipher.plan`
with `ErrEncoding`. The case also exercises that host refusal through both
`Open` and the validated tree hook; it does not require its English wording
or the absence of a diagnostic.

Context-mismatch diagnostic payloads expose only stored descriptor length and
part count, not its value. Go diagnostic numeric metadata is compared as JSON
numbers; plaintext comparisons retain actual host types.

## Mutation vocabulary

| Mutation | Change to the selected row |
|---|---|
| `none` | Preserve it |
| `drop_value` / `drop_c` / `drop_eq` | Remove the named field or output |
| `field_plain` / `c_plain` / `row_plain` | Replace that node with an int32 passthrough |
| `extra_field` / `extra_output` | Add an `extra` int32 passthrough at that level |
| `drop_tenant` | Remove the context field |
| `tenant_no_payload` | Replace its output map with an empty map |
| `tenant_number` | Replace its passthrough string with an int32 |

## Scope and consolidation

The existing native record tests retain repeated keys, invalid leaf framing,
other tree forms, context extensions and wider type/output combinations. Go's
tree encoder accepts maps with unique keys, so repeated-key transport cases
need a separate raw-byte profile rather than being silently skipped here.

The existing Go context-field suite retains generated SDK calls, query context
selection, extension behavior and stored-context tampering. The deterministic
provider's wrong-context refusal remains covered there and by the scalar
profile. This profile observes preflight context checks, not every context
composition or cryptographic tampering case.

No existing tests are removed. Python and TypeScript runners, EQL/JSON read
shapes, provider failures, version compatibility and runtime artifact exchange
remain follow-up work.

Opened row order follows the input batch. Within a row the engine emits fields
in declaration order. Both runners
construct declarations with the context field first, followed by recoverable
fields. Native expected frames use that order; Go's shared production decoder validates it
before converting objects to maps. `context_field_name` may override the default
`tenant` name when `context_field` is true; a `zone` case prevents lexical object
ordering from masquerading as declaration ordering.

Both runners require `context_field`, `adapter`, and `mutation.row` (including
explicit false/zero values), reject empty supplied `expected_context`, and reject
contradictory stage, check-error and retrieval-count expectations. The Rust runner
collects per-case failures so later cases still execute after an earlier failure.
2 changes: 2 additions & 0 deletions conformance/stack-encrypt/runtime-contract.md
Original file line number Diff line number Diff line change
Expand Up @@ -79,6 +79,8 @@ guest's lack of network requests is not proof of logical key-call counts.
The cases cover runtime writes, corresponding reads, and equality queries.
They do not cover malformed stored records, authenticated type mismatches on
read, context composition/preflight, provider failures, or other operators.
The separate [read profile](read-contract.md) adds stored-shape, authenticated
type and context-preflight cases.
Fresh cross-language artifact exchange remains the scalar profile's coverage.

Existing `value_kinds.json` and `query_operators.json` fixtures remain the
Expand Down
Loading
Loading