|
1 | 1 | /** Tests CLI JSON/JSONL output parsing, streamed deltas, and error extraction. */ |
| 2 | +import { readFileSync } from "node:fs"; |
2 | 3 | import { describe, expect, it } from "vitest"; |
3 | 4 | import { |
4 | 5 | createCliJsonlStreamingParser, |
|
7 | 8 | parseCliJsonl, |
8 | 9 | parseCliOutput, |
9 | 10 | supportsCliJsonlToolEvents, |
| 11 | + type CliThinkingProgress, |
10 | 12 | type CliToolResultDelta, |
11 | 13 | type CliToolUseStartDelta, |
12 | 14 | } from "./cli-output.js"; |
@@ -1286,7 +1288,7 @@ describe("createCliJsonlStreamingParser", () => { |
1286 | 1288 | event: { |
1287 | 1289 | type: "content_block_delta", |
1288 | 1290 | index: 1, |
1289 | | - delta: { type: "input_json_delta", partial_json: "{\"file_path\":\"x\"}" }, |
| 1291 | + delta: { type: "input_json_delta", partial_json: '{"file_path":"x"}' }, |
1290 | 1292 | }, |
1291 | 1293 | }), |
1292 | 1294 | JSON.stringify({ |
@@ -1322,6 +1324,127 @@ describe("createCliJsonlStreamingParser", () => { |
1322 | 1324 | ]); |
1323 | 1325 | }); |
1324 | 1326 |
|
| 1327 | + it("streams indexless thinking deltas from content block framing", () => { |
| 1328 | + const thinking: Array<{ text: string; delta: string; isReasoningSnapshot?: boolean }> = []; |
| 1329 | + const parser = createCliJsonlStreamingParser({ |
| 1330 | + backend: { |
| 1331 | + command: "local-cli", |
| 1332 | + output: "jsonl", |
| 1333 | + jsonlDialect: "claude-stream-json", |
| 1334 | + sessionIdFields: ["session_id"], |
| 1335 | + }, |
| 1336 | + providerId: "local-cli", |
| 1337 | + onAssistantDelta: () => {}, |
| 1338 | + onThinkingDelta: (delta) => thinking.push(delta), |
| 1339 | + }); |
| 1340 | + |
| 1341 | + parser.push( |
| 1342 | + [ |
| 1343 | + JSON.stringify({ |
| 1344 | + type: "stream_event", |
| 1345 | + event: { |
| 1346 | + type: "message_start", |
| 1347 | + message: { id: "msg-1" }, |
| 1348 | + }, |
| 1349 | + }), |
| 1350 | + JSON.stringify({ |
| 1351 | + type: "stream_event", |
| 1352 | + event: { |
| 1353 | + type: "content_block_start", |
| 1354 | + content_block: { type: "thinking" }, |
| 1355 | + }, |
| 1356 | + }), |
| 1357 | + JSON.stringify({ |
| 1358 | + type: "stream_event", |
| 1359 | + event: { |
| 1360 | + type: "content_block_delta", |
| 1361 | + delta: { type: "thinking_delta", thinking: "A" }, |
| 1362 | + }, |
| 1363 | + }), |
| 1364 | + JSON.stringify({ |
| 1365 | + type: "stream_event", |
| 1366 | + event: { type: "content_block_stop" }, |
| 1367 | + }), |
| 1368 | + JSON.stringify({ |
| 1369 | + type: "stream_event", |
| 1370 | + event: { |
| 1371 | + type: "content_block_start", |
| 1372 | + content_block: { type: "tool_use", id: "tool-1", name: "Read" }, |
| 1373 | + }, |
| 1374 | + }), |
| 1375 | + JSON.stringify({ |
| 1376 | + type: "stream_event", |
| 1377 | + event: { |
| 1378 | + type: "content_block_delta", |
| 1379 | + delta: { type: "input_json_delta", partial_json: '{"file_path":"x"}' }, |
| 1380 | + }, |
| 1381 | + }), |
| 1382 | + JSON.stringify({ |
| 1383 | + type: "stream_event", |
| 1384 | + event: { type: "content_block_stop" }, |
| 1385 | + }), |
| 1386 | + JSON.stringify({ |
| 1387 | + type: "stream_event", |
| 1388 | + event: { |
| 1389 | + type: "content_block_start", |
| 1390 | + content_block: { type: "thinking" }, |
| 1391 | + }, |
| 1392 | + }), |
| 1393 | + JSON.stringify({ |
| 1394 | + type: "stream_event", |
| 1395 | + event: { |
| 1396 | + type: "content_block_delta", |
| 1397 | + delta: { type: "thinking_delta", thinking: "B" }, |
| 1398 | + }, |
| 1399 | + }), |
| 1400 | + JSON.stringify({ |
| 1401 | + type: "stream_event", |
| 1402 | + event: { type: "content_block_stop" }, |
| 1403 | + }), |
| 1404 | + JSON.stringify({ |
| 1405 | + type: "assistant", |
| 1406 | + message: { |
| 1407 | + id: "msg-1", |
| 1408 | + content: [{ type: "text", text: "Answer." }], |
| 1409 | + }, |
| 1410 | + }), |
| 1411 | + ].join("\n"), |
| 1412 | + ); |
| 1413 | + parser.finish(); |
| 1414 | + |
| 1415 | + expect(thinking).toEqual([ |
| 1416 | + { text: "A", delta: "A", isReasoningSnapshot: true }, |
| 1417 | + { text: "AB", delta: "B", isReasoningSnapshot: true }, |
| 1418 | + ]); |
| 1419 | + }); |
| 1420 | + |
| 1421 | + it("emits token progress for Claude CLI 2.1 empty thinking deltas", () => { |
| 1422 | + const thinking: Array<{ text: string; delta: string; isReasoningSnapshot?: boolean }> = []; |
| 1423 | + const progress: CliThinkingProgress[] = []; |
| 1424 | + const parser = createCliJsonlStreamingParser({ |
| 1425 | + backend: { |
| 1426 | + command: "claude", |
| 1427 | + output: "jsonl", |
| 1428 | + jsonlDialect: "claude-stream-json", |
| 1429 | + sessionIdFields: ["session_id"], |
| 1430 | + }, |
| 1431 | + providerId: "claude-cli", |
| 1432 | + onAssistantDelta: () => {}, |
| 1433 | + onThinkingDelta: (delta) => thinking.push(delta), |
| 1434 | + onThinkingProgress: (payload) => progress.push(payload), |
| 1435 | + }); |
| 1436 | + |
| 1437 | + parser.push(readFileSync("test/fixtures/cli/claude-2.1-thinking-progress.jsonl", "utf8")); |
| 1438 | + parser.finish(); |
| 1439 | + |
| 1440 | + expect(thinking).toEqual([]); |
| 1441 | + expect(progress).toEqual([ |
| 1442 | + { progressTokens: 50 }, |
| 1443 | + { progressTokens: 200 }, |
| 1444 | + { progressTokens: 300 }, |
| 1445 | + ]); |
| 1446 | + }); |
| 1447 | + |
1325 | 1448 | it("resets per-index thinking state on a new message within the same turn (tool round-trip)", () => { |
1326 | 1449 | const thinking: Array<{ text: string; delta: string; isReasoningSnapshot?: boolean }> = []; |
1327 | 1450 | const parser = createCliJsonlStreamingParser({ |
@@ -1410,7 +1533,7 @@ describe("createCliJsonlStreamingParser", () => { |
1410 | 1533 | ]); |
1411 | 1534 | }); |
1412 | 1535 |
|
1413 | | - it("ignores thinking deltas without a numeric content-block index", () => { |
| 1536 | + it("ignores indexless thinking deltas without content block framing", () => { |
1414 | 1537 | const thinking: Array<{ text: string; delta: string; isReasoningSnapshot?: boolean }> = []; |
1415 | 1538 | const parser = createCliJsonlStreamingParser({ |
1416 | 1539 | backend: { |
|
0 commit comments