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