Skip to content

e2e: pin the SDK control-plane structs to the server's JSON shape - #357

Merged
CMGS merged 6 commits into
mainfrom
test/sdk-server-wire-parity
Oct 17, 2026
Merged

CMGS merged 6 commits into
mainfrom
test/sdk-server-wire-parity

Conversation

@CMGS

@CMGS CMGS commented Oct 17, 2026

Copy link
Copy Markdown
Contributor

Stacked on #356 (it moves the server-only DTOs into package server; this test reads them there). Merge #356 first.

What

  • e2e: one table-driven test pairs 38 SDK request/reply structs with their sandboxd counterparts and compares JSON field names, plus omit options for the structs the SDK encodes. Deliberate differences sit in a per-pair allowlist; a stale allowlist entry fails too.
  • types: VolumePatch.add/remove become omitzero, matching the SDK (the one real divergence the test found), with an e2e regression test that sends [] / [] to a real server.
  • mcp: serverInfo.version reports the release version via -X main.version (goreleaser), as sandboxd does; it was a hard-coded 0.1.0.
  • wire: export DecodeData; the engine's private copy of the bulk data-frame decode (fastPortData, portDataHead) is deleted.

Why

The SDK hand-mirrors the server's wire structs and nothing kept them in step; the per-feature *WireShape tests only caught drift after the fact. The types stay where they are: wire is the silkd frame contract and the server types carry validation.

Allowlisted on purpose: server-only claim.egress, fork.claim_ref_prefix/on_expire, renew.on_expire, node-local checkpoint and summary fields, config-owned PoolSpec fields, and PoolSpec.net/size and TenantSpec.token (omit option differs by design).

VolumePatch

An empty or absent add/remove means "change nothing" on the server, so both shapes decode the same. omitempty on the server type disagreed with the SDK's omitzero, which sends an explicit [] and omits only nil; the SDK side is kept so a caller's [] is never dropped on the wire.

Tests

  • TestSDKStructsMatchServerWireShape (mutation-checked: renaming an SDK tag fails it with the pair and field).
  • TestEmptyVolumePatchChangesNothing, TestDecodeDataTakesOnlyCanonicalDataFrames, mcp initialize version assertion.
  • make go-lint: 10 x 0 issues.; make go-test green; asl -forwarder=false clean on e2e, sandboxd, mcp, protocol/wire for linux and darwin.

Hot-path cost

Only the DecodeData change touches a hot path (guest port read). A/B, both arms interleaved in one binary with the order swapped, 20 samples per arm:

frame old ns/op new ns/op allocs/op
1 KiB 562.5 566.0 0 / 0
64 KiB 34,640 36,130 0 / 0
256 KiB 144,000 144,800 0 / 0

benchstat reports no significant difference (p 0.51 to 0.80). No new work on the claim path.

@CMGS
CMGS force-pushed the refactor/api-vocabulary-placement branch from f808535 to 3edf2d7 Compare October 17, 2026 16:28
@CMGS
CMGS force-pushed the test/sdk-server-wire-parity branch from 2106888 to 4cbd937 Compare October 17, 2026 16:28
@CMGS
CMGS deleted the branch refactor/api-vocabulary-placement October 17, 2026 16:57
@CMGS CMGS closed this Oct 17, 2026
@CMGS CMGS reopened this Oct 17, 2026
@CMGS
CMGS changed the base branch from refactor/api-vocabulary-placement to main October 17, 2026 16:57
CMGS added 6 commits October 18, 2026 00:58
The SDK hand-mirrors about 40 request and reply structs from sandboxd, and
only the per-feature WireShape tests noticed drift, after the fact. One
table now pairs each SDK struct with its server counterpart and compares
the JSON field names; for structs the SDK encodes it also compares the
omit options. The SDK side is read from source with go/parser because most
of the mirrors are unexported.

Each pair carries an allowlist of fields that deliberately differ: server
only fields (ClaimRequest.egress, ForkRequest.claim_ref_prefix and
on_expire, RenewRequest.on_expire, the node-local Checkpoint and
SandboxSummary.token fields, the config-owned PoolSpec fields) and omit
options that differ by design (PoolSpec net and size default on the server,
TenantSpec token keeps the stored token when absent). An allowlist entry
that no longer differs fails too, so the list cannot go stale.

VolumePatch add and remove differ (omitzero in the SDK, omitempty on the
server); they are allowlisted here and settled in the next commit.
The parity test found the SDK encoding VolumePatch with omitzero while the
server type said omitempty. The SDK is the side to keep: an explicit empty
slice goes on the wire as [] and a nil one is omitted, the same contract
SetVolumes documents, and sdk/go already pins it. omitempty would drop a
caller's [] silently.

The server reads [] and absent alike (both mean change nothing, not detach
all), so no decoded request changes; only the declared shape now agrees.
The new e2e test sends an empty patch to a real server and checks the held
volume stays attached.
The initialize reply hard-coded 0.1.0 while the repo is at v0.1.17. sandboxd
already takes its version from a main.version variable set by goreleaser
ldflags; mcp now does the same, so the release build stamps {{.Version}} and
a plain go build or go test reports "devel". Reading the module version from
runtime/debug.ReadBuildInfo was rejected: for a binary built inside this
multi-module repo the main module version is "(devel)", so it would add a
fallback path that never carries the release number.
sandboxd/engine re-implemented the canonical data-frame decode that
wire.decodeBulk already owns, only to skip the &DataResp{} that
DecodeResponseInto builds. wire.DecodeData exposes the same decode on the
data head, so the engine's copy (fastPortData, portDataHead) is deleted
and a change to the bulk frame shape now lands in one place.

A/B of the port read decode, both arms interleaved in one binary with the
order swapped each round, 10 rounds per order (count 20 per arm), 1, 64
and 256 KiB frames: allocs/op 0 vs 0 on every size; sec/op 562.5n vs 566.0n,
34.64u vs 36.13u, 144.0u vs 144.8u, all within noise (p 0.51 to 0.80).

The engine still passes the line without its trailing newline, since the
wire decoder expects a bare frame.
@CMGS
CMGS force-pushed the test/sdk-server-wire-parity branch from 60ac2b6 to 8432d63 Compare October 17, 2026 17:00
@CMGS
CMGS merged commit 03cde50 into main Oct 17, 2026
3 checks passed
@CMGS
CMGS deleted the test/sdk-server-wire-parity branch October 17, 2026 17:04
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant