Contract Testing (Golden Files)
Golden-file contract tests prevent silent API drift between loom-core and its sibling consumers (loom VS Code extension, loom-zed).
How It Works
Each contract test serializes a representative struct to JSON and compares the
output against a checked-in .golden file. When a field is added, renamed, or
removed, the golden file diff surfaces the change in code review.
internal/contracts/
├── golden_test.go # Contract test functions
└── testdata/
├── mobile_envelope.golden
├── mobile_dashboard.golden
├── dto_session_info.golden
└── ... # 18 golden files total
Covered Surfaces
| Category | Golden Files | What They Guard |
|---|---|---|
| Mobile API envelope | mobile_envelope, mobile_envelope_error | Response wrapper shape (ok, data, error, meta) |
| Mobile endpoints | mobile_dashboard, mobile_agents, mobile_sessions, mobile_tasks | Dashboard, agent list, session list, task list payloads |
| SSE events | sse_events | Real-time event payloads (hud.fleet, hud.health, hud.session.*, hud.heartbeat) |
| Bridge DTOs | dto_session_info, dto_task_info, dto_presence_info, dto_entity_*, dto_relation_info, dto_context_inspect_request, dto_nudge_queue_policy, dto_heartbeat_request | Serialization shape of types in internal/hud/bridge/ |
Running Contract Tests
# Verify golden files match (CI mode — fails on drift)
make ci-contracts
# Run directly with go test
go test -v -count=1 -run 'Contract$' ./internal/contracts/...
# Update golden files after an intentional change
go test -v ./internal/contracts/... -update-golden
Workflow: Making a Breaking Change
-
Change the Go struct in
internal/hud/bridge/or the test fixture ininternal/contracts/golden_test.go. -
Run tests to see the diff:
make ci-contractsThe test output shows the JSON mismatch so you can verify the change is intentional.
-
Accept the change by updating golden files:
go test ./internal/contracts/... -update-golden -
Commit the updated
.goldenfiles alongside the code change. The diff in the golden files is the contract changelog — reviewers (and sibling repo maintainers) can see exactly which fields changed. -
Notify sibling consumers. Any golden file change may require updates in:
- loom (VS Code extension) — TypeScript interfaces in
src/types/ - loom-zed (Zed extension) — Rust structs mirroring the DTOs
- loom (VS Code extension) — TypeScript interfaces in
Workflow: Consumer Integration (loom / loom-zed)
Sibling repos should track loom-core contract changes to stay aligned:
For loom (TypeScript / VS Code)
- When reviewing loom-core MRs, check
internal/contracts/testdata/diffs. - Map changed fields to the corresponding TypeScript interfaces.
- Update the interface and any dependent code before upgrading the loom-core dependency.
For loom-zed (Rust / Zed)
- Same review process — check golden file diffs.
- Update Rust structs (typically
serde-derived) to match new shapes. - Run the Zed extension test suite before releasing.
Recommended Sibling CI Check
Sibling repos can pull the golden files and validate their own types against them at CI time:
# In loom or loom-zed CI pipeline:
# 1. Fetch golden files from loom-core (same branch or latest main)
# 2. Parse the JSON and assert your local types can round-trip it
This catches drift early — before runtime errors surface in production.
Adding a New Contract
-
Add a test function in
golden_test.gofollowing the naming conventionTest<Name>Contract. -
Construct a representative instance of the type with realistic field values (use fixed timestamps like
2025-01-15T10:30:00Zfor determinism). -
Call
assertGolden(t, "<golden_file_name>", marshalIndent(t, value)). -
Generate the golden file:
go test ./internal/contracts/... -update-golden -run TestNewContract -
Commit the new
.goldenfile.
CI Integration
The ci-contracts target runs as part of the full CI pipeline (make ci):
ci: ci-lint → ci-build → ci-test → ci-contracts → ci-security
It runs separately from ci-test so contract failures are clearly
distinguishable from unit/integration test failures in CI output.
Design Decisions
-
Replicated structs: Mobile envelope types are replicated in the test file rather than imported from
internal/hud. This is intentional — the test validates the wire format independently, so refactoring internal types doesn't silently change the contract. -
-update-goldenflag: Opt-in update prevents accidental acceptance. CI never passes this flag. -
assert.JSONEq: Comparison ignores key ordering, so struct field reordering in Go doesn't cause false positives. -
Trailing newline:
marshalIndentappends\nto satisfy end-of-file-fixer pre-commit hooks.