1import { describe, expect, it } from "vite-plus/test";
2import * as Cause from "effect/Cause";
3import * as Exit from "effect/Exit";
4import * as Option from "effect/Option";
5import * as Schema from "effect/Schema";
6
7import {
8 COMPOSER_CONTEXT_KINDS,
9 ComposerContextRecord,
10 OrchestrationMessageContext,
11} from "./composerContext.ts";
12import {
13 OrchestrationV2Command,
14 OrchestrationV2ConversationMessageJson,
15} from "./orchestrationV2.ts";
16
17const decodeRecord = Schema.decodeUnknownOption(ComposerContextRecord);
18const decodeContext = Schema.decodeUnknownSync(OrchestrationMessageContext);
19const decodeMessage = Schema.decodeUnknownSync(OrchestrationV2ConversationMessageJson);
20const decodeCommand = Schema.decodeUnknownSync(OrchestrationV2Command);
21
22const base = { version: 1, contextId: "ctx_1" } as const;
23
24const knownRecords: Record<(typeof COMPOSER_CONTEXT_KINDS)[number], Record<string, unknown>> = {
25 image: {
26 ...base,
27 kind: "image",
28 label: "checkout-error.png",
29 attachmentId: "att_1",
30 name: "checkout-error.png",
31 mimeType: "image/png",
32 sizeBytes: 1234,
33 },
34 file: {
35 ...base,
36 kind: "file",
37 label: "notes.txt",
38 attachmentId: "att_2",
39 name: "notes.txt",
40 mimeType: "text/plain",
41 sizeBytes: 12,
42 },
43 terminal: {
44 ...base,
45 kind: "terminal",
46 label: "Terminal 1 lines 509-514",
47 terminalId: "term-1",
48 terminalLabel: "Terminal 1",
49 lineStart: 509,
50 lineEnd: 514,
51 text: "error: boom\n at main.ts:1",
52 },
53 element: {
54 ...base,
55 kind: "element",
56 label: "<Button>",
57 pageUrl: "http://localhost:3000/checkout",
58 pageTitle: "Checkout",
59 tagName: "button",
60 selector: "#pay",
61 htmlPreview: '<button id="pay">Pay</button>',
62 componentName: "Button",
63 source: { functionName: "Button", fileName: "Button.tsx", lineNumber: 12, columnNumber: 3 },
64 styles: "color: red;",
65 },
66 "preview-annotation": {
67 ...base,
68 kind: "preview-annotation",
69 label: "Checkout",
70 annotationId: "ann_1",
71 pageUrl: "http://localhost:3000/checkout",
72 pageTitle: "Checkout",
73 comment: "Make this bigger",
74 targetSummary: "1 selected element",
75 styleChanges: ["font-size: 20px"],
76 elements: [
77 {
78 pageUrl: "http://localhost:3000/checkout",
79 pageTitle: "Checkout",
80 tagName: "button",
81 selector: "#pay",
82 htmlPreview: "<button>Pay</button>",
83 componentName: "Button",
84 source: null,
85 styles: "",
86 },
87 ],
88 screenshotContextId: "ctx_2",
89 },
90 "review-comment": {
91 ...base,
92 kind: "review-comment",
93 label: "ChatComposer.tsx L4118",
94 sectionId: "diff-1",
95 sectionTitle: "Changes",
96 filePath: "apps/web/src/ChatComposer.tsx",
97 startIndex: 4118,
98 endIndex: 4118,
99 rangeLabel: "L4118",
100 text: "Why is this here?",
101 diff: "+ const x = 1;",
102 fenceLanguage: "diff",
103 pullRequest: {
104 number: 42,
105 title: "Improve context chips",
106 url: "https://github.com/pingdotgg/t3code/pull/42",
107 headBranch: "feat/context-chips",
108 baseBranch: "main",
109 state: "open",
110 isDraft: false,
111 },
112 },
113 mention: { ...base, kind: "mention", label: "@src/index.ts", path: "src/index.ts" },
114 skill: { ...base, kind: "skill", label: "$pinchtab", name: "pinchtab" },
115 thread: {
116 ...base,
117 kind: "thread",
118 label: "Fix login flow",
119 environmentId: "environment-1",
120 threadId: "thread-1",
121 title: "Fix login flow",
122 },
123};
124
125describe("ComposerContextRecord", () => {
126 it.each(COMPOSER_CONTEXT_KINDS)("round-trips a %s record", (kind) => {
127 const decoded = decodeRecord(knownRecords[kind]);
128 expect(Option.isSome(decoded)).toBe(true);
129 expect(Option.getOrThrow(decoded)).toEqual(knownRecords[kind]);
130 });
131
132 it("keeps unknown kinds with their payload", () => {
133 const decoded = decodeRecord({
134 ...base,
135 kind: "future-thing",
136 label: "Future",
137 payload: { anything: [1, 2, 3] },
138 });
139 expect(Option.getOrThrow(decoded)).toEqual({
140 version: 1,
141 contextId: "ctx_1",
142 kind: "future-thing",
143 label: "Future",
144 payload: { anything: [1, 2, 3] },
145 });
146 });
147
148 it("does not let a malformed known kind slide through as unknown", () => {
149 expect(Option.isNone(decodeRecord({ ...base, kind: "image", label: "x" }))).toBe(true);
150 });
151
152 it("bounds the serialized payload of future context kinds", () => {
153 const unknown = { ...base, kind: "future-thing", label: "Future" };
154 expect(Option.isSome(decodeRecord({ ...unknown, payload: "x".repeat(63_998) }))).toBe(true);
155 expect(Option.isNone(decodeRecord({ ...unknown, payload: "x".repeat(63_999) }))).toBe(true);
156 expect(Option.isNone(decodeRecord({ ...unknown, payload: '"'.repeat(32_000) }))).toBe(true);
157 expect(
158 decodeContext({
159 version: 1,
160 records: [
161 { ...unknown, payload: "x".repeat(64_000) },
162 { ...knownRecords.skill, contextId: "ctx_skill" },
163 ],
164 }).records,
165 ).toEqual([{ ...knownRecords.skill, contextId: "ctx_skill" }]);
166 });
167
168 it("rejects bad ids and versions", () => {
169 expect(Option.isNone(decodeRecord({ ...knownRecords.skill, contextId: "has space" }))).toBe(
170 true,
171 );
172 expect(Option.isNone(decodeRecord({ ...knownRecords.skill, version: 2 }))).toBe(true);
173 expect(Option.isNone(decodeRecord({ ...knownRecords.skill, kind: "Bad Kind" }))).toBe(true);
174 });
175});
176
177describe("OrchestrationMessageContext", () => {
178 it("rejects aggregate context size even when each record is valid", () => {
179 const record = {
180 ...knownRecords["preview-annotation"],
181 styleChangeDetails: Array.from({ length: 200 }, () => ({
182 targetId: "target",
183 selector: null,
184 property: "content",
185 previousValue: "x".repeat(8_000),
186 value: "y".repeat(8_000),
187 })),
188 };
189 expect(Option.isSome(decodeRecord(record))).toBe(true);
190 expect(() => decodeContext({ version: 1, records: [record] })).not.toThrow();
191 expect(() =>
192 decodeContext({
193 version: 1,
194 records: Array.from({ length: 6 }, (_, index) => ({
195 ...record,
196 contextId: `ctx_${index}`,
197 })),
198 }),
199 ).toThrow();
200 });
201
202 it("sends a message without a record it cannot encode", () => {
203 const wire = Schema.encodeUnknownSync(Schema.toCodecJson(OrchestrationMessageContext))({
204 version: 1,
205 records: [
206 decodeContext({ version: 1, records: [knownRecords.terminal] }).records[0],
207 { ...knownRecords.terminal, contextId: "ctx_2", terminalLabel: " " },
208 ],
209 });
210 expect(decodeContext(wire).records.map((record) => record.contextId)).toEqual(["ctx_1"]);
211 });
212
213 it("sends a message without the records the wire cannot carry", () => {
214 const wire = Schema.encodeUnknownSync(Schema.toCodecJson(OrchestrationMessageContext))({
215 version: 1,
216 records: [
217 decodeContext({ version: 1, records: [knownRecords.terminal] }).records[0],
218 {
219 ...base,
220 contextId: "ctx_2",
221 kind: "future-kind",
222 label: "x",
223 payload: { count: Number.NaN },
224 },
225 { ...knownRecords["review-comment"], contextId: "ctx_3", fenceLanguage: undefined },
226 // JSON.stringify throws on a bigint on every engine.
227 { ...base, contextId: "ctx_4", kind: "future-kind", label: "y", payload: { n: 1n } },
228 ],
229 });
230 expect(decodeContext(wire).records.map((record) => record.contextId)).toEqual(["ctx_1"]);
231 });
232
233 it("reports a hole as a schema issue, even when collecting every issue", () => {
234 const result = Schema.decodeUnknownExit(Schema.toType(OrchestrationMessageContext))(
235 { version: 1, records: [undefined] },
236 { errors: "all" },
237 );
238 expect(Exit.isFailure(result) && Cause.hasFails(result.cause)).toBe(true);
239 });
240
241 it("normalizes decoded record identifiers", () => {
242 const context = decodeContext({
243 version: 1,
244 records: [{ ...knownRecords.skill, contextId: " ctx_1 ", name: " review " }],
245 });
246 expect(context.records[0]).toMatchObject({ contextId: "ctx_1", name: "review" });
247 });
248
249 it("rejects oversized arrays before dropping malformed records", () => {
250 expect(() =>
251 decodeContext({ version: 1, records: Array.from({ length: 201 }, () => ({})) }),
252 ).toThrow();
253 });
254
255 it("drops undecodable records and keeps valid siblings", () => {
256 const context = decodeContext({
257 version: 1,
258 records: [
259 knownRecords.skill,
260 { version: 1, kind: "image" },
261 { ...knownRecords.terminal, contextId: "ctx_2" },
262 ],
263 });
264 expect(context.records.map((record) => record.kind)).toEqual(["skill", "terminal"]);
265 });
266
267 it("rejects duplicate normalized context identities", () => {
268 expect(() =>
269 decodeContext({
270 version: 1,
271 records: [knownRecords.skill, { ...knownRecords.terminal, contextId: " ctx_1 " }],
272 }),
273 ).toThrow();
274 });
275
276 it("is optional on messages and message dispatch commands", () => {
277 const message = {
278 createdBy: "user",
279 creationSource: "web",
280 id: "m1",
281 threadId: "t1",
282 runId: null,
283 nodeId: null,
284 role: "user",
285 text: "hi",
286 attachments: [],
287 streaming: false,
288 createdAt: "2026-01-01T00:00:00.000Z",
289 updatedAt: "2026-01-01T00:00:00.000Z",
290 };
291 expect(decodeMessage(message).context).toBeUndefined();
292 const withContext = decodeMessage({
293 ...message,
294 context: { version: 1, records: [knownRecords.mention] },
295 });
296 expect(withContext.context?.records).toHaveLength(1);
297
298 const command = decodeCommand({
299 type: "message.dispatch",
300 createdBy: "user",
301 creationSource: "web",
302 commandId: "c1",
303 threadId: "t1",
304 messageId: "m1",
305 text: "hi",
306 attachments: [],
307 context: { version: 1, records: [knownRecords.image] },
308 dispatchMode: { type: "start_immediately" },
309 });
310 expect(command.type === "message.dispatch" && command.context?.records[0]?.kind).toBe("image");
311 });
312});