- Add paginated thread history with bounded snapshots and replay limits - Trim oversized wire payloads and support progressive mobile history loading
Showing the changes to packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts (1 of 43 files). Show all
Unified · Split
| @@ -0,0 +1,214 @@ | ||
| 1 | +import { ThreadId } from "@t3tools/contracts"; | |
| 2 | +import { describe, expect, it } from "@effect/vitest"; | |
| 3 | +import * as Effect from "effect/Effect"; | |
| 4 | +import * as Layer from "effect/Layer"; | |
| 5 | + | |
| 6 | +import { PrimaryConnectionTarget, type PreparedConnection } from "../connection/model.ts"; | |
| 7 | +import { remoteHttpClientLayer } from "../rpc/http.ts"; | |
| 8 | +import { boundedThreadSnapshotLoaderLayer } from "./boundedThreadSnapshotHttp.ts"; | |
| 9 | +import { ThreadSnapshotLoader } from "./threadSnapshotHttp.ts"; | |
| 10 | + | |
| 11 | +const TARGET = new PrimaryConnectionTarget({ | |
| 12 | + environmentId: "environment-bounded" as never, | |
| 13 | + label: "Bounded", | |
| 14 | + httpBaseUrl: "https://environment.example.test", | |
| 15 | + wsBaseUrl: "wss://environment.example.test", | |
| 16 | +}); | |
| 17 | + | |
| 18 | +const PREPARED: PreparedConnection = { | |
| 19 | + environmentId: TARGET.environmentId, | |
| 20 | + label: TARGET.label, | |
| 21 | + httpBaseUrl: TARGET.httpBaseUrl, | |
| 22 | + socketUrl: TARGET.wsBaseUrl, | |
| 23 | + httpAuthorization: null, | |
| 24 | + target: TARGET, | |
| 25 | +}; | |
| 26 | + | |
| 27 | +const THREAD_ID = ThreadId.make("thread-bounded"); | |
| 28 | +const NOW = "2026-06-20T00:00:00.000Z"; | |
| 29 | + | |
| 30 | +const FULL_SNAPSHOT_BODY = { | |
| 31 | + snapshotSequence: 9, | |
| 32 | + projection: { | |
| 33 | + thread: { | |
| 34 | + createdBy: "user", | |
| 35 | + creationSource: "web", | |
| 36 | + id: String(THREAD_ID), | |
| 37 | + projectId: "project-1", | |
| 38 | + title: "Full fallback thread", | |
| 39 | + providerInstanceId: "codex", | |
| 40 | + modelSelection: { instanceId: "codex", model: "gpt-5" }, | |
| 41 | + runtimeMode: "full-access", | |
| 42 | + interactionMode: "default", | |
| 43 | + branch: null, | |
| 44 | + worktreePath: null, | |
| 45 | + activeProviderThreadId: null, | |
| 46 | + lineage: { | |
| 47 | + parentThreadId: null, | |
| 48 | + relationshipToParent: null, | |
| 49 | + rootThreadId: String(THREAD_ID), | |
| 50 | + }, | |
| 51 | + forkedFrom: null, | |
| 52 | + createdAt: NOW, | |
| 53 | + updatedAt: NOW, | |
| 54 | + archivedAt: null, | |
| 55 | + deletedAt: null, | |
| 56 | + settledOverride: null, | |
| 57 | + settledAt: null, | |
| 58 | + }, | |
| 59 | + runs: [], | |
| 60 | + attempts: [], | |
| 61 | + nodes: [], | |
| 62 | + subagents: [], | |
| 63 | + providerSessions: [], | |
| 64 | + providerThreads: [], | |
| 65 | + providerTurns: [], | |
| 66 | + runtimeRequests: [], | |
| 67 | + messages: [], | |
| 68 | + plans: [], | |
| 69 | + turnItems: [], | |
| 70 | + checkpointScopes: [], | |
| 71 | + checkpoints: [], | |
| 72 | + contextHandoffs: [], | |
| 73 | + contextTransfers: [], | |
| 74 | + visibleTurnItems: [], | |
| 75 | + updatedAt: NOW, | |
| 76 | + }, | |
| 77 | +}; | |
| 78 | + | |
| 79 | +describe("boundedThreadSnapshotLoader", () => { | |
| 80 | + it.effect("falls back to full HTTP snapshot when bounded returns a plain route 404", () => { | |
| 81 | + const fetchFn = ((input: RequestInfo | URL) => { | |
| 82 | + const url = String(input); | |
| 83 | + if (url.includes(`/api/orchestration/threads/${THREAD_ID}/bounded`)) { | |
| 84 | + // Generic HTML/route 404: not a structured thread_not_found payload. | |
| 85 | + return Promise.resolve( | |
| 86 | + new Response("Not Found", { | |
| 87 | + status: 404, | |
| 88 | + headers: { "content-type": "text/plain" }, | |
| 89 | + }), | |
| 90 | + ); | |
| 91 | + } | |
| 92 | + expect(url).toContain(`/api/orchestration/threads/${THREAD_ID}`); | |
| 93 | + expect(url.includes("/bounded")).toBe(false); | |
| 94 | + return Promise.resolve( | |
| 95 | + new Response(JSON.stringify(FULL_SNAPSHOT_BODY), { | |
| 96 | + status: 200, | |
| 97 | + headers: { "content-type": "application/json" }, | |
| 98 | + }), | |
| 99 | + ); | |
| 100 | + }) satisfies typeof fetch; | |
| 101 | + | |
| 102 | + return Effect.gen(function* () { | |
| 103 | + const loader = yield* ThreadSnapshotLoader; | |
| 104 | + const result = yield* loader.load(PREPARED, THREAD_ID); | |
| 105 | + expect(result._tag).toBe("present"); | |
| 106 | + if (result._tag === "present") { | |
| 107 | + expect(result.snapshot.snapshotSequence).toBe(9); | |
| 108 | + expect(result.snapshot.projection.thread.title).toBe("Full fallback thread"); | |
| 109 | + expect(result.history).toBeUndefined(); | |
| 110 | + } | |
| 111 | + }).pipe( | |
| 112 | + Effect.provide( | |
| 113 | + Layer.provide(boundedThreadSnapshotLoaderLayer, remoteHttpClientLayer(fetchFn)), | |
| 114 | + ), | |
| 115 | + ); | |
| 116 | + }); | |
| 117 | + | |
| 118 | + it.effect("treats structured EnvironmentResourceNotFoundError from bounded as missing", () => { | |
| 119 | + const fetchFn = ((input: RequestInfo | URL) => { | |
| 120 | + const url = String(input); | |
| 121 | + expect(url).toContain(`/api/orchestration/threads/${THREAD_ID}/bounded`); | |
| 122 | + return Promise.resolve( | |
| 123 | + Response.json( | |
| 124 | + { | |
| 125 | + _tag: "EnvironmentResourceNotFoundError", | |
| 126 | + code: "not_found", | |
| 127 | + reason: "thread_not_found", | |
| 128 | + traceId: "trace-bounded-missing", | |
| 129 | + }, | |
| 130 | + { status: 404 }, | |
| 131 | + ), | |
| 132 | + ); | |
| 133 | + }) satisfies typeof fetch; | |
| 134 | + | |
| 135 | + return Effect.gen(function* () { | |
| 136 | + const loader = yield* ThreadSnapshotLoader; | |
| 137 | + const result = yield* loader.load(PREPARED, THREAD_ID); | |
| 138 | + expect(result).toEqual({ _tag: "missing" }); | |
| 139 | + }).pipe( | |
| 140 | + Effect.provide( | |
| 141 | + Layer.provide(boundedThreadSnapshotLoaderLayer, remoteHttpClientLayer(fetchFn)), | |
| 142 | + ), | |
| 143 | + ); | |
| 144 | + }); | |
| 145 | + | |
| 146 | + it.effect("treats structured missing from full fallback as missing", () => { | |
| 147 | + let fullCalls = 0; | |
| 148 | + const fetchFn = ((input: RequestInfo | URL) => { | |
| 149 | + const url = String(input); | |
| 150 | + if (url.includes("/bounded")) { | |
| 151 | + return Promise.resolve( | |
| 152 | + new Response("Not Found", { | |
| 153 | + status: 404, | |
| 154 | + headers: { "content-type": "text/plain" }, | |
| 155 | + }), | |
| 156 | + ); | |
| 157 | + } | |
| 158 | + fullCalls += 1; | |
| 159 | + return Promise.resolve( | |
| 160 | + Response.json( | |
| 161 | + { | |
| 162 | + _tag: "EnvironmentResourceNotFoundError", | |
| 163 | + code: "not_found", | |
| 164 | + reason: "thread_not_found", | |
| 165 | + traceId: "trace-full-missing", | |
| 166 | + }, | |
| 167 | + { status: 404 }, | |
| 168 | + ), | |
| 169 | + ); | |
| 170 | + }) satisfies typeof fetch; | |
| 171 | + | |
| 172 | + return Effect.gen(function* () { | |
| 173 | + const loader = yield* ThreadSnapshotLoader; | |
| 174 | + const result = yield* loader.load(PREPARED, THREAD_ID); | |
| 175 | + expect(result).toEqual({ _tag: "missing" }); | |
| 176 | + expect(fullCalls).toBe(1); | |
| 177 | + }).pipe( | |
| 178 | + Effect.provide( | |
| 179 | + Layer.provide(boundedThreadSnapshotLoaderLayer, remoteHttpClientLayer(fetchFn)), | |
| 180 | + ), | |
| 181 | + ); | |
| 182 | + }); | |
| 183 | + | |
| 184 | + it.effect( | |
| 185 | + "uses socket fallback directly when the bounded endpoint has a transient failure", | |
| 186 | + () => { | |
| 187 | + let fullCalls = 0; | |
| 188 | + const fetchFn = ((input: RequestInfo | URL) => { | |
| 189 | + const url = String(input); | |
| 190 | + if (url.includes("/bounded")) { | |
| 191 | + return Promise.resolve(new Response("Service Unavailable", { status: 503 })); | |
| 192 | + } | |
| 193 | + fullCalls += 1; | |
| 194 | + return Promise.resolve( | |
| 195 | + new Response(JSON.stringify(FULL_SNAPSHOT_BODY), { | |
| 196 | + status: 200, | |
| 197 | + headers: { "content-type": "application/json" }, | |
| 198 | + }), | |
| 199 | + ); | |
| 200 | + }) satisfies typeof fetch; | |
| 201 | + | |
| 202 | + return Effect.gen(function* () { | |
| 203 | + const loader = yield* ThreadSnapshotLoader; | |
| 204 | + const result = yield* loader.load(PREPARED, THREAD_ID); | |
| 205 | + expect(result).toEqual({ _tag: "unavailable" }); | |
| 206 | + expect(fullCalls).toBe(0); | |
| 207 | + }).pipe( | |
| 208 | + Effect.provide( | |
| 209 | + Layer.provide(boundedThreadSnapshotLoaderLayer, remoteHttpClientLayer(fetchFn)), | |
| 210 | + ), | |
| 211 | + ); | |
| 212 | + }, | |
| 213 | + ); | |
| 214 | +}); | |