Skip to content
Source
Sign inSign up

composerContext.test.ts

256 lines7.5 KB

1import { describe, expect, it } from "vite-plus/test";
2import * as Option from "effect/Option";
3import * as Schema from "effect/Schema";
4​
5import {
6 COMPOSER_CONTEXT_KINDS,
7 ComposerContextRecord,
8 OrchestrationMessageContext,
9} from "./composerContext.ts";
10​
11const decodeRecord = Schema.decodeUnknownOption(ComposerContextRecord);
12const decodeContext = Schema.decodeUnknownSync(OrchestrationMessageContext);
13​
14const base = { version: 1, contextId: "ctx_1" } as const;
15​
16const knownRecords: Record<(typeof COMPOSER_CONTEXT_KINDS)[number], Record<string, unknown>> = {
17 image: {
18 ...base,
19 kind: "image",
20 label: "checkout-error.png",
21 attachmentId: "att_1",
22 name: "checkout-error.png",
23 mimeType: "image/png",
24 sizeBytes: 1234,
25 },
26 file: {
27 ...base,
28 kind: "file",
29 label: "notes.txt",
30 attachmentId: "att_2",
31 name: "notes.txt",
32 mimeType: "text/plain",
33 sizeBytes: 12,
34 },
35 terminal: {
36 ...base,
37 kind: "terminal",
38 label: "Terminal 1 lines 509-514",
39 terminalId: "term-1",
40 terminalLabel: "Terminal 1",
41 lineStart: 509,
42 lineEnd: 514,
43 text: "error: boom\n at main.ts:1",
44 },
45 element: {
46 ...base,
47 kind: "element",
48 label: "<Button>",
49 pageUrl: "http://localhost:3000/checkout",
50 pageTitle: "Checkout",
51 tagName: "button",
52 selector: "#pay",
53 htmlPreview: '<button id="pay">Pay</button>',
54 componentName: "Button",
55 source: { functionName: "Button", fileName: "Button.tsx", lineNumber: 12, columnNumber: 3 },
56 styles: "color: red;",
57 },
58 "preview-annotation": {
59 ...base,
60 kind: "preview-annotation",
61 label: "Checkout",
62 annotationId: "ann_1",
63 pageUrl: "http://localhost:3000/checkout",
64 pageTitle: "Checkout",
65 comment: "Make this bigger",
66 targetSummary: "1 selected element",
67 styleChanges: ["font-size: 20px"],
68 elements: [
69 {
70 pageUrl: "http://localhost:3000/checkout",
71 pageTitle: "Checkout",
72 tagName: "button",
73 selector: "#pay",
74 htmlPreview: "<button>Pay</button>",
75 componentName: "Button",
76 source: null,
77 styles: "",
78 },
79 ],
80 screenshotContextId: "ctx_2",
81 },
82 "review-comment": {
83 ...base,
84 kind: "review-comment",
85 label: "ChatComposer.tsx L4118",
86 sectionId: "diff-1",
87 sectionTitle: "Changes",
88 filePath: "apps/web/src/ChatComposer.tsx",
89 startIndex: 4118,
90 endIndex: 4118,
91 rangeLabel: "L4118",
92 text: "Why is this here?",
93 diff: "+ const x = 1;",
94 fenceLanguage: "diff",
95 pullRequest: {
96 number: 42,
97 title: "Improve context chips",
98 url: "https://github.com/pingdotgg/t3code/pull/42",
99 headBranch: "feat/context-chips",
100 baseBranch: "main",
101 state: "open",
102 isDraft: false,
103 },
104 },
105 mention: { ...base, kind: "mention", label: "@src/index.ts", path: "src/index.ts" },
106 skill: { ...base, kind: "skill", label: "$pinchtab", name: "pinchtab" },
107 thread: {
108 ...base,
109 kind: "thread",
110 label: "Fix login flow",
111 environmentId: "environment-1",
112 threadId: "thread-1",
113 title: "Fix login flow",
114 },
115 environment: {
116 ...base,
117 kind: "environment",
118 label: "vps",
119 environmentId: "environment-vps",
120 },
121 response: {
122 ...base,
123 kind: "response",
124 label: "Reviewer (search) finished",
125 citation: {
126 version: 1,
127 environmentId: "environment-1",
128 threadId: "thread-1",
129 messageId: "message-1",
130 text: "Reviewer (search) finished and returned blocked on one finding.",
131 start: 0,
132 end: 63,
133 prefix: "",
134 suffix: " I read its report",
135 },
136 itemId: "turn-item-1",
137 subagents: [
138 { threadId: "thread-2", title: "review pr 412 (search)" },
139 { threadId: "thread-3", title: "review pr 413", environmentId: "environment-2" },
140 ],
141 },
142};
143​
144describe("ComposerContextRecord", () => {
145 it("keeps unknown kinds with their payload", () => {
146 const decoded = decodeRecord({
147 ...base,
148 kind: "future-thing",
149 label: "Future",
150 payload: { anything: [1, 2, 3] },
151 });
152 expect(Option.getOrThrow(decoded)).toEqual({
153 version: 1,
154 contextId: "ctx_1",
155 kind: "future-thing",
156 label: "Future",
157 payload: { anything: [1, 2, 3] },
158 });
159 });
160​
161 it("does not let a malformed known kind slide through as unknown", () => {
162 expect(Option.isNone(decodeRecord({ ...base, kind: "image", label: "x" }))).toBe(true);
163 });
164​
165 it("bounds the serialized payload of future context kinds", () => {
166 const unknown = { ...base, kind: "future-thing", label: "Future" };
167 expect(Option.isSome(decodeRecord({ ...unknown, payload: "x".repeat(63_998) }))).toBe(true);
168 expect(Option.isNone(decodeRecord({ ...unknown, payload: "x".repeat(63_999) }))).toBe(true);
169 expect(Option.isNone(decodeRecord({ ...unknown, payload: '"'.repeat(32_000) }))).toBe(true);
170 expect(
171 decodeContext({
172 version: 1,
173 records: [
174 { ...unknown, payload: "x".repeat(64_000) },
175 { ...knownRecords.skill, contextId: "ctx_skill" },
176 ],
177 }).records,
178 ).toEqual([{ ...knownRecords.skill, contextId: "ctx_skill" }]);
179 });
180});
181​
182describe("OrchestrationMessageContext", () => {
183 it("rejects aggregate context size even when each record is valid", () => {
184 const record = {
185 ...knownRecords["preview-annotation"],
186 styleChangeDetails: Array.from({ length: 200 }, () => ({
187 targetId: "target",
188 selector: null,
189 property: "content",
190 previousValue: "x".repeat(8_000),
191 value: "y".repeat(8_000),
192 })),
193 };
194 expect(Option.isSome(decodeRecord(record))).toBe(true);
195 expect(() => decodeContext({ version: 1, records: [record] })).not.toThrow();
196 expect(() =>
197 decodeContext({
198 version: 1,
199 records: Array.from({ length: 6 }, (_, index) => ({
200 ...record,
201 contextId: `ctx_${index}`,
202 })),
203 }),
204 ).toThrow();
205 });
206​
207 it("sends a message without the records the wire cannot carry", () => {
208 const encodeOrchestrationMessageContext = Schema.encodeUnknownSync(
209 Schema.toCodecJson(OrchestrationMessageContext),
210 );
211​
212 const wire = encodeOrchestrationMessageContext({
213 version: 1,
214 records: [
215 decodeContext({ version: 1, records: [knownRecords.terminal] }).records[0],
216 {
217 ...base,
218 contextId: "ctx_2",
219 kind: "future-kind",
220 label: "x",
221 payload: { count: Number.NaN },
222 },
223 { ...knownRecords["review-comment"], contextId: "ctx_3", fenceLanguage: undefined },
224 { ...base, contextId: "ctx_4", kind: "future-kind", label: "y", payload: { n: 1n } },
225 ],
226 });
227 expect(decodeContext(wire).records.map((record) => record.contextId)).toEqual(["ctx_1"]);
228 });
229​
230 it("rejects oversized arrays before dropping malformed records", () => {
231 expect(() =>
232 decodeContext({ version: 1, records: Array.from({ length: 201 }, () => ({})) }),
233 ).toThrow();
234 });
235​
236 it("drops undecodable records and keeps valid siblings", () => {
237 const context = decodeContext({
238 version: 1,
239 records: [
240 knownRecords.skill,
241 { version: 1, kind: "image" },
242 { ...knownRecords.terminal, contextId: "ctx_2" },
243 ],
244 });
245 expect(context.records.map((record) => record.kind)).toEqual(["skill", "terminal"]);
246 });
247​
248 it("rejects duplicate normalized context identities", () => {
249 expect(() =>
250 decodeContext({
251 version: 1,
252 records: [knownRecords.skill, { ...knownRecords.terminal, contextId: " ctx_1 " }],
253 }),
254 ).toThrow();
255 });
256});