import { describe, it, expect, beforeEach } from "bun:test" import { ContextCollector } from "./collector" import { injectPendingContext, createContextInjectorHook, createContextInjectorMessagesTransformHook, } from "./injector" describe("injectPendingContext", () => { let collector: ContextCollector beforeEach(() => { collector = new ContextCollector() }) describe("when parts have text content", () => { it("prepends context to first text part", () => { // #given const sessionID = "ses_inject1" collector.register(sessionID, { id: "ulw", source: "keyword-detector", content: "Ultrawork mode activated", }) const parts = [{ type: "text", text: "User message" }] // #when const result = injectPendingContext(collector, sessionID, parts) // #then expect(result.injected).toBe(true) expect(parts[0].text).toContain("Ultrawork mode activated") expect(parts[0].text).toContain("User message") }) it("uses separator between context and original message", () => { // #given const sessionID = "ses_inject2" collector.register(sessionID, { id: "ctx", source: "keyword-detector", content: "Context content", }) const parts = [{ type: "text", text: "Original message" }] // #when injectPendingContext(collector, sessionID, parts) // #then expect(parts[0].text).toBe("Context content\n\n---\n\nOriginal message") }) it("consumes context after injection", () => { // #given const sessionID = "ses_inject3" collector.register(sessionID, { id: "ctx", source: "keyword-detector", content: "Context", }) const parts = [{ type: "text", text: "Message" }] // #when injectPendingContext(collector, sessionID, parts) // #then expect(collector.hasPending(sessionID)).toBe(false) }) it("returns injected=false when no pending context", () => { // #given const sessionID = "ses_empty" const parts = [{ type: "text", text: "Message" }] // #when const result = injectPendingContext(collector, sessionID, parts) // #then expect(result.injected).toBe(false) expect(parts[0].text).toBe("Message") }) }) describe("when parts have no text content", () => { it("does not inject and preserves context", () => { // #given const sessionID = "ses_notext" collector.register(sessionID, { id: "ctx", source: "keyword-detector", content: "Context", }) const parts = [{ type: "image", url: "https://example.com/img.png" }] // #when const result = injectPendingContext(collector, sessionID, parts) // #then expect(result.injected).toBe(false) expect(collector.hasPending(sessionID)).toBe(true) }) }) describe("with multiple text parts", () => { it("injects into first text part only", () => { // #given const sessionID = "ses_multi" collector.register(sessionID, { id: "ctx", source: "keyword-detector", content: "Context", }) const parts = [ { type: "text", text: "First" }, { type: "text", text: "Second" }, ] // #when injectPendingContext(collector, sessionID, parts) // #then expect(parts[0].text).toContain("Context") expect(parts[1].text).toBe("Second") }) }) }) describe("createContextInjectorHook", () => { let collector: ContextCollector beforeEach(() => { collector = new ContextCollector() }) describe("chat.message handler", () => { it("injects pending context into output parts", async () => { // #given const hook = createContextInjectorHook(collector) const sessionID = "ses_hook1" collector.register(sessionID, { id: "ctx", source: "keyword-detector", content: "Hook context", }) const input = { sessionID } const output = { message: {}, parts: [{ type: "text", text: "User message" }], } // #when await hook["chat.message"](input, output) // #then expect(output.parts[0].text).toContain("Hook context") expect(output.parts[0].text).toContain("User message") expect(collector.hasPending(sessionID)).toBe(false) }) it("does nothing when no pending context", async () => { // #given const hook = createContextInjectorHook(collector) const sessionID = "ses_hook2" const input = { sessionID } const output = { message: {}, parts: [{ type: "text", text: "User message" }], } // #when await hook["chat.message"](input, output) // #then expect(output.parts[0].text).toBe("User message") }) }) }) describe("createContextInjectorMessagesTransformHook", () => { let collector: ContextCollector beforeEach(() => { collector = new ContextCollector() }) const createMockMessage = ( role: "user" | "assistant", text: string, sessionID: string ) => ({ info: { id: `msg_${Date.now()}_${Math.random()}`, sessionID, role, time: { created: Date.now() }, agent: "Sisyphus", model: { providerID: "test", modelID: "test" }, path: { cwd: "/", root: "/" }, }, parts: [ { id: `part_${Date.now()}`, sessionID, messageID: `msg_${Date.now()}`, type: "text" as const, text, }, ], }) it("prepends context to last user message", async () => { // #given const hook = createContextInjectorMessagesTransformHook(collector) const sessionID = "ses_transform1" collector.register(sessionID, { id: "ulw", source: "keyword-detector", content: "Ultrawork context", }) const messages = [ createMockMessage("user", "First message", sessionID), createMockMessage("assistant", "Response", sessionID), createMockMessage("user", "Second message", sessionID), ] // eslint-disable-next-line @typescript-eslint/no-explicit-any const output = { messages } as any // #when await hook["experimental.chat.messages.transform"]!({}, output) // #then expect(output.messages.length).toBe(3) expect(output.messages[2].parts[0].text).toBe("Ultrawork context\n\n---\n\nSecond message") }) it("does nothing when no pending context", async () => { // #given const hook = createContextInjectorMessagesTransformHook(collector) const sessionID = "ses_transform2" const messages = [createMockMessage("user", "Hello world", sessionID)] // eslint-disable-next-line @typescript-eslint/no-explicit-any const output = { messages } as any // #when await hook["experimental.chat.messages.transform"]!({}, output) // #then expect(output.messages.length).toBe(1) }) it("does nothing when no user messages", async () => { // #given const hook = createContextInjectorMessagesTransformHook(collector) const sessionID = "ses_transform3" collector.register(sessionID, { id: "ctx", source: "keyword-detector", content: "Context", }) const messages = [createMockMessage("assistant", "Response", sessionID)] // eslint-disable-next-line @typescript-eslint/no-explicit-any const output = { messages } as any // #when await hook["experimental.chat.messages.transform"]!({}, output) // #then expect(output.messages.length).toBe(1) expect(collector.hasPending(sessionID)).toBe(true) }) it("consumes context after injection", async () => { // #given const hook = createContextInjectorMessagesTransformHook(collector) const sessionID = "ses_transform4" collector.register(sessionID, { id: "ctx", source: "keyword-detector", content: "Context", }) const messages = [createMockMessage("user", "Message", sessionID)] // eslint-disable-next-line @typescript-eslint/no-explicit-any const output = { messages } as any // #when await hook["experimental.chat.messages.transform"]!({}, output) // #then expect(collector.hasPending(sessionID)).toBe(false) }) })