spb/khaelor Public
KHAELOR — a terminal-native autonomous engineering agent powered by Anthropic.
TypeScript 82.9%
HTML 14.9%
CSS 1.1%
JavaScript 0.7%
1/**2 * KHAELOR3 * File: tests/anthropic/translate.test.ts4 * Description: SSE-to-ModelEvent translation tests — deltas, block settlement, tool-input accumulation, usage, stop reasons.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { describe, expect, it } from "vitest";11import { mapStopReason, translateRawStream } from "../../src/anthropic/client.js";12import { ModelError } from "../../src/anthropic/errors.js";13import type { ModelEvent } from "../../src/anthropic/types.js";14import {15 REQUEST_ID,16 asRawStream,17 blockStop,18 inputJsonDelta,19 messageDelta,20 messageStart,21 messageStop,22 simpleTextStream,23 textBlockStart,24 textDelta,25 toolCallStream,26 toolUseBlockStart,27} from "./fixtures.js";2829async function collect(events: Parameters<typeof asRawStream>[0], now?: () => number): Promise<ModelEvent[]> {30 const out: ModelEvent[] = [];31 for await (const event of translateRawStream(asRawStream(events), now)) {32 out.push(event);33 }34 return out;35}3637describe("translateRawStream", () => {38 it("translates a plain text stream into started / deltas / settled block / completed", async () => {39 let tick = 1_000;40 const events = await collect(simpleTextStream(), () => (tick += 25));4142 expect(events.map((e) => e.type)).toEqual([43 "started",44 "text-delta",45 "text-delta",46 "text-block-completed",47 "completed",48 ]);49 expect(events[0]).toEqual({ type: "started", requestId: REQUEST_ID });50 expect(events[1]).toEqual({ type: "text-delta", blockIndex: 0, text: "Hello" });51 expect(events[2]).toEqual({ type: "text-delta", blockIndex: 0, text: " world" });52 // Settled block is the byte-exact concatenation of the deltas.53 expect(events[3]).toEqual({ type: "text-block-completed", blockIndex: 0, text: "Hello world" });5455 const completed = events[4];56 if (completed?.type !== "completed") throw new Error("expected completed event");57 expect(completed.stopReason).toBe("end_turn");58 expect(completed.durationMs).toBeGreaterThan(0);59 // input tokens from message_start; output tokens from cumulative message_delta.60 expect(completed.usage).toEqual({61 inputTokens: 120,62 outputTokens: 8,63 cacheReadTokens: 0,64 cacheWriteTokens: 0,65 });66 });6768 it("accumulates tool input JSON across input_json_delta chunks and parses it once", async () => {69 const events = await collect(toolCallStream());7071 expect(events.map((e) => e.type)).toEqual([72 "started",73 "thinking-delta",74 "thinking-delta",75 "thinking-block-completed",76 "text-delta",77 "text-block-completed",78 "tool-call-started",79 "tool-input-delta",80 "tool-input-delta",81 "tool-input-delta",82 "tool-call-completed",83 "completed",84 ]);8586 expect(events[3]).toEqual({87 type: "thinking-block-completed",88 blockIndex: 0,89 thinking: "Consider the weather.",90 signature: "sig-abc",91 });92 expect(events[6]).toEqual({93 type: "tool-call-started",94 blockIndex: 2,95 toolUseId: "toolu_01AAA",96 toolName: "get_weather",97 });98 expect(events[7]).toEqual({99 type: "tool-input-delta",100 blockIndex: 2,101 toolUseId: "toolu_01AAA",102 partialJson: '{"city":',103 });104 expect(events[10]).toEqual({105 type: "tool-call-completed",106 blockIndex: 2,107 toolUseId: "toolu_01AAA",108 toolName: "get_weather",109 input: { city: "Paris" },110 });111112 const completed = events[11];113 if (completed?.type !== "completed") throw new Error("expected completed event");114 expect(completed.stopReason).toBe("tool_use");115 expect(completed.usage).toEqual({116 inputTokens: 300,117 outputTokens: 47,118 cacheReadTokens: 200,119 cacheWriteTokens: 0,120 });121 });122123 it("treats an empty tool input as {} (no input_json_delta chunks)", async () => {124 const events = await collect([125 messageStart(),126 toolUseBlockStart(0, "toolu_02BBB", "list_files"),127 blockStop(0),128 messageDelta({ stop_reason: "tool_use" }),129 messageStop(),130 ]);131 const completed = events.find((e) => e.type === "tool-call-completed");132 expect(completed).toEqual({133 type: "tool-call-completed",134 blockIndex: 0,135 toolUseId: "toolu_02BBB",136 toolName: "list_files",137 input: {},138 });139 });140141 it("throws a fatal invalid-request ModelError on malformed tool input JSON", async () => {142 const events = [143 messageStart(),144 toolUseBlockStart(0, "toolu_03CCC", "get_weather"),145 inputJsonDelta(0, '{"city": '),146 blockStop(0), // truncated JSON147 messageDelta({ stop_reason: "tool_use" }),148 messageStop(),149 ];150 await expect(collect(events)).rejects.toSatisfy(151 (e: unknown) => e instanceof ModelError && e.kind === "invalid-request" && !e.retryable,152 );153 });154155 it("updates cache usage fields from message_delta when present", async () => {156 const events = await collect([157 messageStart({ input_tokens: 10, cache_creation_input_tokens: 500 }),158 textBlockStart(0),159 textDelta(0, "ok"),160 blockStop(0),161 messageDelta({ output_tokens: 3, cache_read_input_tokens: 900 }),162 messageStop(),163 ]);164 const completed = events.at(-1);165 if (completed?.type !== "completed") throw new Error("expected completed event");166 expect(completed.usage).toEqual({167 inputTokens: 10,168 outputTokens: 3,169 cacheReadTokens: 900,170 cacheWriteTokens: 500,171 });172 });173174 it("throws a retryable ModelError if the stream ends without message_stop", async () => {175 const events = [messageStart(), textBlockStart(0), textDelta(0, "partial")];176 await expect(collect(events)).rejects.toSatisfy(177 (e: unknown) => e instanceof ModelError && e.kind === "retryable",178 );179 });180181 it("normalizes unknown/stop_sequence stop reasons to end_turn", async () => {182 expect(mapStopReason("stop_sequence")).toBe("end_turn");183 expect(mapStopReason("pause_turn")).toBe("end_turn");184 expect(mapStopReason(null)).toBe("end_turn");185 expect(mapStopReason("tool_use")).toBe("tool_use");186 expect(mapStopReason("max_tokens")).toBe("max_tokens");187 expect(mapStopReason("refusal")).toBe("refusal");188 });189});190