History

feat(orchestration): bound thread history and resume payloads

- Add paginated thread history with bounded snapshots and replay limits - Trim oversized wire payloads and support progressive mobile history loading

Julius Marminge committed 3 months ago

Commit 7d42d311 parent e3883a943 files · +4239 −211Browse files

Showing the changes to packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts (1 of 43 files). Show all

Unified · Split

packages/client-runtime/src/state/boundedThreadSnapshotHttp.test.ts+214−0
@@ -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+});