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.9 KB · 117 lines typescript
Raw Blame History
1/**2 * KHAELOR3 * File: tests/agent/steering.test.ts4 * Description: Steering tests — mid-turn instructions queue durably and inject only at the safe seams (ADR-11).5 *6 * Author: Simon-Pierre Boucher7 * Contact: contact@spboucher.ai8 */910import { rmSync, writeFileSync } from "node:fs";11import * as path from "node:path";12import { afterEach, describe, expect, it } from "vitest";13import type { AnthropicMessage } from "../../src/anthropic/index.js";14import { assertPairingSafe, makeHarness, textTurn, toolTurn } from "./fixtures.js";15import type { AgentHarness } from "./fixtures.js";1617let harness: AgentHarness | null = null;1819afterEach(async () => {20  if (harness !== null) {21    await harness.log.close();22    rmSync(harness.dir, { recursive: true, force: true });23    harness = null;24  }25});2627function messageText(message: AnthropicMessage | undefined): string {28  return (message?.content ?? [])29    .map((block) => (block.type === "text" ? block.text : ""))30    .join("\n");31}3233describe("queued steering", () => {34  it("queues mid-stream text and injects it at the post-tool-batch seam, never mid-stream", async () => {35    const STEER = "Also check the README while you are at it.";36    harness = await makeHarness({37      turns: [38        {39          ...toolTurn("req_1", "Reading the file.", [40            { toolUseId: "toolu_1", toolName: "read", input: { file_path: "a.txt" } },41          ]),42          // The user types while the model is streaming — queued, not injected.43          onStart: () => harness?.steering.queue(STEER),44        },45        textTurn("req_2", "Done, including the README."),46      ],47    });48    writeFileSync(path.join(harness.dir, "a.txt"), "alpha\n", "utf8");4950    harness.user("Read a.txt");51    const outcome = await harness.kernel.runTurn();52    expect(outcome.kind).toBe("done");5354    const events = harness.events();55    const queued = events.find((event) => event.type === "user.steering-queued");56    const injected = events.find((event) => event.type === "user.steering-injected");57    if (queued?.type !== "user.steering-queued") throw new Error("expected steering-queued");58    if (injected?.type !== "user.steering-injected") throw new Error("expected steering-injected");59    expect(injected.payload.queuedEventId).toBe(queued.id);60    expect(injected.payload.seam).toBe("post-tool-batch");6162    // Injection lands AFTER the tool batch: afterSeq is the tool result's seq.63    const toolResult = events.find((event) => event.type === "tool.completed");64    if (toolResult?.type !== "tool.completed") throw new Error("expected tool.completed");65    expect(injected.payload.afterSeq).toBe(toolResult.seq);66    expect(injected.seq).toBeGreaterThan(toolResult.seq);6768    // First request was already in flight — it must NOT contain the steering text.69    const [first, second] = harness.client.requests;70    expect(JSON.stringify(first?.messages)).not.toContain(STEER);7172    // Second request: steering rides inside the tool-result user message —73    // never a new bare user message breaking role alternation.74    const lastMessage = second?.messages[second.messages.length - 1];75    expect(lastMessage?.role).toBe("user");76    expect(lastMessage?.content.some((block) => block.type === "tool_result")).toBe(true);77    expect(messageText(lastMessage)).toContain(STEER);78    assertPairingSafe(events);79  });8081  it("injects steering queued before the model call at the pre-model-call seam", async () => {82    const STEER = "Prefer a one-line answer.";83    harness = await makeHarness({ turns: [textTurn("req_1", "42.")] });84    harness.user("What is the answer?");85    harness.steering.queue(STEER);8687    const outcome = await harness.kernel.runTurn();88    expect(outcome.kind).toBe("done");8990    const injected = harness.events().find((event) => event.type === "user.steering-injected");91    if (injected?.type !== "user.steering-injected") throw new Error("expected steering-injected");92    expect(injected.payload.seam).toBe("pre-model-call");9394    // The queued text joined the pending user message of the FIRST request.95    const first = harness.client.requests[0];96    const firstMessage = first?.messages[0];97    expect(firstMessage?.role).toBe("user");98    expect(messageText(firstMessage)).toContain("What is the answer?");99    expect(messageText(firstMessage)).toContain(STEER);100  });101102  it("drains multiple queued instructions in order at one seam", async () => {103    harness = await makeHarness({ turns: [textTurn("req_1", "ok")] });104    harness.user("Task");105    harness.steering.queue("First note.");106    harness.steering.queue("Second note.");107    expect(harness.steering.pending()).toHaveLength(2);108109    await harness.kernel.runTurn();110    expect(harness.steering.pending()).toHaveLength(0);111112    const text = messageText(harness.client.requests[0]?.messages[0]);113    expect(text.indexOf("First note.")).toBeGreaterThan(-1);114    expect(text.indexOf("First note.")).toBeLessThan(text.indexOf("Second note."));115  });116});117