---
id: ST-10
title: "Wire-format integration test: serde roundtrip for every newtype"
status: Pending
type: implementation
blocked_by: [ST-2, ST-3, ST-4, ST-5, ST-6, ST-7, ST-8, ST-9]
unlocks: []
severity: High
target_repo: plexus-substrate
---

## Problem

ST-2 defines every cross-boundary newtype with `#[serde(transparent)]`. ST-3 through ST-9 apply these types to activation surfaces. The wire-format preservation promise ("old clients ingesting new schemas still work; new serializers produce output old clients can parse") is only as strong as the tests that check it. A single missing `#[serde(transparent)]` would silently wrap a value as `{"0": "abc"}` instead of `"abc"` — undetected without a round-trip fixture.

This ticket pins wire-format byte-identity via fixtures and a dedicated integration test crate.

## Context

Test file: `tests/strong_typing_wire_format.rs` (new integration test — file does not exist yet).

Fixture directory: `tests/fixtures/` — per-activation JSON fixtures committed by each migration ticket (ST-4 through ST-9) plus new fixtures specifically for each newtype added here if not otherwise covered.

The strong-typing skill's wire-format rule:

> `#[serde(transparent)]` — Serializes as bare string, not `{"0": "value"}`

Per-newtype verification:

| Newtype | Wire form | Test form |
|---|---|---|
| `SessionId` | bare JSON string | serialize → `"\"abc\""`; deserialize from `"\"abc\""` → value |
| `GraphId` | bare JSON string | same |
| `NodeId` | bare JSON string | same |
| `StreamId` | bare JSON string (UUID canonical) | same |
| `ApprovalId` | bare JSON string (UUID canonical) | same |
| `ToolUseId` | bare JSON string | same |
| `TicketId` | bare JSON string | same |
| `WorkingDir` | bare JSON string (path form) | same |
| `ModelId` | bare JSON string | same |
| `TemplateId` | bare JSON string | same |
| `BackendUrl` | JSON object `{ protocol, host, port }` | object roundtrip |
| `BackendProtocol` | JSON string `"ws"` / `"wss"` | same |
| `PluginId` (Mustache-local) | bare JSON string (UUID canonical) | same |

Additionally: integration-level fixture comparison against pre-migration fixtures committed by ST-4 (Orcha), ST-5 (Lattice), ST-6 (ClaudeCode), ST-7 (Loopback), ST-8 (Cone), ST-9 (Registry + Mustache).

## Required behavior

The integration test performs the following observable checks:

| Check | Expected |
|---|---|
| Per-newtype: serialize a constructed value via `serde_json::to_string(&v)` | Matches the "wire form" column above for that newtype |
| Per-newtype: deserialize the wire form back via `serde_json::from_str::<T>(wire)` | Returns a value equal to the original |
| Cross-type swap negative: a `SessionId` serialized and then deserialized as `StreamId` (different wrapped type: String vs Uuid) | Fails with a deserialization error |
| Cross-type swap negative between same-wrapped newtypes (e.g., `SessionId` serialized as JSON string `"abc"` deserialized as `ToolUseId`) | Succeeds at the serde level — both wrap `String` — BUT the test documents this with a comment explaining that the compiler, not serde, provides the swap-safety. This is a documentation check, not a behavioral one. |
| Fixture roundtrip: load each committed `tests/fixtures/*.json` from ST-4 through ST-9, deserialize into the activation's domain type, re-serialize, and compare byte-for-byte | Must match exactly |
| `#[serde(transparent)]` regression gate: for each string/uuid-backed newtype, assert that `serde_json::to_value(&v)` returns `Value::String(_)` (not `Value::Object(_)` or `Value::Array(_)`) | String/UUID newtypes produce `Value::String` |
| `BackendUrl` object roundtrip: serialize `BackendUrl { protocol: Wss, host: "h".into(), port: 443 }` | Produces JSON object with fields `protocol`, `host`, `port`; deserializes back to equal value |
| Pre-migration compatibility: a fixture containing the OLD bare-string JSON for `SessionId` (e.g., `"\"session-abc\""`) deserializes successfully into the new `SessionId` newtype | Yes — this proves old clients remain compatible |

## Risks

- **Missing fixtures.** Each migration ticket (ST-4 through ST-9) is expected to commit its own fixture files. If any are missing when ST-10 lands, this ticket identifies the gap in acceptance criterion #5 and blocks until the owning ticket supplies the fixture.
- **Forgotten `#[serde(transparent)]`.** The regression-gate check (item 6 in the Required Behavior table) catches this. If any ST-2 newtype produces `Value::Object(_)` instead of `Value::String(_)`, the test fails and ST-2 must be re-opened.
- **`WorkingDir` on non-UTF8 paths.** Acceptance is limited to UTF-8 paths; the fixture uses a valid UTF-8 path like `"/tmp/work"`. Document in the test.
- **`Uuid` canonical form.** The UUID canonical string form (`550e8400-e29b-41d4-a716-446655440000`) is what `serde_json` emits for `Uuid`. `StreamId` / `ApprovalId` must match this byte-for-byte.

## What must NOT change

- ST-2 through ST-9 implementations — this ticket only adds tests and fixtures.
- Existing test infrastructure — new integration test goes in `tests/strong_typing_wire_format.rs`.
- Any serde trait implementations.

## Acceptance criteria

1. `cargo test -p plexus-substrate --test strong_typing_wire_format` succeeds with all checks above.
2. `cargo test -p plexus-substrate` (full suite) succeeds.
3. Each of the twelve identifier newtypes (`SessionId`, `GraphId`, `NodeId`, `StreamId`, `ApprovalId`, `ToolUseId`, `TicketId`, `WorkingDir`, `ModelId`, `TemplateId`, `BackendProtocol`, `PluginId`) has at least one positive roundtrip check and one `#[serde(transparent)]` regression-gate check. `BackendUrl` has an object-form roundtrip check.
4. At least one fixture per activation (Orcha, Lattice, ClaudeCode, Loopback, Cone, Registry, Mustache) is loaded from `tests/fixtures/*.json`, deserialized, re-serialized, and compared byte-for-byte.
5. A pre-migration compatibility check exists for at least `SessionId`, `StreamId`, and `ApprovalId`: a fixture JSON fragment representing the old bare-string shape deserializes into the new newtype successfully.
6. The test source includes comments explaining that swap-safety for same-wrapped newtypes (e.g., `SessionId` vs `ToolUseId` both wrapping `String`) is compile-time, not runtime — serde cannot distinguish them, and that is expected.

## Completion

Implementor delivers:

- A commit adding `tests/strong_typing_wire_format.rs` and any missing fixtures under `tests/fixtures/`.
- `cargo test -p plexus-substrate --test strong_typing_wire_format` green.
- Full `cargo test -p plexus-substrate` green.
- Ticket status flipped from `Ready` → `Complete`.
- ST-1 epic marked Complete in its frontmatter.
