/**
 * swarm-dispatch-and-wait.test.mjs — Tests for SwarmDispatchLayer.dispatchAndWait().
 *
 * Purpose: verify that dispatchAndWait enqueues an envelope, drives runAgentTurn,
 * reads the agent reply from the bus, and returns the structured result with `reply`
 * and `replyMessageId` populated.
 *
 * Consumer: CI unit-test suite (`npm run test:unit`).
 */

import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, test, vi } from "vitest";
import { _clearSfRootCache } from "../paths.js";
import { closeDatabase } from "../sf-db.js";
import {
	SwarmDispatchLayer,
	swarmDispatchAndWait,
} from "../uok/swarm-dispatch.js";
import { createDefaultSwarm } from "../uok/swarm-roles.js";

// ─── Mock agent-runner.js ─────────────────────────────────────────────────────
// We stub runAgentTurn so the test never spawns an LLM subprocess. The mock
// writes a deterministic reply to the bus and returns turnsProcessed + replyId,
// exactly as the real implementation does.

const MOCK_REPLY_TEXT = "mock agent reply: task complete";

vi.mock("../uok/agent-runner.js", () => ({
	runAgentTurn: vi.fn(async (agent, opts = {}) => {
		const { onlyMessageId } = opts;

		// When onlyMessageId is set, force-refresh the inbox from SQLite so
		// messages delivered via a different MessageBus instance are visible.
		// This mirrors the real runAgentTurn's Bug 1 fix.
		if (onlyMessageId) {
			agent._inbox.refresh();
		}

		// Isolate to the target message when onlyMessageId is provided, otherwise
		// read all unread messages — mirrors the real runAgentTurn logic.
		let messages;
		if (onlyMessageId) {
			const all = agent.receive(false); // all messages (read + unread)
			const target = all.find((m) => m.id === onlyMessageId && !m.read);
			messages = target ? [target] : [];
		} else {
			messages = agent.receive(true);
		}

		if (messages.length === 0) {
			return { turnsProcessed: 0, response: null };
		}
		// Mark messages as read
		for (const msg of messages) {
			agent.markRead(msg.id);
		}
		// Write a deterministic reply back to the bus — replyTo the last message
		const lastMsg = messages[messages.length - 1];
		const replyId = agent._bus.send(
			`agent:${agent.identity.name}`,
			lastMsg.from,
			MOCK_REPLY_TEXT,
			{ replyTo: lastMsg.id, type: "response" },
		);
		return {
			turnsProcessed: messages.length,
			response: MOCK_REPLY_TEXT,
			replyId,
		};
	}),
}));

// ─── Shared Setup ─────────────────────────────────────────────────────────────

const tmpRoots = [];

beforeEach(() => {
	vi.clearAllMocks();
});

afterEach(() => {
	closeDatabase();
	_clearSfRootCache();
	for (const root of tmpRoots.splice(0)) {
		rmSync(root, { recursive: true, force: true });
	}
});

function makeProject() {
	const root = mkdtempSync(join(tmpdir(), "sf-daw-"));
	tmpRoots.push(root);
	return root;
}

// ─── dispatchAndWait — happy path ─────────────────────────────────────────────

describe("SwarmDispatchLayer.dispatchAndWait — happy path", () => {
	test("returns messageId, targetAgent, reply, and replyMessageId", async () => {
		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const result = await layer.dispatchAndWait({
			unitId: "task-daw-1",
			unitType: "task",
			workMode: "build",
			payload: "implement feature X",
			priority: 5,
			scope: "test-scope",
		});

		expect(result.messageId).toBeTruthy();
		expect(result.targetAgent).toBeTruthy();
		expect(result.reply).toBe(MOCK_REPLY_TEXT);
		expect(result.replyMessageId).toBeTruthy();
		expect(typeof result.replyMessageId).toBe("string");
		expect(result.error).toBeUndefined();
	});

	test("targetAgent is a worker for workMode=build", async () => {
		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const result = await layer.dispatchAndWait({
			unitId: "task-daw-2",
			unitType: "task",
			workMode: "build",
			payload: "build something",
			priority: 3,
			scope: "scope-A",
		});

		const swarm = await layer.getOrCreateSwarm();
		const agent = swarm.get(result.targetAgent);
		expect(agent.identity.role).toBe("worker");
	});

	test("targetAgent is a scout for workMode=research", async () => {
		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const result = await layer.dispatchAndWait({
			unitId: "task-daw-3",
			unitType: "research",
			workMode: "research",
			payload: "research topic Y",
			priority: 2,
			scope: "scope-B",
		});

		const swarm = await layer.getOrCreateSwarm();
		const agent = swarm.get(result.targetAgent);
		expect(agent.identity.role).toBe("scout");
	});
});

