# Test plan — §13.5.2 knowledge-driven send model + §13 desync/corroboration

Status: tests are written FIRST (TDD). The systems are **not built**, so most of these are expected RED.
A good RED here is an **assertion failure on observable behaviour**, NOT a crash on a missing internal —
write black-box tests (converged state + the network delivery log), not calls into unbuilt methods.

The umbrella opt-in flag for the whole new model is **`knowledgeDrivenSend: true`** (constructor option).
Until it's implemented the engine ignores it and does the OLD frontier-broadcast behaviour → tests go red.
When built behind that flag, they must flip green. Every test must pass the flag where it asserts new behaviour.

THE SPIRIT (what the design is FOR — every test should defend one of these):
1. **Bandwidth minimality** — steady state is silent; you send a peer only what it provably lacks; no echo storms.
2. **Eventual delivery under loss** — per-input backoff recovers drops; corroborated inputs propagate.
3. **Corroboration correctness** — HOLDING not RECEIPT; only your OWN input can be uncorroborated; a lone input is VOID.
4. **Deterministic canon** — integer-product winner is acyclic (no Condorcet cycle) AND convergent (all adopt one);
   corroboration FREEZES at the checkpoint; the count is judged with NO AUGMENTATION.
5. **Grace gating** — uncorroborated never accepted past acceptance; corroborated accepted in grace; gate = the corroborated flag.
6. **Liveness independence** — peer-gone detection survives steady-state input silence (rides §6.1 beats).

## Shared conventions (read `DESIGN_PARTICIPATION.md` §13.5–§13.7 first)

- New file per category: `tests/desync-sendmodel-<slug>.test.js`. It lands in the DESYNC bucket
  (excluded from default `npm test`; run via `DESYNC_SPECS=1 NO_COLOR=1 npx vitest run tests/desync-sendmodel-<slug>.test.js`).
- Imports + factory:
  ```js
  import { describe, it, expect } from 'vitest';
  import { PeerHarness } from '../test-harness/PeerHarness.js';
  import { SimulationEngine } from '../SimulationEngine.js';
  const factory = (transport, opts) => new SimulationEngine(transport, opts);
  ```
- Base config pattern (copy from `tests/desync-grace-acceptance.test.js`): `summingStep`, `scripted()` inputs,
  flags `{ step, initialState:{sum:0}, getLocalInputs, snapshotIntervalTicks:20, recovery:true, syncedTick:true,
  synced:true, inputForwarding:true, knowledgeDrivenSend:true }`. Drive ticks with `h.advanceTo(t*TICK_MS)`.
- **Message observability** — the network logs every send outcome:
  ```js
  // each entry: { at, from, to, event:'deliver'|'drop', msg:JSON, reason? }
  const log = h.network.log();
  const sentBy = (id, type) => log.filter(e => e.from === id &&
      (() => { try { return JSON.parse(e.msg ?? '{}').type === type; } catch { return false; } })());
  ```
  Use this to PROVE steady-state silence, count resends, count forwards, prove a send was/ wasn't gated.
  (Inspect `MemoryTransport`/`NetworkSim` for exact message `type` strings — e.g. `em-input`, the ack type.)
- State / decoder observability: `h.peer(id).getState().sum`; `h.peer(id).inputChanges(src)` → held changes.
- Loss/topology: `h.network.setDefaultLink({latency,jitter,dropRate,duplicateRate})`,
  `h.network.setLink(a,b,{...})`, `h.partition(a,b)`, `h.heal(a,b)`.
- Avoid timing-fragile exact references: assert convergence (`sumOf(A)===sumOf(B)`) and the
  closer-to-with/without pattern (see grace test), not exact sums, where loss/timing varies.
- Every test = a natural-language doc comment (WHAT it checks, WHY it falsifies the design, what RED means today)
  THEN the code. Tests must try to BREAK the design: include edge/boundary/adversarial/invalid cases.
- DO NOT touch: `vitest.config.js`, `package.json`, any existing test file, `tests/graph-pacman-scenarios.test.js`,
  `New thoughts..txt`. Only create your one new file.
- Before finishing: run your file, confirm it EXECUTES (no syntax/harness errors) and report the red/green tally.
  Reds are expected; crashes are not — restructure any crash into a black-box assertion failure.

## Categories (one sub-agent each)

### C1 — Steady-state silence & delta-only send  → `desync-sendmodel-silence.test.js`
- On a clean link, once every input is acked, per-node input-message traffic over a long window → ~0
  (the OLD model keeps re-broadcasting the frontier forever; assert the new model goes quiet).
- An input is sent to a peer only until that peer acks/holds it, then NEVER again (delta-only).
- A fresh input goes to peers that lack it but NOT to a peer already known to hold it.

### C2 — Per-input backoff + the send-gate  → `desync-sendmodel-backoff.test.js`
- A single dropped copy is re-sent on backoff and STOPS the instant its `(peer,source,tick)` ack arrives.
- SEND-GATE: an own UNcorroborated input is never sent past the *acceptance* window; a CORROBORATED input is
  never sent past the *grace* window; but acking either is still allowed past those edges.
