- 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 | }); | ||