Skip to content
Source
Sign inSign up

threadCacheEncoding.test.ts

141 lines4.8 KB

1import { describe, expect, it } from "@effect/vitest";
2import {
3 EnvironmentId,
4 ThreadId,
5 TurnItemId,
6 type OrchestrationV2ThreadProjection,
7} from "@t3tools/contracts";
8import * as DateTime from "effect/DateTime";
9import * as Effect from "effect/Effect";
10import * as Option from "effect/Option";
11import * as Schema from "effect/Schema";
12​
13import { v2Now, v2Projection, v2ThreadId } from "../state/orchestrationV2TestFixtures.ts";
14import {
15 ORCHESTRATION_THREAD_CACHE_SCHEMA_VERSION,
16 StoredOrchestrationThreadSnapshot,
17} from "./orchestrationCache.ts";
18import {
19 decodeStoredThreadSnapshot,
20 makeStoredThreadSnapshotEncoder,
21} from "./threadCacheEncoding.ts";
22​
23const environmentId = EnvironmentId.make("environment-thread-cache");
24const otherThreadId = ThreadId.make("thread-parent");
25const encodeOriginal = Schema.encodeEffect(
26 Schema.fromJsonString(StoredOrchestrationThreadSnapshot),
27);
28​
29const notice = (index: number, threadId = v2ThreadId) => ({
30 id: TurnItemId.make(`notice-${threadId}-${index}`),
31 threadId,
32 runId: null,
33 nodeId: null,
34 providerThreadId: null,
35 providerTurnId: null,
36 nativeItemRef: null,
37 parentItemId: null,
38 ordinal: index,
39 status: "completed" as const,
40 title: index % 2 === 0 ? null : ` Notice ${index} `,
41 startedAt: DateTime.add(v2Now, { seconds: index }),
42 completedAt: index % 3 === 0 ? null : v2Now,
43 updatedAt: v2Now,
44 type: "system_notice" as const,
45 message: `Notice ${index}`,
46});
47​
48// Inherited rows come first, as in a fork, then local rows whose items alias turnItems.
49function projectionWith(count: number): OrchestrationV2ThreadProjection {
50 const inherited = Array.from({ length: 5 }, (_, index) => notice(index, otherThreadId));
51 const turnItems = Array.from({ length: count }, (_, index) => notice(index));
52 return {
53 ...v2Projection,
54 turnItems,
55 visibleTurnItems: [
56 ...inherited.map((item) => ({
57 position: 0,
58 visibility: "inherited" as const,
59 sourceThreadId: otherThreadId,
60 sourceItemId: item.id,
61 item,
62 })),
63 ...turnItems.map((item) => ({
64 position: 0,
65 visibility: "local" as const,
66 sourceThreadId: v2ThreadId,
67 sourceItemId: item.id,
68 item,
69 })),
70 ].map((row, position) => ({ ...row, position })),
71 };
73​
74const stored = (projection: OrchestrationV2ThreadProjection) => ({
75 environmentId,
76 threadId: v2ThreadId,
77 snapshot: { snapshotSequence: 7, projection, historyCursor: null, hasMoreHistory: false },
78});
79​
80describe("cooperative thread cache encoding", () => {
81 it.effect("matches the plain encoding across chunks, cold and after rows change", () =>
82 Effect.gen(function* () {
83 const encode = makeStoredThreadSnapshotEncoder({ format: "full" });
84 const original = (projection: OrchestrationV2ThreadProjection) =>
85 encodeOriginal({
86 ...stored(projection),
87 schemaVersion: ORCHESTRATION_THREAD_CACHE_SCHEMA_VERSION,
88 });
89 const cold = projectionWith(70);
90 expect(yield* encode(stored(cold))).toEqual(Option.some(yield* original(cold)));
91​
92 const [first, ...rest] = cold.turnItems;
93 const replaced = { ...first!, message: "Edited" };
94 const added = notice(70);
95 const warm: OrchestrationV2ThreadProjection = {
96 ...cold,
97 turnItems: [replaced, ...rest, added],
98 visibleTurnItems: [
99 ...cold.visibleTurnItems.map((row) =>
100 row.item === first ? { ...row, item: replaced } : row,
101 ),
102 {
103 position: cold.visibleTurnItems.length,
104 visibility: "local",
105 sourceThreadId: v2ThreadId,
106 sourceItemId: added.id,
107 item: added,
108 },
109 ],
110 };
111 expect(yield* encode(stored(warm))).toEqual(Option.some(yield* original(warm)));
112 }),
113 );
114​
115 it.effect("round-trips the compact format", () =>
116 Effect.gen(function* () {
117 const projection = projectionWith(40);
118 const encoded = yield* makeStoredThreadSnapshotEncoder({ format: "compact" })(
119 stored(projection),
120 );
121 const decoded = yield* decodeStoredThreadSnapshot(Option.getOrThrow(encoded));
122 expect(decoded.snapshot.projection).toEqual(projection);
123 }),
124 );
125​
126 it.effect("encodes nothing once the rows exceed the budget, warm or cold", () =>
127 Effect.gen(function* () {
128 const small = stored(projectionWith(4));
129 const large = stored(projectionWith(200));
130 const limit = Option.getOrThrow(
131 yield* makeStoredThreadSnapshotEncoder({ format: "full" })(small),
132 ).length;
133 for (const format of ["full", "compact"] as const) {
134 const encode = makeStoredThreadSnapshotEncoder({ format, maxJsonLength: limit });
135 expect(Option.isSome(yield* encode(small))).toBe(true);
136 expect(Option.isNone(yield* encode(large))).toBe(true);
137 expect(Option.isNone(yield* encode(large))).toBe(true);
138 }
139 }),
140 );
141});