- A small base interval lands ≥1 retry INSIDE acceptance; an un-acked input simply ceases at its window edge
  (no endless resend, and not via a special cap — via the gate).
- Edge: successive resend intervals to the same peer GROW (geometric backoff).

### C3 — Corroboration semantics  → `desync-sendmodel-corroboration.test.js`
- HOLDING not RECEIPT: a late RAW input a peer receives but rejects is NOT held → never corroborates.
- author≠you ⇒ corroborated on hold (author+you=2). only-own-input-can-be-uncorroborated.
- VOID rule / network-beats-straggler: A's lone input (reached no one) is excluded → only A rolls back & drops it.
- GRADUATION: A's input lost to B but accepted by C becomes corroborated; the now-corroborated copy reaches B
  (automatically, via backoff), B grace-accepts it, it enters canon.
- RE-ACK: a peer re-acks an already-held input on every receipt (a witness has many delivery chances).

### C4 — Grace acceptance is a flag check (two cases)  → `desync-sendmodel-grace.test.js`
- A late input with the corroborated flag SET is grace-accepted; UNSET is rejected.
- Case 1: a forward (author≠sender) is grace-accepted. Case 2: the author's own resend carrying the flag is grace-accepted.
- A lone uncorroborated self-resend (flag unset) is REJECTED past acceptance (network beats straggler).
- Edge/adversarial: an uncorroborated input dishonestly flagged would manufacture false corroboration — assert the
  sender only sets the flag when corroborated (the `_sendOne` corroboration gate).

### C5 — Knowledge ledger + provenance + forward self-suppression  → `desync-sendmodel-knowledge.test.js`
- `known[P][X]` advances from P's ACCEPTED ack and from provenance (a copy from P, or X's authorship), and
  NEVER from an optimistic send (a sole lost copy stays a resend candidate).
- Forward self-suppression: in a full triangle, the redundant B→C forward of A's x is suppressed when C's direct
  ack/provenance beats the first forward interval (count forwards in the log → ~0 redundant).
- Real forward where needed: line topology A–B–C (A,C NOT linked) — B MUST forward A's x to C; the log proves it.
- No echo: never forward a source's input back to that source nor to a peer known to hold it.

### C6 — Canon decision: integer-product + freeze + no-augmentation  → `desync-sendmodel-canon.test.js`
- ACYCLIC: across ≥3 nodes with incomparable count vectors, `∏(1+count_p)` is a total order; A>B,B>C ⇒ A>C;
  all nodes adopt the SAME canon (pick-one, never merge). Bit-identical (BigInt, no float log).
- FREEZE: a count that grows between asserting and receiving must not change the decision — both lock the
  asserted value and still agree (construct the A-asserts-N-then-learns-N+1 race).
- NO-AUGMENTATION: judging an assert by its own asserted count → consistent winner; a judge that UNIONs the
  assert with local knowledge can be driven into a cycle (the falsifier that justifies no-augmentation).
- AUTHOR-ASYMMETRY: all-acks-lost author under-counts → its input dropped CONSISTENTLY everywhere (no split-brain).
- Count is ONE number scoped to the checkpoint window (not per-input flags).

### C7 — Acceptance / end-to-end SPIRIT  → `desync-sendmodel-acceptance.test.js`  (spec authored below; implement faithfully)
The headline behaviours that capture the whole design's intent. Lossy mesh, realistic conditions.
1. **Convergence under loss** — 3–4 peer mesh, 10% then 25% default dropRate, run ~200 ticks: ALL peers end at
   identical state (`sumOf` equal across all). Repeat across several seeds.
2. **Bandwidth win** — on a CLEAN link after warm-up, count input-type messages per node per tick in a steady
   window: with `knowledgeDrivenSend:true` it must be DRAMATICALLY lower (≈0 in steady state) than with the
   flag off (the frontier-broadcast baseline keeps chattering). Assert the ratio, not an absolute.
3. **Network beats straggler, end-to-end** — A's tick-30 change dropped to everyone during acceptance (lone) →
   converged value is CLOSER to "without"; the SAME change reaching ≥1 other node (corroborated) → CLOSER to
   "with". (Reuse the `refSum`/`closerToWith` pattern from `desync-grace-acceptance.test.js`.)
4. **Determinism under adversarial ordering** — scramble input arrival (jitter/reorder via the link) across
   permutations/seeds and assert every peer converges to the SAME state (no order-dependence).
5. **Liveness independence** — after steady-state input silence, drop a peer (partition/disconnect) and assert
   the others still flip its presence (isDisconnected / roster) within the attendance horizon — liveness did not
   die with the quiet input path.
6. **Partition + heal repair** — partition one peer for a stretch (it misses inputs and finalizes a divergence),
   heal, and assert all peers re-converge to identical state (repair-replay, transfer as the fallback).
