import { beforeEach, describe, expect, mock, test } from "bun:test"; // persistWakeTriggerMessage syncs each row to the disk view and publishes a // sync invalidation; stub both so this unit test stays a pure DB round-trip // without touching the filesystem or the event hub. mock.module("../../persistence/conversation-disk-view.js", () => ({ syncMessageToDisk: () => {}, })); mock.module("../../runtime/sync/resource-sync-events.js", () => ({ publishConversationMessagesChanged: () => {}, })); import { createConversation, getMessages, } from "../../persistence/conversation-crud.js"; import { initializeDb } from "../../persistence/db-init.js"; import type { Message } from "../../providers/types.js"; import type { CompletedBackgroundTool } from "../../tools/background-tool-registry.js"; import type { Conversation } from "../conversation.js"; import { persistWakeTriggerMessage } from "../wake-conversation-ops.js"; await initializeDb(); /** Minimal Conversation stub exercising only the fields the function reads. */ function makeConversationStub( conversationId: string, clientState: { hasNoClient?: boolean; headlessLock?: boolean } = {}, ): Conversation { return { conversationId, trustContext: undefined, hasNoClient: clientState.hasNoClient ?? false, headlessLock: clientState.headlessLock ?? false, getTurnChannelContext: () => null, getTurnInterfaceContext: () => null, } as unknown as Conversation; } function triggerMessage(): Message { return { role: "user", content: [ { type: "text", text: 'Background command completed (id=bg-1, exit=0):', }, ], }; } function readMetadata(conversationId: string): Record { const rows = getMessages(conversationId); expect(rows.length).toBe(1); const raw = rows[0]?.metadata; expect(raw).toBeTruthy(); return JSON.parse(raw as string) as Record; } describe("persistWakeTriggerMessage backgroundToolCompletion", () => { let conversationId: string; beforeEach(() => { conversationId = createConversation("wake-completion-test").id; }); test("round-trips a completion onto the persisted metadata", async () => { const completion: CompletedBackgroundTool = { id: "bg-1", toolName: "bash", conversationId, command: "sleep 1 && echo done", startedAt: 1_700_000_000_000, status: "completed", exitCode: 0, output: "done\n", completedAt: 1_700_000_001_000, }; await persistWakeTriggerMessage( makeConversationStub(conversationId), triggerMessage(), "background-tool", false, completion, ); const metadata = readMetadata(conversationId); expect(metadata.backgroundToolCompletion).toEqual(completion); // Existing wake-trigger metadata is unaffected. expect(metadata.kind).toBe("background-event"); expect(metadata.backgroundEventSource).toBe("background-tool"); expect(metadata.automated).toBe(true); // A non-clientless wake on a client-connected conversation runs interactive, // and that mode is recorded for later retries. expect(metadata.backgroundEventInteractive).toBe(true); }); test("omits the completion key and records non-interactive for a clientless wake", async () => { await persistWakeTriggerMessage( makeConversationStub(conversationId), triggerMessage(), "background-tool", true, ); const metadata = readMetadata(conversationId); expect("backgroundToolCompletion" in metadata).toBe(false); expect(metadata.kind).toBe("background-event"); // A clientless wake (interrupted-turn recovery, local IPC wake) pins // `hasNoClient` for its dispatch, so it records the non-interactive mode. expect(metadata.backgroundEventInteractive).toBe(false); }); test("records non-interactive for a cold-hydrated wake without clientless", async () => { // A conversation cold-hydrated after a restart carries `hasNoClient = true` // (no client attached). A scheduled wake omits `clientless`, yet the loop // still resolves the turn non-interactive from `hasNoClient`, so the // recorded mode is false rather than the requested `!clientless`. await persistWakeTriggerMessage( makeConversationStub(conversationId, { hasNoClient: true }), triggerMessage(), "schedule", false, ); const metadata = readMetadata(conversationId); expect(metadata.backgroundEventSource).toBe("schedule"); expect(metadata.backgroundEventInteractive).toBe(false); }); });