feat(fixtures): SSE patch + pagination scenario fixtures (K7)

Two new golden-fixture suites, expectations machine-generated from the web
implementation (same source-of-truth principle as the chat suite):

- shared/fixtures/sse/ (12 cases): session-updated versioned-patch
  application via applySessionDetailPatch — strict version gates for
  metadata/agentState/todos/teamState, out-of-order arrival, teamState:null
  clear, max-monotonic updatedAt, flat last-write-wins fields, sub-minute
  activeAt keep-alive drop, scratchlistUpdatedAt trigger, and the pinned
  actual behavior that activeTurnStartedAt is NOT applied by the patch path.
  Inputs are stored schema-normalized and validated against SessionSchema /
  strict SessionPatchSchema at generation time; per-patch applied/unchanged
  verdicts are part of the contract.

- shared/fixtures/pagination/ (11 cases): op scripts driving the real
  message-window store with a scripted ApiClient — latest page + SSE ingest,
  before-cursor older pages, epoch-mismatch reset (window discard + recorded
  internal latest request), reset:true replace preserving optimistic rows,
  localId echo reconciliation, messages-consumed invokedAt stamping (no
  cursor advance), message-cancelled removal, cancel-too-late invoked-row
  ingest, hidden-row cursor advance, 400-row trim preserving queued rows,
  and queued-state gap recovery. Documents pin the exact requests the store
  issued, older-load outcomes, reconcile candidates, and a minimal window
  projection (ids/order, queued/optimistic flags, hasMore, epoch, viewMode,
  compound cursors).

Generator: web/scripts/fixtures/{sse,pagination}/ extend the K5 framework
(canonical serialization reused; suites pruned of stale files). Web
self-conformance tests replay every fixture against the real implementation
in bun run test:web. README documents schemas, projections, replay
contracts, and native consumption for both suites.

