Skip to content
Source
Sign inSign up

shellCacheEncoding.test.ts

251 lines9.1 KB

1import {
2 CommandId,
3 EnvironmentId,
4 ProjectId,
5 ProviderInstanceId,
6 ThreadId,
7 type OrchestrationV2ShellSnapshot,
8} from "@t3tools/contracts";
9import { describe, expect, it } from "@effect/vitest";
10import * as DateTime from "effect/DateTime";
11import * as Effect from "effect/Effect";
12import * as Schema from "effect/Schema";
13​
14import {
15 ORCHESTRATION_CACHE_SCHEMA_VERSION,
16 StoredOrchestrationShellSnapshot,
17} from "./orchestrationCache.ts";
18import { makeStoredShellSnapshotEncoder } from "./shellCacheEncoding.ts";
19​
20const ENVIRONMENT_ID = EnvironmentId.make("environment-1");
21const PROJECT_ID = ProjectId.make("project-1");
22const THREAD_ID = ThreadId.make("thread-1");
23const PROVIDER_INSTANCE_ID = ProviderInstanceId.make("codex");
24const NOW = DateTime.makeUnsafe("2026-07-29T12:00:00.000Z");
25const SHELL_SNAPSHOT: OrchestrationV2ShellSnapshot = {
26 schemaVersion: 1,
27 snapshotSequence: 3,
28 projects: [
29 {
30 id: PROJECT_ID,
31 title: "Project",
32 workspaceRoot: "/repo",
33 repositoryIdentity: null,
34 defaultModelSelection: null,
35 scripts: [],
36 createdAt: "2026-07-29T12:00:00.000Z",
37 updatedAt: "2026-07-29T12:00:00.000Z",
38 },
39 ],
40 threads: [
41 {
42 id: THREAD_ID,
43 projectId: PROJECT_ID,
44 title: "Thread",
45 providerInstanceId: PROVIDER_INSTANCE_ID,
46 modelSelection: {
47 instanceId: PROVIDER_INSTANCE_ID,
48 model: "gpt-5.4",
49 },
50 runtimeMode: "full-access",
51 interactionMode: "default",
52 branch: null,
53 worktreePath: null,
54 activeProviderThreadId: null,
55 lineage: {
56 rootThreadId: THREAD_ID,
57 parentThreadId: null,
58 relationshipToParent: null,
59 },
60 forkedFrom: null,
61 createdBy: "user",
62 creationSource: "mobile",
63 latestRunId: null,
64 activeRunId: null,
65 status: "idle",
66 pendingRuntimeRequest: null,
67 latestVisibleMessage: null,
68 latestUserMessageAt: NOW,
69 activityRunStartedAt: NOW,
70 activityRunStatus: "running",
71 hasActionableProposedPlan: false,
72 itemCount: 0,
73 visibleItemCount: 0,
74 createdAt: NOW,
75 updatedAt: NOW,
76 archivedAt: null,
77 settledOverride: null,
78 settledAt: NOW,
79 unsettledAt: NOW,
80 lastVisitedAt: null,
81 titleRegeneration: {
82 requestId: CommandId.make("title-regeneration-1"),
83 startedAt: NOW,
84 },
85 deletedAt: null,
86 },
87 ],
88 archivedThreads: [],
89};
90​
91describe("cooperative shell cache encoding", () => {
92 // A fresh encoder per call keeps these cases cold; the cache has its own cases below.
93 const encodeStoredShellSnapshot = (stored: typeof StoredOrchestrationShellSnapshot.Type) =>
94 makeStoredShellSnapshotEncoder()(stored);
95 const encodeOriginal = Schema.encodeEffect(
96 Schema.fromJsonString(StoredOrchestrationShellSnapshot),
97 );
98 const stored = {
99 schemaVersion: ORCHESTRATION_CACHE_SCHEMA_VERSION,
100 environmentId: ENVIRONMENT_ID,
101 snapshot: {
102 ...SHELL_SNAPSHOT,
103 projects: SHELL_SNAPSHOT.projects.map((project) => ({
104 ...project,
105 title: " Project ",
106 unexpected: "drop this field",
107 })),
108 threads: Array.from({ length: 64 }, (_, index) => ({
109 ...SHELL_SNAPSHOT.threads[0]!,
110 id: ThreadId.make(`thread-${index}`),
111 branch: " main ",
112 activityRunStartedAt: index % 2 === 0 ? NOW : null,
113 unsettledAt: index % 2 === 0 ? NOW : null,
114 modelSelection: {
115 instanceId: PROVIDER_INSTANCE_ID,
116 model: " gpt-5.4 ",
117 unexpected: "drop this field",
118 },
119 unexpected: "drop this field",
120 })),
121 archivedThreads: [{ ...SHELL_SNAPSHOT.threads[0]!, archivedAt: NOW }],
122 unexpected: "drop this field",
123 },
124 unexpected: "drop this field",
125 };
126​
127 it.effect("preserves exact JSON, transforms, nulls, dates, and unknown-field stripping", () =>
128 Effect.gen(function* () {
129 const expected = yield* encodeOriginal(stored);
130 const actual = yield* encodeStoredShellSnapshot(stored);
131 expect(actual).toBe(expected);
132 const parsed = JSON.parse(actual);
133 expect(parsed).not.toHaveProperty("unexpected");
134 expect(parsed.snapshot).not.toHaveProperty("unexpected");
135 expect(parsed.snapshot.projects[0]).not.toHaveProperty("unexpected");
136 expect(parsed.snapshot.projects[0].title).toBe("Project");
137 expect(parsed.snapshot.threads[0]).not.toHaveProperty("unexpected");
138 expect(parsed.snapshot.threads[0].branch).toBe("main");
139 expect(parsed.snapshot.threads[0].modelSelection).toEqual({
140 instanceId: "codex",
141 model: "gpt-5.4",
142 });
143 expect(parsed.snapshot.threads[0].activityRunStartedAt).toBe(DateTime.formatIso(NOW));
144 expect(parsed.snapshot.threads[1].activityRunStartedAt).toBeNull();
145 expect(parsed.snapshot.archivedThreads[0].archivedAt).toBe(DateTime.formatIso(NOW));
146 }),
147 );
148​
149 it.effect("preserves optional fields and dates across chunk boundaries and archived rows", () =>
150 Effect.gen(function* () {
151 const base = SHELL_SNAPSHOT.threads[0]!;
152 const { titleRegeneration: _, unsettledAt: __, ...withoutOptional } = base;
153 const row = (index: number) =>
154 index % 3 === 0
155 ? { ...withoutOptional, id: ThreadId.make(`row-${index}`) }
156 : {
157 ...base,
158 id: ThreadId.make(`row-${index}`),
159 snoozedUntil: DateTime.add(NOW, { minutes: index }),
160 pinnedAt: index % 2 === 0 ? null : NOW,
161 };
162 const mixed = {
163 ...stored,
164 snapshot: {
165 ...stored.snapshot,
166 threads: Array.from({ length: 65 }, (_, index) => row(index)),
167 archivedThreads: Array.from({ length: 33 }, (_, index) => ({
168 ...row(index + 100),
169 archivedAt: DateTime.add(NOW, { hours: index }),
170 })),
171 },
172 };
173 const actual = yield* encodeStoredShellSnapshot(mixed);
174 expect(actual).toBe(yield* encodeOriginal(mixed));
175 const parsed = JSON.parse(actual);
176 expect(parsed.snapshot.threads).toHaveLength(65);
177 expect(parsed.snapshot.threads[63]).not.toHaveProperty("titleRegeneration");
178 expect(parsed.snapshot.threads[63]).not.toHaveProperty("unsettledAt");
179 expect(parsed.snapshot.threads[64].snoozedUntil).toBe(
180 DateTime.formatIso(DateTime.add(NOW, { minutes: 64 })),
181 );
182 expect(parsed.snapshot.archivedThreads).toHaveLength(33);
183 expect(parsed.snapshot.archivedThreads[32].archivedAt).toBe(
184 DateTime.formatIso(DateTime.add(NOW, { hours: 32 })),
185 );
186 }),
187 );
188​
189 it.effect("still rejects invalid active and archived rows after an encoding chunk yields", () =>
190 Effect.gen(function* () {
191 const invalidRow = { ...stored.snapshot.threads[0]!, itemCount: -1 };
192 const invalidActive = {
193 ...stored,
194 snapshot: { ...stored.snapshot, threads: [...stored.snapshot.threads, invalidRow] },
195 };
196 const invalidArchived = {
197 ...stored,
198 snapshot: { ...stored.snapshot, archivedThreads: [invalidRow] },
199 };
200 const invalidEnvelope = { ...stored, snapshot: { ...stored.snapshot, snapshotSequence: -1 } };
201 expect(yield* Effect.isFailure(encodeStoredShellSnapshot(invalidActive))).toBe(true);
202 expect(yield* Effect.isFailure(encodeStoredShellSnapshot(invalidArchived))).toBe(true);
203 expect(yield* Effect.isFailure(encodeOriginal(invalidEnvelope))).toBe(true);
204 expect(yield* Effect.isFailure(encodeStoredShellSnapshot(invalidEnvelope))).toBe(true);
205 }),
206 );
207​
208 it.effect("encodes replaced, added, and moved-to-archive rows on a warm encoder", () =>
209 Effect.gen(function* () {
210 const encode = makeStoredShellSnapshotEncoder();
211 yield* encode(stored);
212 const [first, second, ...rest] = stored.snapshot.threads;
213 const next = {
214 ...stored,
215 snapshot: {
216 ...stored.snapshot,
217 threads: [
218 { ...first!, title: "Renamed", itemCount: 9 },
219 ...rest,
220 { ...first!, id: ThreadId.make("thread-new"), branch: " feature " },
221 ],
222 archivedThreads: [
223 ...stored.snapshot.archivedThreads,
224 { ...second!, archivedAt: DateTime.add(NOW, { hours: 1 }) },
225 ],
226 },
227 };
228 const actual = yield* encode(next);
229 expect(actual).toBe(yield* encodeOriginal(next));
230 const parsed = JSON.parse(actual);
231 expect(parsed.snapshot.threads[0].title).toBe("Renamed");
232 expect(parsed.snapshot.threads.at(-1).branch).toBe("feature");
233 expect(parsed.snapshot.archivedThreads).toHaveLength(2);
234 }),
235 );
236​
237 it.effect("does not cache rows from a failed encode", () =>
238 Effect.gen(function* () {
239 const encode = makeStoredShellSnapshotEncoder();
240 const invalidRow = { ...stored.snapshot.threads[0]!, itemCount: -1 };
241 const invalid = {
242 ...stored,
243 snapshot: { ...stored.snapshot, threads: [...stored.snapshot.threads, invalidRow] },
244 };
245 expect(yield* Effect.isFailure(encode(invalid))).toBe(true);
246 expect(yield* Effect.isFailure(encode(invalid))).toBe(true);
247 // Valid rows from the failed save still encode correctly afterwards.
248 expect(yield* encode(stored)).toBe(yield* encodeOriginal(stored));
249 }),
250 );
251});