@bastani/atomic 0.5.21 → 0.5.22-0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -112,9 +112,13 @@ describe("renderMessagesToText", () => {
112
112
 
113
113
  // --- Copilot ---
114
114
 
115
- test("extracts content from a copilot assistant.message event", () => {
116
- const messages: SavedMessage[] = [makeCopilotAssistantEvent("Hello from Copilot")];
117
- expect(renderMessagesToText(messages)).toBe("Hello from Copilot");
115
+ test("renders a copilot assistant.message under an Assistant header", () => {
116
+ const messages: SavedMessage[] = [
117
+ makeCopilotAssistantEvent("Hello from Copilot"),
118
+ ];
119
+ expect(renderMessagesToText(messages)).toBe(
120
+ "### Assistant\n\nHello from Copilot",
121
+ );
118
122
  });
119
123
 
120
124
  test("skips copilot non-assistant events (session.start)", () => {
@@ -122,26 +126,30 @@ describe("renderMessagesToText", () => {
122
126
  expect(renderMessagesToText(messages)).toBe("");
123
127
  });
124
128
 
125
- test("only includes copilot assistant.message events when mixed with other event types", () => {
129
+ test("only renders copilot assistant.message events when mixed with other event types", () => {
126
130
  const messages: SavedMessage[] = [
127
131
  makeCopilotSessionStartEvent(),
128
132
  makeCopilotAssistantEvent("First response"),
129
133
  makeCopilotSessionStartEvent(),
130
134
  makeCopilotAssistantEvent("Second response"),
131
135
  ];
132
- expect(renderMessagesToText(messages)).toBe("First response\n\nSecond response");
136
+ expect(renderMessagesToText(messages)).toBe(
137
+ "### Assistant\n\nFirst response\n\n### Assistant\n\nSecond response",
138
+ );
133
139
  });
134
140
 
135
141
  // --- OpenCode ---
136
142
 
137
- test("joins opencode text parts with newlines", () => {
143
+ test("renders opencode text parts under an Assistant header", () => {
138
144
  const messages: SavedMessage[] = [
139
145
  makeOpenCodeMessage([
140
146
  { type: "text", text: "Line one" },
141
147
  { type: "text", text: "Line two" },
142
148
  ]),
143
149
  ];
144
- expect(renderMessagesToText(messages)).toBe("Line one\nLine two");
150
+ expect(renderMessagesToText(messages)).toBe(
151
+ "### Assistant\n\nLine one\n\nLine two",
152
+ );
145
153
  });
146
154
 
147
155
  test("filters out non-text parts from opencode messages", () => {
@@ -162,24 +170,32 @@ describe("renderMessagesToText", () => {
162
170
  { type: "subtask", text: "" },
163
171
  ]),
164
172
  ];
165
- expect(renderMessagesToText(messages)).toBe("The answer is 42");
173
+ expect(renderMessagesToText(messages)).toBe(
174
+ "### Assistant\n\nThe answer is 42",
175
+ );
166
176
  });
167
177
 
168
178
  // --- Claude ---
169
179
 
170
- test("returns string message from claude assistant with plain string message", () => {
171
- const messages: SavedMessage[] = [makeClaudeMessage("assistant", "Plain string output")];
172
- expect(renderMessagesToText(messages)).toBe("Plain string output");
180
+ test("renders a claude assistant string message under an Assistant header", () => {
181
+ const messages: SavedMessage[] = [
182
+ makeClaudeMessage("assistant", "Plain string output"),
183
+ ];
184
+ expect(renderMessagesToText(messages)).toBe(
185
+ "### Assistant\n\nPlain string output",
186
+ );
173
187
  });
174
188
 
175
- test("returns content when claude assistant message is an object with content string", () => {
189
+ test("renders claude assistant message with content as string under an Assistant header", () => {
176
190
  const messages: SavedMessage[] = [
177
191
  makeClaudeMessage("assistant", { content: "Content field string" }),
178
192
  ];
179
- expect(renderMessagesToText(messages)).toBe("Content field string");
193
+ expect(renderMessagesToText(messages)).toBe(
194
+ "### Assistant\n\nContent field string",
195
+ );
180
196
  });
181
197
 
182
- test("joins text blocks when claude assistant message has content as text block array", () => {
198
+ test("joins claude text blocks with a double newline under a single Assistant header", () => {
183
199
  const messages: SavedMessage[] = [
184
200
  makeClaudeMessage("assistant", {
185
201
  content: [
@@ -188,48 +204,176 @@ describe("renderMessagesToText", () => {
188
204
  ],
189
205
  }),
190
206
  ];
191
- expect(renderMessagesToText(messages)).toBe("Block one\nBlock two");
207
+ expect(renderMessagesToText(messages)).toBe(
208
+ "### Assistant\n\nBlock one\n\nBlock two",
209
+ );
192
210
  });
193
211
 
194
- test("skips claude user messages", () => {
195
- const messages: SavedMessage[] = [makeClaudeMessage("user", "user prompt")];
196
- expect(renderMessagesToText(messages)).toBe("");
212
+ test("renders a claude user string message under a User header", () => {
213
+ const messages: SavedMessage[] = [
214
+ makeClaudeMessage("user", "user prompt"),
215
+ ];
216
+ expect(renderMessagesToText(messages)).toBe("### User\n\nuser prompt");
197
217
  });
198
218
 
199
219
  test("skips claude system messages", () => {
200
- const messages: SavedMessage[] = [makeClaudeMessage("system", "system instructions")];
220
+ const messages: SavedMessage[] = [
221
+ makeClaudeMessage("system", "system instructions"),
222
+ ];
201
223
  expect(renderMessagesToText(messages)).toBe("");
202
224
  });
203
225
 
204
- test("returns empty string for claude assistant with unknown message shape", () => {
226
+ test("returns empty string for a claude assistant message with an unknown content shape", () => {
205
227
  const unknownMsg = { weird: "shape", count: 99 };
206
- const messages: SavedMessage[] = [makeClaudeMessage("assistant", unknownMsg)];
228
+ const messages: SavedMessage[] = [
229
+ makeClaudeMessage("assistant", unknownMsg),
230
+ ];
207
231
  expect(renderMessagesToText(messages)).toBe("");
208
232
  });
209
233
 
210
- test("extracts text blocks from mixed claude content array (text + tool_use)", () => {
234
+ test("renders tool_use blocks inline with text under a single Assistant header", () => {
211
235
  const messages: SavedMessage[] = [
212
236
  makeClaudeMessage("assistant", {
213
237
  content: [
214
238
  { type: "text", text: "I'll read the file" },
215
- { type: "tool_use", id: "tu-1", name: "Read", input: { path: "/tmp/foo" } },
239
+ {
240
+ type: "tool_use",
241
+ id: "tu-1",
242
+ name: "Read",
243
+ input: { path: "/tmp/foo" },
244
+ },
216
245
  { type: "text", text: "Here's what I found" },
217
246
  ],
218
247
  }),
219
248
  ];
220
- expect(renderMessagesToText(messages)).toBe("I'll read the file\nHere's what I found");
249
+ expect(renderMessagesToText(messages)).toBe(
250
+ [
251
+ "### Assistant",
252
+ "",
253
+ "I'll read the file",
254
+ "",
255
+ "**→ `Read`**",
256
+ "",
257
+ "```json",
258
+ "{\n \"path\": \"/tmp/foo\"\n}",
259
+ "```",
260
+ "",
261
+ "Here's what I found",
262
+ ].join("\n"),
263
+ );
264
+ });
265
+
266
+ test("skips claude `thinking` blocks in the rendered transcript", () => {
267
+ const messages: SavedMessage[] = [
268
+ makeClaudeMessage("assistant", {
269
+ content: [
270
+ { type: "thinking", thinking: "internal reasoning…", signature: "sig" },
271
+ { type: "text", text: "Public answer" },
272
+ ],
273
+ }),
274
+ ];
275
+ expect(renderMessagesToText(messages)).toBe(
276
+ "### Assistant\n\nPublic answer",
277
+ );
278
+ });
279
+
280
+ test("omits `tool_result` payloads entirely — only the call and subsequent assistant turns survive", () => {
281
+ const big = "x".repeat(5_000);
282
+ const messages: SavedMessage[] = [
283
+ makeClaudeMessage("assistant", {
284
+ content: [
285
+ {
286
+ type: "tool_use",
287
+ id: "tu-42",
288
+ name: "Read",
289
+ input: { file_path: "/tmp/note.md" },
290
+ },
291
+ ],
292
+ }),
293
+ makeClaudeMessage("user", {
294
+ content: [
295
+ { type: "tool_result", tool_use_id: "tu-42", content: big },
296
+ ],
297
+ }),
298
+ makeClaudeMessage("assistant", {
299
+ content: [{ type: "text", text: "Done." }],
300
+ }),
301
+ ];
302
+ const rendered = renderMessagesToText(messages);
303
+
304
+ // The tool call itself is present, with its input JSON.
305
+ expect(rendered).toContain("**→ `Read`**");
306
+ expect(rendered).toContain("/tmp/note.md");
307
+
308
+ // The follow-up assistant turn is present.
309
+ expect(rendered).toContain("### Assistant\n\nDone.");
310
+
311
+ // The tool_result payload is completely absent — not truncated, not
312
+ // labelled, not present in any form. This is the context-rot guard:
313
+ // even a 5_000-char result must not leak into the transcript.
314
+ expect(rendered).not.toContain("xxxxx");
315
+ expect(rendered).not.toContain("← `Read` result");
316
+ expect(rendered).not.toContain("← `Read`");
317
+ });
318
+
319
+ test("truncates very long tool_use `input` payloads with a `[+N chars]` suffix", () => {
320
+ const bigCommand = "echo " + "a".repeat(5_000);
321
+ const messages: SavedMessage[] = [
322
+ makeClaudeMessage("assistant", {
323
+ content: [
324
+ {
325
+ type: "tool_use",
326
+ id: "tu-big",
327
+ name: "Bash",
328
+ input: { command: bigCommand },
329
+ },
330
+ ],
331
+ }),
332
+ ];
333
+ const rendered = renderMessagesToText(messages);
334
+ expect(rendered).toContain("**→ `Bash`**");
335
+ // Input budget is 800 chars of JSON — the long command must be truncated.
336
+ expect(rendered).toContain("chars]");
337
+ expect(rendered.length).toBeLessThan(bigCommand.length);
338
+ });
339
+
340
+ test("skips a user message whose only content is `tool_result` blocks", () => {
341
+ const messages: SavedMessage[] = [
342
+ makeClaudeMessage("user", {
343
+ content: [
344
+ {
345
+ type: "tool_result",
346
+ tool_use_id: "tu-a",
347
+ content: "would-be-noisy output",
348
+ },
349
+ ],
350
+ }),
351
+ ];
352
+ expect(renderMessagesToText(messages)).toBe("");
221
353
  });
222
354
 
223
355
  // --- Mixed providers ---
224
356
 
225
- test("joins messages from mixed providers with double newlines", () => {
357
+ test("joins messages from mixed providers with double newlines and provider-appropriate headers", () => {
226
358
  const messages: SavedMessage[] = [
227
359
  makeCopilotAssistantEvent("Copilot says hello"),
228
360
  makeOpenCodeMessage([{ type: "text", text: "OpenCode says hello" }]),
229
361
  makeClaudeMessage("assistant", "Claude says hello"),
230
362
  ];
231
363
  expect(renderMessagesToText(messages)).toBe(
232
- "Copilot says hello\n\nOpenCode says hello\n\nClaude says hello",
364
+ [
365
+ "### Assistant",
366
+ "",
367
+ "Copilot says hello",
368
+ "",
369
+ "### Assistant",
370
+ "",
371
+ "OpenCode says hello",
372
+ "",
373
+ "### Assistant",
374
+ "",
375
+ "Claude says hello",
376
+ ].join("\n"),
233
377
  );
234
378
  });
235
379
 
@@ -239,7 +383,9 @@ describe("renderMessagesToText", () => {
239
383
  makeCopilotAssistantEvent("Only one has content"),
240
384
  makeOpenCodeMessage([{ type: "reasoning", text: "ignored" }]),
241
385
  ];
242
- expect(renderMessagesToText(messages)).toBe("Only one has content");
386
+ expect(renderMessagesToText(messages)).toBe(
387
+ "### Assistant\n\nOnly one has content",
388
+ );
243
389
  });
244
390
  });
245
391