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/cancel.test.ts4 * Description: Cancellation tests — AbortSignal propagation before start, mid-stream, and during retry backoff.5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";11import { APIError, APIUserAbortError } from "@anthropic-ai/sdk";12import type { RawMessageStreamEvent } from "@anthropic-ai/sdk/resources/messages";13import { AnthropicModelClient } from "../../src/anthropic/client.js";14import { ModelError } from "../../src/anthropic/errors.js";15import type { ModelEvent, ModelRequest } from "../../src/anthropic/types.js";16import { asRawStream, messageStart, simpleTextStream, textBlockStart, textDelta } from "./fixtures.js";1718const REQUEST: ModelRequest = {19 model: "claude-sonnet-5",20 system: [{ name: "identity", text: "You are KHAELOR." }],21 messages: [{ role: "user", content: [{ type: "text", text: "hi" }] }],22 tools: [],23 maxOutputTokens: 64,24};2526function isCancelled(e: unknown): boolean {27 return e instanceof ModelError && e.kind === "cancelled";28}2930describe("AnthropicModelClient cancellation", () => {31 it("throws cancelled immediately when the signal is already aborted", async () => {32 const factory = vi.fn();33 const client = new AnthropicModelClient({ apiKey: "test-key", streamFactory: factory });34 const controller = new AbortController();35 controller.abort();3637 await expect(38 (async () => {39 for await (const _ of client.stream(REQUEST, controller.signal)) {40 // unreachable41 }42 })(),43 ).rejects.toSatisfy(isCancelled);44 expect(factory).not.toHaveBeenCalled();45 });4647 it("passes the AbortSignal to the raw stream factory (it reaches the HTTP request)", async () => {48 const factory = vi.fn().mockImplementation(() => Promise.resolve(asRawStream(simpleTextStream())));49 const client = new AnthropicModelClient({ apiKey: "test-key", streamFactory: factory });50 const controller = new AbortController();5152 for await (const _ of client.stream(REQUEST, controller.signal)) {53 // drain54 }55 expect(factory).toHaveBeenCalledWith(expect.anything(), controller.signal);56 });5758 it("aborting mid-stream surfaces ModelError{kind:cancelled} after real deltas arrived", async () => {59 const controller = new AbortController();60 async function* abortableStream(): AsyncGenerator<RawMessageStreamEvent, void, undefined> {61 yield messageStart();62 yield textBlockStart(0);63 yield textDelta(0, "Hel");64 // Simulate the SDK: the aborted HTTP request rejects with APIUserAbortError.65 await new Promise<never>((_, reject) => {66 if (controller.signal.aborted) {67 reject(new APIUserAbortError());68 return;69 }70 controller.signal.addEventListener("abort", () => reject(new APIUserAbortError()), {71 once: true,72 });73 });74 }75 const client = new AnthropicModelClient({76 apiKey: "test-key",77 streamFactory: () => Promise.resolve(abortableStream()),78 });7980 const seen: ModelEvent[] = [];81 await expect(82 (async () => {83 for await (const event of client.stream(REQUEST, controller.signal)) {84 seen.push(event);85 if (event.type === "text-delta") controller.abort();86 }87 })(),88 ).rejects.toSatisfy(isCancelled);89 expect(seen.map((e) => e.type)).toEqual(["started", "text-delta"]);90 });9192 it("aborting during retry backoff cancels promptly without another attempt (fake timers)", async () => {93 vi.useFakeTimers();94 try {95 const factory = vi96 .fn()97 .mockRejectedValue(98 APIError.generate(500, { error: { type: "error", message: "boom" } }, "boom", new Headers()),99 );100 const client = new AnthropicModelClient({ apiKey: "test-key", streamFactory: factory });101 const controller = new AbortController();102103 const promise = (async () => {104 for await (const _ of client.stream(REQUEST, controller.signal)) {105 // unreachable106 }107 })();108 const expectation = expect(promise).rejects.toSatisfy(isCancelled);109110 await vi.advanceTimersByTimeAsync(100); // inside the first ≥250 ms backoff window111 expect(factory).toHaveBeenCalledTimes(1);112 controller.abort();113 await expectation;114 expect(factory).toHaveBeenCalledTimes(1); // no retry after abort115 } finally {116 vi.useRealTimers();117 }118 });119});120