SPB Git

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%
6.1 KB · 190 lines typescript
Raw Blame History
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