No time injection needed: the store's only Date.now() reads gate
notification throttling, which never reaches persisted or projected state.
This commit is contained in:
weishu
2026-08-17 12:06:57 +08:00
parent 81146e68c5
commit e1bfdd6265
36 changed files with 14424 additions and 71 deletions
+220 -50
View File
@@ -1,26 +1,75 @@
# Golden chat fixtures
# Golden protocol fixtures
Machine-generated conformance fixtures for the HAPI chat rendering pipeline.
The **web implementation is the source of truth**: every file under
`chat/` is produced by running the real web pipeline
(`web/src/chat/`: `normalizeDecryptedMessage` → `reduceChatBlocks` →
`buildVisibleChatBlocks`) over hand-authored wire inputs, then applying the
normative projection described below. Native clients (iOS `HapiProtocol`,
Android `:core:protocol`) port that pipeline and must reproduce `expected`
from `input` exactly.
Machine-generated conformance fixtures for the HAPI client protocol. The
**web implementation is the source of truth**: every `expected*` value in
every file is produced by running the real web implementation over
hand-authored wire inputs, then applying the normative projection each suite
documents below. Native clients (iOS `HapiProtocol`, Android
`:core:protocol`) port the same logic and must reproduce the expectations
from the inputs exactly.
Three suites:
| Suite | Pins | Web source of truth |
|-------|------|---------------------|
| `chat/` | message decode/render pipeline (normalize → reduce → group) | `web/src/chat/` |
| `sse/` | `session-updated` versioned-patch application | `applySessionDetailPatch` in `web/src/lib/sessionPatch.ts` |
| `pagination/` | message window: paging cursors, epoch resets, optimistic sends, trims | `web/src/lib/message-window-store.ts` + `web/src/lib/messages.ts` |
Never edit the JSON by hand — edit the case definitions in
`web/scripts/fixtures/cases/` and regenerate.
`web/scripts/fixtures/` (`cases/`, `sse/cases.ts`, `pagination/cases.ts`) and
regenerate.
## Layout
```
shared/fixtures/
├── VERSION # current fixtureVersion (single integer + \n)
├── chat/<name>.json # one fixture per case
├── VERSION # current fixtureVersion (single integer + \n)
├── chat/<name>.json # one fixture per case
├── sse/<name>.json
├── pagination/<name>.json
└── README.md
```
## How to consume (all suites)
- **iOS** (`ios/`, SPM test target): resolve the repo checkout from the test
file's own location and load every `<suite>/*.json`, e.g.
`URL(fileURLWithPath: #filePath)` → walk up to the package root →
`../../shared/fixtures`. Decode the inputs, run the ported implementation,
project, and compare against the stored expectations.
- **Android** (`android/`, `:core:protocol` JVM tests): pass the directory via
Gradle — `tasks.withType<Test> { systemProperty("hapi.fixtures.dir",
rootDir.resolve("../shared/fixtures")) }` — and read it with
`System.getProperty("hapi.fixtures.dir")` in the test.
- Iterate **all** files in a suite directory (fail on zero files) so newly
added fixtures are picked up without native-side changes.
### Acceptance bar
Exact match on the normative projection: serialize your computed output and
the fixture's stored expectation to **canonical JSON** (recursively sorted
object keys; numbers as JSON numbers; no `undefined`/absent-key differences)
and compare for equality. Field order in the files is already canonical
(sorted keys, 4-space indent, LF, trailing newline), so a key-sorted
structural deep-compare is equivalent.
### fixtureVersion policy
`fixtureVersion` (mirrored in `VERSION`, shared by all suites) is bumped when
a document schema or a normative projection changes shape. Native suites must
**fail loudly when the on-disk version is newer than the version they
support** (do not silently skip), and should tolerate older versions only if
they explicitly implement them. Additive new fixture *files* and new wire
cases do not bump the version.
---
# Chat suite (`chat/`)
Runs the real web pipeline (`normalizeDecryptedMessage` → `reduceChatBlocks`
→ `buildVisibleChatBlocks`) over wire messages, then projects.
## Document schema (`fixtureVersion: 1`)
```jsonc
@@ -42,37 +91,8 @@ shared/fixtures/
}
```
## How to consume
- **iOS** (`ios/`, SPM test target): resolve the repo checkout from the test
file's own location and load every `chat/*.json`, e.g.
`URL(fileURLWithPath: #filePath)` → walk up to the package root →
`../../shared/fixtures`. Decode `input`, run the ported pipeline, project,
and compare against `expected`.
- **Android** (`android/`, `:core:protocol` JVM tests): pass the directory via
Gradle — `tasks.withType<Test> { systemProperty("hapi.fixtures.dir",
rootDir.resolve("../shared/fixtures")) }` — and read it with
`System.getProperty("hapi.fixtures.dir")` in the test.
- Iterate **all** files in `chat/` (fail on zero files) so newly added
fixtures are picked up without native-side changes.
### Acceptance bar
Exact match on the normative projection: serialize your projected output and
the fixture's `expected` to **canonical JSON** (recursively sorted object
keys; numbers as JSON numbers; no `undefined`/absent-key differences) and
compare for equality — per fixture, on `blocks`, `hasReadyEvent`,
`latestUsage`, and `visibleBlocks`. Field order in the files is already
canonical (sorted keys, 4-space indent, LF, trailing newline), so a
key-sorted structural deep-compare is equivalent.
### fixtureVersion policy
`fixtureVersion` (mirrored in `VERSION`) is bumped when the document schema or
the normative projection changes shape. Native suites must **fail loudly when
the on-disk version is newer than the version they support** (do not silently
skip), and should tolerate older versions only if they explicitly implement
them. Additive new fixture *files* and new wire cases do not bump the version.
Per fixture, the acceptance comparison covers `blocks`, `hasReadyEvent`,
`latestUsage`, and `visibleBlocks`.
## Normative field contract
@@ -133,16 +153,166 @@ Tool group: `defaultOpen`, `historyState`, `needsOlderHistory`,
`activityTitle`, `presentationMode`, `summary`. Top level: `latestGoal`,
`latestUsage.cacheCreation`/`cacheRead`/`model`/`timestamp`.
## Regeneration & drift gate
---
# SSE session-patch suite (`sse/`)
Pins the versioned-patch algorithm for `session-updated` events carrying a
`SessionPatch` (contract: `docs/api/client-contract/sse.md`, "Versioned patch
algorithm"). Expectations are computed by the real web fold —
`applySessionDetailPatch` in `web/src/lib/sessionPatch.ts`.
## Document schema (`fixtureVersion: 1`)
```jsonc
{
"fixtureVersion": 1,
"name": "metadata-newer-version-applied", // equals the file name
"description": "…",
"initialSession": { /* full Session as cached before the first patch */ },
"patches": [ /* SessionPatch payloads in arrival order */ ],
"expectedPatchResults": [ "applied" | "unchanged", … ], // aligned with patches
"expectedSession": { /* Session after folding all patches */ }
}
```
## Replay contract
Fold over `patches` in order:
```
next = applySessionDetailPatch(session, patch) // your port
results[i] = next == null ? "unchanged" : "applied"
session = next ?? session
```
Compare `results` against `expectedPatchResults` and the final session
against `expectedSession` (canonical-JSON equality). `unchanged` is
normative: it means the call reported "nothing changed — keep the previous
object identity" (version-gated wrapper, sub-minute `activeAt` keep-alive,
no-op assignment). A port that applies a stale wrapper, or that treats a
keep-alive as a change, fails on the verdict even when the final session
happens to match.
Both inputs are stored **schema-normalized**: `initialSession` is the parse
output of `SessionSchema` and every patch is the parse output of the strict
`SessionPatchSchema` (zod defaults applied, e.g. `TodoItem.priority`), so
consumers decode the JSON verbatim — no schema re-run needed. The generator
validates every authored patch against the wire schema, and the web
self-check re-validates on every test run, so fixtures cannot drift from
`shared/src/schemas.ts`.
Behavior deliberately pinned (see the individual descriptions):
- versioned wrappers (`metadata`/`agentState`/`todos`/`teamState`) apply only
when `version` is **strictly greater** than the cached watermark; equal and
older versions are dropped regardless of value; absent `todosUpdatedAt`/
`teamStateUpdatedAt` watermarks count as 0
- `teamState` wrapper with `value: null` clears the field (absent in
`expectedSession`) and still stores the watermark
- `updatedAt` is max-monotonic; an older `updatedAt` alone is `unchanged`
- flat fields are last-write-wins; a `serviceTier` key explicitly present as
`null` clears it
- `activeTurnStartedAt` is **not applied** by the patch path (web behavior:
no assignment branch) — a patch carrying only it is `unchanged`
- a patch whose only effective change is an `activeAt` delta < 60s is
`unchanged` and the cached `activeAt` does **not** move
- `scratchlistUpdatedAt` is a refetch trigger only: `unchanged`, no session
mutation
---
# Pagination suite (`pagination/`)
Pins the message-window store: paging requests it must issue, epoch-reset
handling, optimistic-send reconciliation, queued-row lifecycle, and window
trimming (contract: `docs/api/client-contract/pagination.md`). Expectations
are recorded from the real web store (`web/src/lib/message-window-store.ts`)
driven by a scripted ApiClient.
## Document schema (`fixtureVersion: 1`)
```jsonc
{
"fixtureVersion": 1,
"name": "older-page-epoch-mismatch-resets", // equals the file name
"description": "…",
"ops": [ /* operation script, in order; see below */ ],
"expectedState": { /* final window projection, see below */ }
}
```
## Operations
Replay each op strictly sequentially against your ported store. Ops that hit
the server carry `responses` — the scripted `GET /messages` replies, consumed
FIFO — and the machine-recorded `expectedRequests`: the exact query objects
the web store sent (`limit` only for a latest page; `beforeAt`+`beforeSeq`+
`limit` for older pages; `afterAt`+`afterSeq`+`untilAt`+`untilSeq`+`epoch`+
`limit` for tail catch-up, where `untilAt`/`untilSeq` are `null` on the first
loop request). Your port must issue the same requests in the same order and
consume every scripted response.
| `op` | Store operation (web name) | Notes |
|------|----------------------------|-------|
| `sync-tail` | `syncTailMessages` | Full tail sync; latest page when no usable cursor, else the after-cursor loop. |
| `fetch-older` | `fetchOlderMessages` | One older page. `expectedOutcome` is `{kind:'applied', hasMore, addedRenderableCount}` or `{kind:'stopped', reason}`. On an epoch mismatch the store runs an internal tail sync — its request/response belong to this same op and the outcome is `stopped`/`epoch-reset`. |
| `sse-messages` | `ingestIncomingMessages` | SSE `message-received` delivery (also advances the newest cursor when an epoch is cached — even past rows the pipeline hides). |
| `append-optimistic` | `appendOptimisticMessage` | Local optimistic row (`id === localId`). Does **not** advance the newest cursor. |
| `update-status` | `updateMessageStatus` | Client send-state transition by `localId`. |
| `messages-consumed` | `markMessagesConsumed` | SSE event: stamp `invokedAt`, flip status to `sent` (skip `failed`), re-sort by position; never advances the newest cursor. |
| `message-cancelled` | `removeOptimisticMessage` | SSE event / optimistic DELETE removal; matches `localId` **or** `id`; idempotent. |
| `cancel-invoked` | remove + append | DELETE answered `{"status":"invoked"}`: remove by `localId`, then ingest the returned `message` with client status `sent` (the harness adds the status). |
| `set-view-mode` | `setMessageViewMode` | `tail` re-entry trims to the visible window and, after a history overflow, forces a latest reset. |
| `queued-state` | reconcile round trip | Collect candidates (user rows with `invokedAt === null`, pinned in `expectedCandidates`, window order), apply `invoked` entries like `messages-consumed`, then drop candidates in neither `queuedLocalIds` nor `invoked`. |
## `expectedState` projection (normative)
```jsonc
{
"messages": [ { "id", "localId", "seq", "createdAt",
"invokedAt"?, // wire tri-state: absent / null / number
"scheduledAt"?, // wire tri-state
"status"?, // client send state when present
"queued": bool, // user row ∧ invokedAt === null ∧ status ≠ 'failed'
"optimistic": bool } ], // localId ∧ id === localId
"hasMore": bool, // older history exists (server flag ∨ trim)
"epoch": number | null,
"viewMode": "tail" | "history",
"olderCursor": { "at", "seq" } | null, // next before-request position
"newestCursor": { "at", "seq" } | null // next after-request position
}
```
`messages` order is normative (position order: `at = invokedAt ?? createdAt`,
ties by `seq`). The cursors are the store's compound paging positions (web
internals `oldestPosition*` / `newestPosition*`); everything else that is
web-internal (render/version counters, notification throttling, persistence)
is intentionally NOT part of the contract.
Determinism notes: the web store's only wall-clock reads gate notification
throttling and never touch this projection, so no time injection is needed —
replays are exact. Fixtures keep every position pair distinct (no `(at, seq)`
ties), so the web's id tie-break (`localeCompare`) never decides an order;
native ports should still break full ties with an ASCII id comparison.
---
# Regeneration & drift gate
```bash
bun run gen:fixtures # from the repo root (runs web/scripts/generate-fixtures.ts)
```
Output is byte-deterministic (canonical serialization), so `git status` after
a regeneration is the drift signal: when `web/src/chat/**` changes behavior,
regenerated fixtures differ, the diff gets committed, and the native
conformance suites go red until the ports catch up. The web-side self-check
lives at `web/src/chat/fixtures.test.ts` (runs in `bun run test:web`): it
re-runs the pipeline over every stored `input` and fails on any divergence
from `expected` or from canonical serialization.
a regeneration is the drift signal: when `web/src/chat/**`,
`web/src/lib/sessionPatch.ts`, `web/src/lib/message-window-store.ts` or
`web/src/lib/messages.ts` change behavior, regenerated fixtures differ, the
diff gets committed, and the native conformance suites go red until the ports
catch up. The web-side self-checks (all in `bun run test:web`) re-run every
stored input against the live implementation and fail on any divergence from
the stored expectations or from canonical serialization:
- `web/src/chat/fixtures.test.ts` — chat suite
- `web/src/lib/sessionPatch.fixtures.test.ts` — sse suite
- `web/src/lib/message-window-store.fixtures.test.ts` — pagination suite
@@ -0,0 +1,91 @@
{
"description": "DELETE answered status:invoked (the agent consumed the message before the cancel landed): the queued snapshot must NOT be resurrected — the returned authoritative row is ingested with its server invokedAt and client status sent, landing in the thread at its invocation position.",
"expectedState": {
"epoch": 0,
"hasMore": false,
"messages": [
{
"createdAt": 1000,
"id": "srv-1",
"invokedAt": 5000,
"localId": "local-1",
"optimistic": false,
"queued": false,
"seq": 1,
"status": "sent"
}
],
"newestCursor": {
"at": 1000,
"seq": 1
},
"olderCursor": {
"at": 1000,
"seq": 1
},
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "cancel-too-late-ingests-invoked-row",
"ops": [
{
"expectedRequests": [
{
"limit": 200
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"text": "race with the agent",
"type": "text"
},
"role": "user"
},
"createdAt": 1000,
"id": "srv-1",
"invokedAt": null,
"localId": "local-1",
"seq": 1
}
],
"page": {
"direction": "latest",
"epoch": 0,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 1000,
"nextBeforeSeq": 1,
"reset": false,
"snapshotHeadAt": 1000,
"snapshotHeadSeq": 1
}
}
]
},
{
"localId": "local-1",
"message": {
"content": {
"content": {
"text": "race with the agent",
"type": "text"
},
"role": "user"
},
"createdAt": 1000,
"id": "srv-1",
"invokedAt": 5000,
"localId": "local-1",
"seq": 1
},
"op": "cancel-invoked"
}
]
}
@@ -0,0 +1,135 @@
{
"description": "Older pagination sends the compound (beforeAt, beforeSeq) pair from the current older cursor, prepends the rows, and adopts the response page.nextBefore as the new older cursor and page.hasMore as the exhaustion flag.",
"expectedState": {
"epoch": 4,
"hasMore": false,
"messages": [
{
"createdAt": 9000,
"id": "a-9",
"invokedAt": 9000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 9
},
{
"createdAt": 10000,
"id": "a-10",
"invokedAt": 10000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 10
}
],
"newestCursor": {
"at": 10000,
"seq": 10
},
"olderCursor": {
"at": 9000,
"seq": 9
},
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "fetch-older-before-cursor",
"ops": [
{
"expectedRequests": [
{
"limit": 200
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "a-10",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 10000,
"id": "a-10",
"invokedAt": 10000,
"localId": null,
"seq": 10
}
],
"page": {
"direction": "latest",
"epoch": 4,
"hasMore": true,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 10000,
"nextBeforeSeq": 10,
"reset": false,
"snapshotHeadAt": 10000,
"snapshotHeadSeq": 10
}
}
]
},
{
"expectedOutcome": {
"addedRenderableCount": 1,
"hasMore": false,
"kind": "applied"
},
"expectedRequests": [
{
"beforeAt": 10000,
"beforeSeq": 10,
"limit": 200
}
],
"op": "fetch-older",
"responses": [
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "a-9",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 9000,
"id": "a-9",
"invokedAt": 9000,
"localId": null,
"seq": 9
}
],
"page": {
"direction": "before",
"epoch": 4,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 9000,
"nextBeforeSeq": 9,
"reset": false,
"snapshotHeadAt": null,
"snapshotHeadSeq": null
}
}
]
}
]
}
@@ -0,0 +1,132 @@
{
"description": "Cold start: a latest page seeds the window, epoch, older cursor (page.nextBefore) and newest cursor (page.snapshotHead); a live SSE message appends and advances the newest cursor to its position.",
"expectedState": {
"epoch": 0,
"hasMore": true,
"messages": [
{
"createdAt": 1000,
"id": "a-1",
"invokedAt": 1000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 1
},
{
"createdAt": 2000,
"id": "a-2",
"invokedAt": 2000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 2
},
{
"createdAt": 3000,
"id": "a-3",
"invokedAt": 3000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 3
}
],
"newestCursor": {
"at": 3000,
"seq": 3
},
"olderCursor": {
"at": 1000,
"seq": 1
},
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "latest-page-then-sse-ingest",
"ops": [
{
"expectedRequests": [
{
"limit": 200
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "a-1",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 1000,
"id": "a-1",
"invokedAt": 1000,
"localId": null,
"seq": 1
},
{
"content": {
"content": {
"data": {
"message": "a-2",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 2000,
"id": "a-2",
"invokedAt": 2000,
"localId": null,
"seq": 2
}
],
"page": {
"direction": "latest",
"epoch": 0,
"hasMore": true,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 1000,
"nextBeforeSeq": 1,
"reset": false,
"snapshotHeadAt": 2000,
"snapshotHeadSeq": 2
}
}
]
},
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "a-3",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 3000,
"id": "a-3",
"invokedAt": 3000,
"localId": null,
"seq": 3
}
],
"op": "sse-messages"
}
]
}
@@ -0,0 +1,41 @@
{
"description": "message-cancelled removes the queued row by localId; a repeat delivery is an idempotent no-op on the already-empty window.",
"expectedState": {
"epoch": null,
"hasMore": false,
"messages": [],
"newestCursor": null,
"olderCursor": null,
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "message-cancelled-removes-queued-row",
"ops": [
{
"message": {
"content": {
"content": {
"text": "cancel me",
"type": "text"
},
"role": "user"
},
"createdAt": 1000,
"id": "local-1",
"invokedAt": null,
"localId": "local-1",
"seq": null,
"status": "queued"
},
"op": "append-optimistic"
},
{
"localId": "local-1",
"op": "message-cancelled"
},
{
"localId": "local-1",
"op": "message-cancelled"
}
]
}
@@ -0,0 +1,106 @@
{
"description": "messages-consumed stamps invokedAt and flips status to sent: the row leaves the queued bar and moves to its invocation position (after the agent row), while the newest cursor does NOT advance from this out-of-band update.",
"expectedState": {
"epoch": 0,
"hasMore": false,
"messages": [
{
"createdAt": 2000,
"id": "a-2",
"invokedAt": 2000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 2
},
{
"createdAt": 1000,
"id": "q-1",
"invokedAt": 3000,
"localId": "local-1",
"optimistic": false,
"queued": false,
"seq": 1,
"status": "sent"
}
],
"newestCursor": {
"at": 2000,
"seq": 2
},
"olderCursor": {
"at": 1000,
"seq": 1
},
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "messages-consumed-stamps-invoked-at",
"ops": [
{
"expectedRequests": [
{
"limit": 200
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"text": "queued question",
"type": "text"
},
"role": "user"
},
"createdAt": 1000,
"id": "q-1",
"invokedAt": null,
"localId": "local-1",
"seq": 1
},
{
"content": {
"content": {
"data": {
"message": "a-2",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 2000,
"id": "a-2",
"invokedAt": 2000,
"localId": null,
"seq": 2
}
],
"page": {
"direction": "latest",
"epoch": 0,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 1000,
"nextBeforeSeq": 1,
"reset": false,
"snapshotHeadAt": 2000,
"snapshotHeadSeq": 2
}
}
]
},
{
"invokedAt": 3000,
"localIds": [
"local-1"
],
"op": "messages-consumed"
}
]
}
@@ -0,0 +1,144 @@
{
"description": "An older page answering with a different epoch invalidates every cursor: the load stops with epoch-reset, the window is discarded, and a fresh latest request (issued by the store within the same operation) replaces it wholesale.",
"expectedState": {
"epoch": 2,
"hasMore": false,
"messages": [
{
"createdAt": 20000,
"id": "f-20",
"invokedAt": 20000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 20
}
],
"newestCursor": {
"at": 20000,
"seq": 20
},
"olderCursor": {
"at": 20000,
"seq": 20
},
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "older-page-epoch-mismatch-resets",
"ops": [
{
"expectedRequests": [
{
"limit": 200
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "a-10",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 10000,
"id": "a-10",
"invokedAt": 10000,
"localId": null,
"seq": 10
}
],
"page": {
"direction": "latest",
"epoch": 1,
"hasMore": true,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 10000,
"nextBeforeSeq": 10,
"reset": false,
"snapshotHeadAt": 10000,
"snapshotHeadSeq": 10
}
}
]
},
{
"expectedOutcome": {
"kind": "stopped",
"reason": "epoch-reset"
},
"expectedRequests": [
{
"beforeAt": 10000,
"beforeSeq": 10,
"limit": 200
},
{
"limit": 200
}
],
"op": "fetch-older",
"responses": [
{
"messages": [],
"page": {
"direction": "before",
"epoch": 2,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": null,
"nextBeforeSeq": null,
"reset": false,
"snapshotHeadAt": null,
"snapshotHeadSeq": null
}
},
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "f-20",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 20000,
"id": "f-20",
"invokedAt": 20000,
"localId": null,
"seq": 20
}
],
"page": {
"direction": "latest",
"epoch": 2,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 20000,
"nextBeforeSeq": 20,
"reset": false,
"snapshotHeadAt": 20000,
"snapshotHeadSeq": 20
}
}
]
}
]
}
@@ -0,0 +1,68 @@
{
"description": "The message-received echo of the caller's own send (server id, real seq, same localId) replaces the optimistic row while preserving the client-side status; only one row remains and it is no longer optimistic.",
"expectedState": {
"epoch": null,
"hasMore": false,
"messages": [
{
"createdAt": 1000,
"id": "srv-1",
"invokedAt": null,
"localId": "local-1",
"optimistic": false,
"queued": true,
"seq": 1,
"status": "queued"
}
],
"newestCursor": null,
"olderCursor": null,
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "optimistic-echo-reconciles-by-localid",
"ops": [
{
"message": {
"content": {
"content": {
"text": "hello agent",
"type": "text"
},
"role": "user"
},
"createdAt": 1000,
"id": "local-1",
"invokedAt": null,
"localId": "local-1",
"seq": null,
"status": "sending"
},
"op": "append-optimistic"
},
{
"localId": "local-1",
"op": "update-status",
"status": "queued"
},
{
"messages": [
{
"content": {
"content": {
"text": "hello agent",
"type": "text"
},
"role": "user"
},
"createdAt": 1000,
"id": "srv-1",
"invokedAt": null,
"localId": "local-1",
"seq": 1
}
],
"op": "sse-messages"
}
]
}
@@ -0,0 +1,127 @@
{
"description": "Queued-state recovery after a resume gap: candidates are the user rows with invokedAt strictly null; the server verdict stamps invoked ones like messages-consumed, keeps the still-queued one, and drops candidates in neither list (deleted server-side).",
"expectedState": {
"epoch": 0,
"hasMore": false,
"messages": [
{
"createdAt": 2000,
"id": "u-2",
"invokedAt": null,
"localId": "local-kept",
"optimistic": false,
"queued": true,
"seq": 2
},
{
"createdAt": 3000,
"id": "u-3",
"invokedAt": 4000,
"localId": "local-consumed",
"optimistic": false,
"queued": false,
"seq": 3,
"status": "sent"
}
],
"newestCursor": {
"at": 3000,
"seq": 3
},
"olderCursor": {
"at": 1000,
"seq": 1
},
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "queued-state-reconciliation-drops-deleted",
"ops": [
{
"expectedRequests": [
{
"limit": 200
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"text": "deleted server-side",
"type": "text"
},
"role": "user"
},
"createdAt": 1000,
"id": "u-1",
"invokedAt": null,
"localId": "local-stale",
"seq": 1
},
{
"content": {
"content": {
"text": "still queued",
"type": "text"
},
"role": "user"
},
"createdAt": 2000,
"id": "u-2",
"invokedAt": null,
"localId": "local-kept",
"seq": 2
},
{
"content": {
"content": {
"text": "consumed during the gap",
"type": "text"
},
"role": "user"
},
"createdAt": 3000,
"id": "u-3",
"invokedAt": null,
"localId": "local-consumed",
"seq": 3
}
],
"page": {
"direction": "latest",
"epoch": 0,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 1000,
"nextBeforeSeq": 1,
"reset": false,
"snapshotHeadAt": 3000,
"snapshotHeadSeq": 3
}
}
]
},
{
"expectedCandidates": [
"local-stale",
"local-kept",
"local-consumed"
],
"invoked": [
{
"invokedAt": 4000,
"localId": "local-consumed"
}
],
"op": "queued-state",
"queuedLocalIds": [
"local-kept"
]
}
]
}
@@ -0,0 +1,152 @@
{
"description": "A tail sync that gets page.reset:true replaces every server row from before the request but keeps optimistic rows (id === localId): the not-yet-echoed send survives the epoch bump and the window adopts the new epoch and cursors.",
"expectedState": {
"epoch": 2,
"hasMore": false,
"messages": [
{
"createdAt": 1500,
"id": "local-1",
"invokedAt": null,
"localId": "local-1",
"optimistic": true,
"queued": true,
"seq": null,
"status": "sending"
},
{
"createdAt": 2000,
"id": "fresh-2",
"invokedAt": 2000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 2
}
],
"newestCursor": {
"at": 2000,
"seq": 2
},
"olderCursor": {
"at": 2000,
"seq": 2
},
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "reset-latest-preserves-optimistic",
"ops": [
{
"expectedRequests": [
{
"limit": 200
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "old-1",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 1000,
"id": "old-1",
"invokedAt": 1000,
"localId": null,
"seq": 1
}
],
"page": {
"direction": "latest",
"epoch": 1,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 1000,
"nextBeforeSeq": 1,
"reset": false,
"snapshotHeadAt": 1000,
"snapshotHeadSeq": 1
}
}
]
},
{
"message": {
"content": {
"content": {
"text": "optimistic send",
"type": "text"
},
"role": "user"
},
"createdAt": 1500,
"id": "local-1",
"invokedAt": null,
"localId": "local-1",
"seq": null,
"status": "sending"
},
"op": "append-optimistic"
},
{
"expectedRequests": [
{
"afterAt": 1000,
"afterSeq": 1,
"epoch": 1,
"limit": 200,
"untilAt": null,
"untilSeq": null
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "fresh-2",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 2000,
"id": "fresh-2",
"invokedAt": 2000,
"localId": null,
"seq": 2
}
],
"page": {
"direction": "latest",
"epoch": 2,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 2000,
"nextBeforeSeq": 2,
"reset": true,
"snapshotHeadAt": 2000,
"snapshotHeadSeq": 2
}
}
]
}
]
}
@@ -0,0 +1,123 @@
{
"description": "SSE rows the chat pipeline hides (e.g. meta system output) are not retained in the window, but the newest cursor still advances past their position so a later tail sync does not refetch them.",
"expectedState": {
"epoch": 0,
"hasMore": false,
"messages": [
{
"createdAt": 1000,
"id": "a-1",
"invokedAt": 1000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 1
},
{
"createdAt": 2000,
"id": "a-2",
"invokedAt": 2000,
"localId": null,
"optimistic": false,
"queued": false,
"seq": 2
}
],
"newestCursor": {
"at": 3000,
"seq": 3
},
"olderCursor": {
"at": 1000,
"seq": 1
},
"viewMode": "tail"
},
"fixtureVersion": 1,
"name": "sse-hidden-rows-advance-cursor-only",
"ops": [
{
"expectedRequests": [
{
"limit": 200
}
],
"op": "sync-tail",
"responses": [
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "a-1",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 1000,
"id": "a-1",
"invokedAt": 1000,
"localId": null,
"seq": 1
}
],
"page": {
"direction": "latest",
"epoch": 0,
"hasMore": false,
"limit": 200,
"nextAfterAt": null,
"nextAfterSeq": null,
"nextBeforeAt": 1000,
"nextBeforeSeq": 1,
"reset": false,
"snapshotHeadAt": 1000,
"snapshotHeadSeq": 1
}
}
]
},
{
"messages": [
{
"content": {
"content": {
"data": {
"message": "a-2",
"type": "message"
},
"type": "codex"
},
"role": "agent"
},
"createdAt": 2000,
"id": "a-2",
"invokedAt": 2000,
"localId": null,
"seq": 2
},
{
"content": {
"content": {
"data": {
"isMeta": true,
"type": "system"
},
"type": "output"
},
"role": "agent"
},
"createdAt": 3000,
"id": "hidden-3",
"invokedAt": 3000,
"localId": null,
"seq": 3
}
],
"op": "sse-messages"
}
]
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,70 @@
{
"description": "Pins actual web behavior: applySessionDetailPatch has no branch for activeTurnStartedAt, so a patch carrying only it is a no-op, and in a mixed patch the other fields apply while activeTurnStartedAt keeps its cached value. (Clients relying on this field must take it from full-session payloads.)",
"expectedPatchResults": [
"unchanged",
"applied"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": true,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "active-turn-started-at-not-applied",
"patches": [
{
"activeTurnStartedAt": 1755000005000
},
{
"activeTurnStartedAt": 1755000005000,
"thinking": true
}
]
}
@@ -0,0 +1,105 @@
{
"description": "Out-of-order arrival across the two SSE connections: version 7 lands first and applies; the older version 6 arrives second and is dropped. Final state is the v7 value.",
"expectedPatchResults": [
"applied",
"unchanged"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": {
"requests": {
"req-2": {
"arguments": {
"file_path": "/home/dev/project/README.md"
},
"createdAt": 1755000002000,
"tool": "Write"
}
}
},
"agentStateVersion": 7,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "agent-state-out-of-order-versions",
"patches": [
{
"agentState": {
"value": {
"requests": {
"req-2": {
"arguments": {
"file_path": "/home/dev/project/README.md"
},
"createdAt": 1755000002000,
"tool": "Write"
}
}
},
"version": 7
}
},
{
"agentState": {
"value": {
"requests": {
"req-1": {
"arguments": {
"command": "ls"
},
"createdAt": 1755000001000,
"tool": "Bash"
}
}
},
"version": 6
}
}
]
}
@@ -0,0 +1,106 @@
{
"description": "An agentState wrapper older than the cached agentStateVersion is rejected: applying it would resurrect a permission request that was already resolved into completedRequests.",
"expectedPatchResults": [
"unchanged"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": {
"completedRequests": {
"req-1": {
"arguments": {
"command": "rm -rf build"
},
"completedAt": 1754999980000,
"createdAt": 1754999970000,
"decision": "approved",
"status": "approved",
"tool": "Bash"
}
},
"requests": null
},
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": {
"completedRequests": {
"req-1": {
"arguments": {
"command": "rm -rf build"
},
"completedAt": 1754999980000,
"createdAt": 1754999970000,
"decision": "approved",
"status": "approved",
"tool": "Bash"
}
},
"requests": null
},
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "agent-state-stale-version-dropped",
"patches": [
{
"agentState": {
"value": {
"requests": {
"req-1": {
"arguments": {
"command": "rm -rf build"
},
"createdAt": 1754999970000,
"tool": "Bash"
}
}
},
"version": 4
}
}
]
}
@@ -0,0 +1,74 @@
{
"description": "Flat fields (active, thinking, model, modelReasoningEffort, effort, permissionMode) are last-write-wins assignments with no version gate; serviceTier additionally honors an explicit null (key present) as a clear.",
"expectedPatchResults": [
"applied",
"applied"
],
"expectedSession": {
"active": false,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": "high",
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-opus-4-6",
"modelReasoningEffort": "high",
"namespace": "default",
"permissionMode": "acceptEdits",
"seq": 12,
"serviceTier": null,
"thinking": true,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": "standard",
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "flat-fields-last-write-wins",
"patches": [
{
"active": false,
"effort": "high",
"model": "claude-opus-4-6",
"modelReasoningEffort": "high",
"permissionMode": "acceptEdits",
"thinking": true
},
{
"serviceTier": null
}
]
}
@@ -0,0 +1,73 @@
{
"description": "Keep-alive noise gate: a patch whose only effective change is an activeAt delta below 60s is render-irrelevant and returns unchanged (the cached activeAt does NOT move); a delta of at least 60s applies.",
"expectedPatchResults": [
"unchanged",
"applied"
],
"expectedSession": {
"active": true,
"activeAt": 1755000060000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "keepalive-subminute-active-at-dropped",
"patches": [
{
"active": true,
"activeAt": 1755000010000,
"thinking": false
},
{
"active": true,
"activeAt": 1755000060000,
"thinking": false
}
]
}
@@ -0,0 +1,73 @@
{
"description": "A metadata wrapper whose version equals the cached metadataVersion is dropped even though its value differs — the gate is strictly-greater on version, never a value comparison (dual SSE connections can replay the same version).",
"expectedPatchResults": [
"unchanged"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "metadata-equal-version-dropped",
"patches": [
{
"metadata": {
"value": {
"flavor": "claude",
"host": "devbox",
"name": "renamed-should-not-apply",
"path": "/home/dev/project"
},
"version": 3
}
}
]
}
@@ -0,0 +1,82 @@
{
"description": "A metadata wrapper with version strictly greater than the cached metadataVersion replaces the value and stores the version; the accompanying newer updatedAt advances.",
"expectedPatchResults": [
"applied"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project",
"summary": {
"text": "Add retry to upload path",
"updatedAt": 1755000009000
}
},
"metadataVersion": 4,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000010000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "metadata-newer-version-applied",
"patches": [
{
"metadata": {
"value": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project",
"summary": {
"text": "Add retry to upload path",
"updatedAt": 1755000009000
}
},
"version": 4
},
"updatedAt": 1755000010000
}
]
}
@@ -0,0 +1,93 @@
{
"description": "One patch carrying flat fields, a newer metadata wrapper, and a stale agentState wrapper: the flat fields and metadata apply, the stale agentState is rejected, and all of it happens in a single applied call.",
"expectedPatchResults": [
"applied"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": {
"controlledByUser": false
},
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project-renamed",
"path": "/home/dev/project"
},
"metadataVersion": 5,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": true,
"thinkingAt": 0,
"updatedAt": 1755000030000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": {
"controlledByUser": false
},
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "mixed-flat-and-versioned-patch",
"patches": [
{
"agentState": {
"value": {
"requests": {
"req-stale": {
"arguments": {
"command": "echo stale"
},
"createdAt": 1754999940000,
"tool": "Bash"
}
}
},
"version": 2
},
"metadata": {
"value": {
"flavor": "claude",
"host": "devbox",
"name": "project-renamed",
"path": "/home/dev/project"
},
"version": 5
},
"thinking": true,
"updatedAt": 1755000030000
}
]
}
@@ -0,0 +1,65 @@
{
"description": "scratchlistUpdatedAt is a refetch trigger, not session state: the patch validates and is render-relevant, but applySessionDetailPatch stores nothing and reports unchanged. Clients react by refetching the scratchlist endpoint, never by mutating the session.",
"expectedPatchResults": [
"unchanged"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "scratchlist-updated-at-trigger-only",
"patches": [
{
"scratchlistUpdatedAt": 1755000900000
}
]
}
@@ -0,0 +1,86 @@
{
"description": "teamState with value null means \"team deleted\": a strictly newer version clears the field entirely (absent in the expected session) and stores the watermark so a lagged pre-delete patch cannot resurrect the team.",
"expectedPatchResults": [
"applied"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"teamStateUpdatedAt": 1755000200000,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"teamState": {
"members": [
{
"agentType": "claude",
"name": "lead",
"status": "active"
},
{
"agentType": "claude",
"name": "reviewer",
"status": "idle"
}
],
"teamName": "hapi-dev",
"updatedAt": 1754999900000
},
"teamStateUpdatedAt": 1754999900000,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "team-state-null-clear",
"patches": [
{
"teamState": {
"value": null,
"version": 1755000200000
}
}
]
}
@@ -0,0 +1,113 @@
{
"description": "todos wrappers gate on todosUpdatedAt with an absent watermark treated as 0: the first patch applies, then an older-versioned todos replay is dropped instead of resurrecting the pre-update list.",
"expectedPatchResults": [
"applied",
"unchanged"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"todos": [
{
"content": "Wire the SSE reconnect backoff",
"id": "todo-1",
"priority": "high",
"status": "completed"
},
{
"activeForm": "Porting the patch gate to native",
"content": "Port the patch gate to native",
"id": "todo-2",
"priority": "medium",
"status": "in_progress"
}
],
"todosUpdatedAt": 1755000300000,
"updatedAt": 1755000000000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "todos-version-watermark-from-absent",
"patches": [
{
"todos": {
"value": [
{
"content": "Wire the SSE reconnect backoff",
"id": "todo-1",
"priority": "high",
"status": "completed"
},
{
"activeForm": "Porting the patch gate to native",
"content": "Port the patch gate to native",
"id": "todo-2",
"priority": "medium",
"status": "in_progress"
}
],
"version": 1755000300000
}
},
{
"todos": {
"value": [
{
"activeForm": "Wiring the SSE reconnect backoff",
"content": "Wire the SSE reconnect backoff",
"id": "todo-1",
"priority": "high",
"status": "in_progress"
}
],
"version": 1755000200000
}
}
]
}
@@ -0,0 +1,74 @@
{
"description": "updatedAt is max-monotonic: a patch carrying an older updatedAt applies its other fields without rewinding the clock; a newer updatedAt advances it; an older updatedAt alone is a no-op (unchanged).",
"expectedPatchResults": [
"applied",
"applied",
"unchanged"
],
"expectedSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": true,
"thinkingAt": 0,
"updatedAt": 1755000005000
},
"fixtureVersion": 1,
"initialSession": {
"active": true,
"activeAt": 1755000000000,
"activeTurnStartedAt": null,
"agentState": null,
"agentStateVersion": 5,
"createdAt": 1754996400000,
"effort": null,
"id": "sess-sse-fixture",
"metadata": {
"flavor": "claude",
"host": "devbox",
"name": "project",
"path": "/home/dev/project"
},
"metadataVersion": 3,
"model": "claude-sonnet-4-6",
"modelReasoningEffort": null,
"namespace": "default",
"permissionMode": "default",
"seq": 12,
"serviceTier": null,
"thinking": false,
"thinkingAt": 0,
"updatedAt": 1755000000000
},
"name": "updated-at-max-monotonic",
"patches": [
{
"thinking": true,
"updatedAt": 1754999995000
},
{
"updatedAt": 1755000005000
},
{
"updatedAt": 1754999998000
}
]
}