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/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