# Test Rewrite Spec — async turn.js writes own logs

## New turn.js API

ALL mutating funcs now `async`, take `ctx` last param, write encounter + log internally, return `newEnc`.

```js
// OLD
const r = nextTurn(enc);
enc = { ...enc, ...r.patch };
// caller had to write log separately

// NEW
enc = await nextTurn(enc, ctx);
// encounter + log already written inside func
```

### Func signatures (all async unless noted)
- `startEncounter(enc, ctx)` → newEnc
- `nextTurn(enc, ctx)` → newEnc
- `togglePause(enc, ctx)` → newEnc
- `addParticipant(enc, participant, ctx)` → newEnc
- `addParticipants(enc, newParticipants[], ctx)` → newEnc
- `updateParticipant(enc, id, updatedData, ctx)` → newEnc
- `removeParticipant(enc, id, ctx)` → newEnc
- `toggleParticipantActive(enc, id, ctx)` → newEnc
- `applyHpChange(enc, id, changeType, amount, ctx)` → newEnc (no-op = returns enc unchanged, no write)
- `deathSave(enc, id, type, n, ctx)` → `{ enc, status, isDying }` (object now!)
- `toggleCondition(enc, id, conditionId, ctx)` → newEnc
- `reorderParticipants(enc, draggedId, targetId, ctx)` → newEnc (no-op = returns enc, no write)
- `endEncounter(enc, ctx)` → newEnc

### Pure helpers (sync, unchanged)
- `makeParticipant`, `buildCharacterParticipant`, `buildMonsterParticipant`
- `generateId`, `rollD20`, `formatInitMod`
- `sortParticipantsByInitiative`, `syncTurnOrder`, `computeTurnOrderAfterRemoval`
- `snapshotOf`, `buildEntry`

### Display lifecycle (async, no combat log)
- `activateDisplay({campaignId, encounterId}, ctx)` → void
- `clearDisplay(ctx)` → void
- `toggleHidePlayerHp(currentValue, ctx)` → void

## Test helper: shared/tests/_helpers.js

```js
const { createMockStorage, mockCtx, readyEnc } = require('./_helpers');
const { storage, ctx } = mockCtx();  // ctx = {storage, encPath:'encounters/e1', logPath:'logs', displayPath:'activeDisplay/status'}
const enc = readyEnc();  // {id:'e1', name:'TestEnc', round:0, ... 2 participants p1(Bob,init10) p2(Mob,init5)}
```

### Mock storage methods
- `storage.calls` — array of `{fn, path, data}` ordered
- `storage.logs()` — array of log entries written (addDoc data)
- `storage.updatesFor(prefix)` — updateDoc calls matching path prefix
- `storage.docs` — Map path→data

## Transform patterns

### Pattern 1: state assertion
```js
// OLD
const r = startEncounter(enc);
expect(r.patch.round).toBe(1);

// NEW
const newEnc = await startEncounter(enc, ctx);
expect(newEnc.round).toBe(1);
```

### Pattern 2: log assertion
```js
// OLD
const r = startEncounter(enc);
expect(r.log.type).toBe('start_encounter');

// NEW
const newEnc = await startEncounter(enc, ctx);
expect(storage.logs()).toHaveLength(1);
expect(storage.logs()[0].type).toBe('start_encounter');
```

### Pattern 3: chained calls (sequence)
```js
// OLD
let enc = readyEnc();
enc = { ...enc, ...startEncounter(enc).patch };
enc = { ...enc, ...nextTurn(enc).patch };

// NEW
let enc = readyEnc();
enc = await startEncounter(enc, ctx);
enc = await nextTurn(enc, ctx);
```

### Pattern 4: no-op
```js
// OLD (returned {patch:null, log:null})
const r = applyHpChange(enc, 'p1', 'damage', 0);
expect(r.patch).toBeNull();

// NEW (returns enc unchanged, no write)
const newEnc = await applyHpChange(enc, 'p1', 'damage', 0, ctx);
expect(newEnc).toBe(enc);  // same ref = no write
expect(storage.calls.filter(c => c.fn === 'updateDoc')).toHaveLength(0);
```

### Pattern 5: deathSave (returns object now)
```js
// OLD
const r = deathSave(enc, 'p1', 'success', 1);
expect(r.status).toBe('pending');

// NEW
const { enc: newEnc, status, isDying } = await deathSave(enc, 'p1', 'success', 1, ctx);
expect(status).toBe('pending');
```

## Static guards

- `static.no-sort.test.js` — KEEP. Still valid (scans turn.js for stray `.sort(`).
- `static.no-unlogged.test.js` — DELETE. Old `{patch, log}` shape gone. Every func writes log internally via `commit()`. No way to "forget".
- `static.eslint.test.js` — KEEP.

## Constraints

- KEEP test names (describe/it blocks) — same coverage intent.
- KEEP assertions — same values checked, just via newEnc or storage.logs().
- Fresh `mockCtx()` per test (isolation).
- For seeded-RNG combat tests: keep the LCG seed + rand helpers, just swap call pattern.
- `await` every mutating func call.