// ─── swarmDispatchAndWait — convenience function ───────────────────────────────

describe("swarmDispatchAndWait — convenience function", () => {
	test("delegates to SwarmDispatchLayer and returns structured result", async () => {
		const root = makeProject();
		// Pre-create the swarm so dispatch can route
		await createDefaultSwarm(root);

		const result = await swarmDispatchAndWait(root, {
			unitId: "task-daw-convenience",
			unitType: "task",
			workMode: "build",
			payload: "convenience test payload",
			priority: 1,
			scope: "scope-C",
		});

		expect(result.messageId).toBeTruthy();
		expect(result.targetAgent).toBeTruthy();
		expect(result.reply).toBe(MOCK_REPLY_TEXT);
		expect(result.replyMessageId).toBeTruthy();
	});
});

// ─── dispatchAndWait — error / no-reply paths ─────────────────────────────────

describe("SwarmDispatchLayer.dispatchAndWait — error paths", () => {
	test("returns error when runAgentTurn returns zero turns (empty inbox)", async () => {
		// Override mock for this test to simulate an agent that has no messages
		// (can happen if the bus is inconsistent). We simulate this by having
		// the mock return turnsProcessed=0 after the first call drains the inbox.
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		// Make runAgentTurn drain the message without writing a reply
		runAgentTurn.mockImplementationOnce(async (agent, _opts) => {
			const messages = agent.receive(true);
			for (const msg of messages) agent.markRead(msg.id);
			// Deliberately do NOT write a reply message
			return { turnsProcessed: messages.length, response: null };
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const result = await layer.dispatchAndWait({
			unitId: "task-daw-no-reply",
			unitType: "task",
			workMode: "build",
			payload: "no reply scenario",
			priority: 1,
			scope: "scope-D",
		});

		// No reply message was written, so we expect error="no reply"
		expect(result.reply).toBeNull();
		expect(result.replyMessageId).toBeNull();
		expect(result.error).toBe("no reply");
	});

	test("returns error when runAgentTurn throws", async () => {
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		runAgentTurn.mockImplementationOnce(async () => {
			throw new Error("LLM timeout");
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const result = await layer.dispatchAndWait({
			unitId: "task-daw-throw",
			unitType: "task",
			workMode: "build",
			payload: "throw scenario",
			priority: 1,
			scope: "scope-E",
		});

		expect(result.reply).toBeNull();
		expect(result.replyMessageId).toBeNull();
		expect(result.error).toContain("LLM timeout");
	});

	test("returns error struct not throw when runAgentTurn returns error field", async () => {
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		runAgentTurn.mockImplementationOnce(async () => ({
			turnsProcessed: 0,
			response: null,
			error: "sf headless failed: non-zero exit",
		}));

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const result = await layer.dispatchAndWait({
			unitId: "task-daw-error-field",
			unitType: "task",
			workMode: "build",
			payload: "error field scenario",
			priority: 1,
			scope: "scope-F",
		});

		expect(result.reply).toBeNull();
		expect(result.replyMessageId).toBeNull();
		expect(result.error).toContain("sf headless failed");
	});
});

// ─── Multi-dispatch to the same agent ────────────────────────────────────────

describe("SwarmDispatchLayer.dispatchAndWait — multi-dispatch to same agent", () => {
	test("second dispatch to same agent returns correct reply (not null)", async () => {
		// This covers Bug 1: the agent's in-memory inbox was stale after the first
		// dispatch+turn because INBOX_REFRESH_INTERVAL_MS had not elapsed. The second
		// message arrived via a different MessageBus instance (SwarmDispatchLayer._bus)
		// and was in SQLite but not in agent._inbox._messages. The fix forces a
		// refresh via opts.onlyMessageId before runAgentTurn reads the inbox.
		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		// First dispatch — must succeed
		const result1 = await layer.dispatchAndWait({
			unitId: "task-multi-1",
			unitType: "task",
			workMode: "build",
			payload: "first task",
			priority: 5,
			scope: "scope-multi",
		});

		expect(result1.reply).toBe(MOCK_REPLY_TEXT);
		expect(result1.error).toBeUndefined();

		// Second dispatch to the SAME agent type (build → worker) — must also succeed.
		// Before the fix, runAgentTurn would find the agent's inbox stale and return
		// turnsProcessed=0, causing getReplyTo to find no reply → error="no reply".
		const result2 = await layer.dispatchAndWait({
			unitId: "task-multi-2",
			unitType: "task",
			workMode: "build",
			payload: "second task",
			priority: 5,
			scope: "scope-multi",
		});

		expect(result2.reply).toBe(MOCK_REPLY_TEXT);
		expect(result2.error).toBeUndefined();
		expect(result2.replyMessageId).toBeTruthy();
		// The two dispatches must have produced different reply message ids
		expect(result2.replyMessageId).not.toBe(result1.replyMessageId);
	});

	test("third dispatch to same agent returns correct reply", async () => {
		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		for (let i = 1; i <= 3; i++) {
			const result = await layer.dispatchAndWait({
				unitId: `task-triple-${i}`,
				unitType: "task",
				workMode: "build",
				payload: `task ${i}`,
				priority: 5,
				scope: "scope-triple",
			});
			expect(result.reply).toBe(MOCK_REPLY_TEXT);
			expect(result.error).toBeUndefined();
		}
	});

	test("runAgentTurn receives onlyMessageId and does not pick up stale messages", async () => {
		// Verify that the onlyMessageId option causes the mock runner to process
		// only the target message. The mock in this test file calls agent.receive(false)
		// internally, but here we verify the option is forwarded by checking that the
		// reply targets the correct messageId via getReplyTo.
		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		// Dispatch two tasks to the same agent sequentially
		const r1 = await layer.dispatchAndWait({
			unitId: "task-isolation-1",
			unitType: "task",
			workMode: "build",
			payload: "isolation task 1",
			priority: 5,
			scope: "scope-iso",
		});

		const r2 = await layer.dispatchAndWait({
			unitId: "task-isolation-2",
			unitType: "task",
			workMode: "build",
			payload: "isolation task 2",
			priority: 5,
			scope: "scope-iso",
		});

		// Both replies are correctly wired to their respective dispatch messageIds
		expect(r1.reply).toBe(MOCK_REPLY_TEXT);
		expect(r2.reply).toBe(MOCK_REPLY_TEXT);
		expect(r1.error).toBeUndefined();
		expect(r2.error).toBeUndefined();
	});
});

// ─── Round 7: executorSystemPrompt / executorTools forwarded from envelope ────

describe("SwarmDispatchLayer.dispatchAndWait — Round 7: executor config forwarding", () => {
	test("executorSystemPrompt from envelope is forwarded to runAgentTurn as systemPromptOverride", async () => {
		// dispatchAndWait must extract envelope.executorSystemPrompt and pass it as
		// opts.systemPromptOverride to runAgentTurn so the swarm worker receives the
		// autonomous executor contract (including checkpoint requirement).
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		let capturedOpts = null;
		runAgentTurn.mockImplementationOnce(async (agent, opts = {}) => {
			capturedOpts = opts;
			const { onlyMessageId } = opts;
			if (onlyMessageId) agent._inbox.refresh();
			const all = agent.receive(false);
			const target = all.find((m) => m.id === onlyMessageId && !m.read);
			const messages = target ? [target] : [];
			if (messages.length === 0) return { turnsProcessed: 0, response: null };
			for (const msg of messages) agent.markRead(msg.id);
			const lastMsg = messages[messages.length - 1];
			const replyId = agent._bus.send(
				`agent:${agent.identity.name}`,
				lastMsg.from,
				MOCK_REPLY_TEXT,
				{ replyTo: lastMsg.id, type: "response" },
			);
			return { turnsProcessed: 1, response: MOCK_REPLY_TEXT, replyId };
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const EXECUTOR_PROMPT =
			"You are an autonomous executor. Call checkpoint when done.";
		await layer.dispatchAndWait({
			unitId: "task-r7-sys-prompt",
			unitType: "execute-task",
			workMode: "build",
			payload: "do the work",
			priority: 5,
			scope: "scope-r7",
			executorSystemPrompt: EXECUTOR_PROMPT,
		});

		expect(capturedOpts).not.toBeNull();
		expect(capturedOpts.systemPromptOverride).toBe(EXECUTOR_PROMPT);
	});

	test("executorTools from envelope is forwarded to runAgentTurn as toolsOverride", async () => {
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		let capturedOpts = null;
		runAgentTurn.mockImplementationOnce(async (agent, opts = {}) => {
			capturedOpts = opts;
			const { onlyMessageId } = opts;
			if (onlyMessageId) agent._inbox.refresh();
			const all = agent.receive(false);
			const target = all.find((m) => m.id === onlyMessageId && !m.read);
			const messages = target ? [target] : [];
			if (messages.length === 0) return { turnsProcessed: 0, response: null };
			for (const msg of messages) agent.markRead(msg.id);
			const lastMsg = messages[messages.length - 1];
			const replyId = agent._bus.send(
				`agent:${agent.identity.name}`,
				lastMsg.from,
				MOCK_REPLY_TEXT,
				{ replyTo: lastMsg.id, type: "response" },
			);
			return { turnsProcessed: 1, response: MOCK_REPLY_TEXT, replyId };
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const EXECUTOR_TOOLS = ["Read", "Write", "Bash", "checkpoint"];
		await layer.dispatchAndWait({
			unitId: "task-r7-tools",
			unitType: "execute-task",
			workMode: "build",
			payload: "do the work",
			priority: 5,
			scope: "scope-r7b",
			executorTools: EXECUTOR_TOOLS,
		});

		expect(capturedOpts).not.toBeNull();
		expect(capturedOpts.toolsOverride).toEqual(EXECUTOR_TOOLS);
	});

	test("executorPermissionLevel from envelope is forwarded to runAgentTurn as permissionLevel", async () => {
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		let capturedOpts = null;
		runAgentTurn.mockImplementationOnce(async (agent, opts = {}) => {
			capturedOpts = opts;
			const { onlyMessageId } = opts;
			if (onlyMessageId) agent._inbox.refresh();
			const all = agent.receive(false);
			const target = all.find((m) => m.id === onlyMessageId && !m.read);
			const messages = target ? [target] : [];
			if (messages.length === 0) return { turnsProcessed: 0, response: null };
			for (const msg of messages) agent.markRead(msg.id);
			const lastMsg = messages[messages.length - 1];
			const replyId = agent._bus.send(
				`agent:${agent.identity.name}`,
				lastMsg.from,
				MOCK_REPLY_TEXT,
				{ replyTo: lastMsg.id, type: "response" },
			);
			return { turnsProcessed: 1, response: MOCK_REPLY_TEXT, replyId };
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		await layer.dispatchAndWait({
			unitId: "task-r7-permission",
			unitType: "execute-task",
			workMode: "build",
			payload: "edit files",
			priority: 5,
			scope: "scope-r7-permission",
			executorPermissionLevel: "low",
		});

		expect(capturedOpts).not.toBeNull();
		expect(capturedOpts.permissionLevel).toBe("low");
	});

	test("noOutputTimeoutMs option is forwarded to runAgentTurn", async () => {
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		let capturedOpts = null;
		runAgentTurn.mockImplementationOnce(async (agent, opts = {}) => {
			capturedOpts = opts;
			const { onlyMessageId } = opts;
			if (onlyMessageId) agent._inbox.refresh();
			const all = agent.receive(false);
			const target = all.find((m) => m.id === onlyMessageId && !m.read);
			const messages = target ? [target] : [];
			if (messages.length === 0) return { turnsProcessed: 0, response: null };
			for (const msg of messages) agent.markRead(msg.id);
			const lastMsg = messages[messages.length - 1];
			const replyId = agent._bus.send(
				`agent:${agent.identity.name}`,
				lastMsg.from,
				MOCK_REPLY_TEXT,
				{ replyTo: lastMsg.id, type: "response" },
			);
			return { turnsProcessed: 1, response: MOCK_REPLY_TEXT, replyId };
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		await layer.dispatchAndWait(
			{
				unitId: "task-no-output-timeout",
				unitType: "execute-task",
				workMode: "build",
				payload: "edit files",
				priority: 5,
				scope: "scope-timeout",
			},
			{ noOutputTimeoutMs: 45_000 },
		);

		expect(capturedOpts).not.toBeNull();
		expect(capturedOpts.noOutputTimeoutMs).toBe(45_000);
	});

	test("noOutputTimeoutMs aborts a silent turn even when runtime heartbeats continue", async () => {
		vi.useFakeTimers();
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		runAgentTurn.mockImplementationOnce(async (_agent, opts = {}) => {
			setTimeout(() => {
				opts.onEvent?.({ type: "runtime_heartbeat" });
			}, 5);
			setTimeout(() => {
				opts.onEvent?.({ type: "runtime_heartbeat" });
			}, 9);
			return new Promise(() => {
				// Simulate setup/model code that ignores AbortSignal. The outer watchdog
				// must still resolve dispatchAndWait instead of waiting for this promise.
			});
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);
		const pending = layer.dispatchAndWait(
			{
				unitId: "task-heartbeat-only-timeout",
				unitType: "execute-task",
				workMode: "build",
				payload: "edit files",
				priority: 5,
				scope: "scope-heartbeat-only-timeout",
			},
			{ noOutputTimeoutMs: 10, timeoutMs: 1_000 },
		);

		await vi.advanceTimersByTimeAsync(20);
		const result = await pending;
		vi.useRealTimers();

		expect(result.reply).toBeNull();
		expect(result.error).toContain("produced no output for 10ms");
	});

	test("envelope without executorSystemPrompt does not forward systemPromptOverride", async () => {
		// Envelopes without the optional fields must not pass undefined opts to runAgentTurn.
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		let capturedOpts = null;
		runAgentTurn.mockImplementationOnce(async (agent, opts = {}) => {
			capturedOpts = opts;
			const { onlyMessageId } = opts;
			if (onlyMessageId) agent._inbox.refresh();
			const all = agent.receive(false);
			const target = all.find((m) => m.id === onlyMessageId && !m.read);
			const messages = target ? [target] : [];
			if (messages.length === 0) return { turnsProcessed: 0, response: null };
			for (const msg of messages) agent.markRead(msg.id);
			const lastMsg = messages[messages.length - 1];
			const replyId = agent._bus.send(
				`agent:${agent.identity.name}`,
				lastMsg.from,
				MOCK_REPLY_TEXT,
				{ replyTo: lastMsg.id, type: "response" },
			);
			return { turnsProcessed: 1, response: MOCK_REPLY_TEXT, replyId };
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		await layer.dispatchAndWait({
			unitId: "task-r7-no-override",
			unitType: "task",
			workMode: "build",
			payload: "no override",
			priority: 5,
			scope: "scope-r7c",
			// No executorSystemPrompt or executorTools
		});

		expect(capturedOpts).not.toBeNull();
		expect(capturedOpts.systemPromptOverride).toBeUndefined();
		expect(capturedOpts.toolsOverride).toBeUndefined();
		expect(capturedOpts.permissionLevel).toBeUndefined();
	});
});

// ─── Round 7: checkpoint tool call emitted by worker → outcome propagates ────

describe("SwarmDispatchLayer.dispatchAndWait — Round 7: checkpoint outcome end-to-end", () => {
	test("worker emitting checkpoint tool_call with outcome=complete is received via onEvent", async () => {
		// End-to-end integration test: the mock runAgentTurn emits a checkpoint
		// toolcall_end event via opts.onEvent before returning. The caller's onEvent
		// callback must receive it so runUnitViaSwarm can detect outcome=complete.
		// (This mirrors what a real runSubagent would do when the worker calls checkpoint.)
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		const CHECKPOINT_EVENT = {
			type: "message_update",
			assistantMessageEvent: {
				type: "toolcall_end",
				contentIndex: 0,
				toolCall: {
					type: "toolCall",
					id: "tc-checkpoint-1",
					name: "checkpoint",
					arguments: {
						outcome: "complete",
						summary: "Unit finished successfully.",
						completedItems: ["feature done", "tests passing"],
						remainingItems: [],
						verificationEvidence: ["npm test: green"],
						unitType: "execute-task",
						unitId: "task-r7-e2e",
						pdd: {
							purpose: "signal completion",
							consumer: "orchestrator",
							contract: "outcome=complete",
							failureBoundary: "none",
							evidence: "tests green",
							nonGoals: "n/a",
							invariants: "accurate outcome",
							assumptions: "tests are definitive",
						},
					},
				},
				partial: {},
			},
		};

		runAgentTurn.mockImplementationOnce(async (agent, opts = {}) => {
			const { onlyMessageId, onEvent } = opts;
			if (onlyMessageId) agent._inbox.refresh();
			const all = agent.receive(false);
			const target = all.find((m) => m.id === onlyMessageId && !m.read);
			const messages = target ? [target] : [];
			if (messages.length === 0) return { turnsProcessed: 0, response: null };
			for (const msg of messages) agent.markRead(msg.id);

			// Emit checkpoint event — simulates a real worker calling the checkpoint tool
			if (typeof onEvent === "function") {
				onEvent(CHECKPOINT_EVENT);
			}

			const lastMsg = messages[messages.length - 1];
			const replyId = agent._bus.send(
				`agent:${agent.identity.name}`,
				lastMsg.from,
				"Task completed.",
				{ replyTo: lastMsg.id, type: "response" },
			);
			return { turnsProcessed: 1, response: "Task completed.", replyId };
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const receivedEvents = [];
		const result = await layer.dispatchAndWait(
			{
				unitId: "task-r7-e2e",
				unitType: "execute-task",
				workMode: "build",
				payload: "implement feature",
				priority: 5,
				scope: "scope-r7-e2e",
				executorSystemPrompt: "You must call checkpoint when done.",
			},
			{ onEvent: (evt) => receivedEvents.push(evt) },
		);

		expect(result.reply).toBe("Task completed.");
		expect(result.error).toBeUndefined();

		// The checkpoint event must have been received by the caller
		const checkpointEvent = receivedEvents.find(
			(e) =>
				e?.assistantMessageEvent?.toolCall?.name === "checkpoint" &&
				e?.assistantMessageEvent?.toolCall?.arguments?.outcome === "complete",
		);
		expect(checkpointEvent).toBeDefined();
	});
});

// ─── onEvent propagation through dispatchAndWait ─────────────────────────────

describe("SwarmDispatchLayer.dispatchAndWait — onEvent propagation", () => {
	test("onEvent is forwarded to runAgentTurn and events emitted by the mock are received", async () => {
		// This test verifies the end-to-end onEvent threading:
		// dispatchAndWait(options.onEvent) → runAgentTurn(opts.onEvent) → runHeadlessPrompt(opts.onEvent)
		// We override the mock runAgentTurn for this test to call opts.onEvent with a
		// synthetic tool-call event, then verify the caller's onEvent received it.
		const { runAgentTurn } = await import("../uok/agent-runner.js");

		const SYNTHETIC_EVENT = {
			type: "message_update",
			assistantMessageEvent: {
				type: "toolcall_end",
				contentIndex: 0,
				toolCall: {
					type: "toolCall",
					id: "tc-001",
					name: "checkpoint",
					arguments: { outcome: "complete", summary: "Task done" },
				},
				partial: {},
			},
		};

		// Override: call onEvent with the synthetic event, then write a normal reply.
		runAgentTurn.mockImplementationOnce(async (agent, opts = {}) => {
			const { onlyMessageId, onEvent } = opts;

			if (onlyMessageId) agent._inbox.refresh();
			const all = agent.receive(false);
			const target = all.find((m) => m.id === onlyMessageId && !m.read);
			const messages = target ? [target] : [];

			if (messages.length === 0) return { turnsProcessed: 0, response: null };
			for (const msg of messages) agent.markRead(msg.id);

			// Emit the synthetic event to the caller's onEvent callback
			if (typeof onEvent === "function") {
				onEvent(SYNTHETIC_EVENT);
			}

			const lastMsg = messages[messages.length - 1];
			const replyId = agent._bus.send(
				`agent:${agent.identity.name}`,
				lastMsg.from,
				MOCK_REPLY_TEXT,
				{ replyTo: lastMsg.id, type: "response" },
			);
			return { turnsProcessed: 1, response: MOCK_REPLY_TEXT, replyId };
		});

		const root = makeProject();
		const layer = new SwarmDispatchLayer(root);

		const receivedEvents = [];
		const result = await layer.dispatchAndWait(
			{
				unitId: "task-event-1",
				unitType: "task",
				workMode: "build",
				payload: "propagation test",
				priority: 5,
				scope: "scope-event",
			},
			{ onEvent: (event) => receivedEvents.push(event) },
		);

		expect(result.reply).toBe(MOCK_REPLY_TEXT);
		expect(result.error).toBeUndefined();
		// The synthetic event must have been received by the caller's onEvent.
		expect(receivedEvents).toHaveLength(1);
		expect(receivedEvents[0]).toBe(SYNTHETIC_EVENT);
	});
});
