import test from "node:test"; import assert from "node:assert/strict"; import os from "node:os"; import path from "node:path"; import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises"; import { DatabaseSync } from "node:sqlite"; import { buildCodexTaskExecution, prepareCodexTaskExecution, } from "../local-agent/codex-task-runner.mjs"; let runtimeRoot = ""; async function ensureRuntimeRoot() { if (!runtimeRoot) { runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-local-agent-task-runner-")); } return runtimeRoot; } test.after(async () => { if (runtimeRoot) { await rm(runtimeRoot, { recursive: true, force: true }); } }); async function createCodexStateDb(threads) { const root = await ensureRuntimeRoot(); const dbPath = path.join(root, `state-${Math.random().toString(16).slice(2)}.sqlite`); const db = new DatabaseSync(dbPath); db.exec(` CREATE TABLE threads ( id TEXT PRIMARY KEY, rollout_path TEXT NOT NULL, created_at INTEGER NOT NULL, updated_at INTEGER NOT NULL, source TEXT NOT NULL, model_provider TEXT NOT NULL, cwd TEXT NOT NULL, title TEXT NOT NULL, sandbox_policy TEXT NOT NULL, approval_mode TEXT NOT NULL, tokens_used INTEGER NOT NULL DEFAULT 0, has_user_event INTEGER NOT NULL DEFAULT 0, archived INTEGER NOT NULL DEFAULT 0, archived_at INTEGER, git_sha TEXT, git_branch TEXT, git_origin_url TEXT, cli_version TEXT NOT NULL DEFAULT '', first_user_message TEXT NOT NULL DEFAULT '', agent_nickname TEXT, agent_role TEXT, memory_mode TEXT NOT NULL DEFAULT 'enabled', model TEXT, reasoning_effort TEXT, agent_path TEXT ); `); const insertThread = db.prepare(` INSERT INTO threads ( id, rollout_path, created_at, updated_at, source, model_provider, cwd, title, sandbox_policy, approval_mode, tokens_used, has_user_event, archived, cli_version, first_user_message, agent_nickname, agent_role, memory_mode, model, reasoning_effort ) VALUES (?, ?, ?, ?, 'desktop', 'openai', ?, ?, ?, 'never', 0, 1, 0, '0.118.0', '', '', '', 'enabled', 'gpt-5.4', 'medium') `); for (const thread of threads) { insertThread.run( thread.id, thread.rolloutPath ?? path.join(root, `${thread.id}.jsonl`), 1774845600, 1774845618, thread.cwd, thread.title ?? thread.id, thread.sandboxPolicy ?? '{"type":"workspace-write"}', ); } db.close(); return dbPath; } test("conversation reply resumes the real Codex thread when thread ref is available", () => { const execution = buildCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", masterAgentModel: "gpt-5.4", }, { taskType: "conversation_reply", executionPrompt: "请回复用户", sourceMessageId: "msg-1", sourceMessageBody: "请回复用户", sourceMessageSentAt: "2026-04-21T09:00:00.000Z", mirrorBossUserMessageToCodexDesktop: true, targetCodexThreadRef: "019d-thread-real", targetCodexFolderRef: "/Users/kris/code/meiyesaas", }, "/tmp/reply.txt", ); assert.equal(execution.mode, "resume"); assert.equal(execution.cwd, "/Users/kris/code/meiyesaas"); assert.deepEqual(execution.args, [ "exec", "resume", "--skip-git-repo-check", "-o", "/tmp/reply.txt", "-m", "gpt-5.4", "019d-thread-real", "请回复用户", ]); assert.deepEqual(execution.desktopMirror, { enabled: true, targetThreadRef: "019d-thread-real", sourceMessageId: "msg-1", sourceMessageBody: "请回复用户", sourceMessageSentAt: "2026-04-21T09:00:00.000Z", }); }); test("dispatch execution falls back to targetThreadId when codex thread ref is missing", () => { const execution = buildCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", }, { taskType: "dispatch_execution", executionPrompt: "请执行群聊任务", targetThreadId: "019d-thread-fallback", }, "/tmp/reply.txt", ); assert.equal(execution.mode, "resume"); assert.deepEqual(execution.args, [ "exec", "resume", "--skip-git-repo-check", "-o", "/tmp/reply.txt", "019d-thread-fallback", "请执行群聊任务", ]); }); test("master agent reply without target thread stays on ephemeral exec", () => { const execution = buildCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", masterAgentModel: "gpt-5.4", }, { taskType: "conversation_reply", executionPrompt: "你是主 Agent", }, "/tmp/master.txt", ); assert.equal(execution.mode, "ephemeral"); assert.equal(execution.cwd, "/Users/kris/code/boss"); assert.deepEqual(execution.args, [ "exec", "--ephemeral", "--skip-git-repo-check", "-C", "/Users/kris/code/boss", "-s", "workspace-write", "-o", "/tmp/master.txt", "-m", "gpt-5.4", "你是主 Agent", ]); assert.deepEqual(execution.desktopMirror, { enabled: false }); }); test("relay conversation reply mirrors the clean Boss user message into the desktop child thread", () => { const execution = buildCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", masterAgentModel: "gpt-5.4", }, { taskType: "conversation_reply", executionPrompt: "你是主 Agent", relayViaMasterAgent: true, sourceMessageId: "msg-relay", sourceMessageBody: "帮我推进当前线程", sourceMessageSentAt: "2026-04-21T09:10:00.000Z", mirrorBossUserMessageToCodexDesktop: true, targetCodexThreadRef: "019d-thread-real", targetCodexFolderRef: "/Users/kris/code/meiyesaas", }, "/tmp/master.txt", ); assert.deepEqual(execution.desktopMirror, { enabled: true, targetThreadRef: "019d-thread-real", sourceMessageId: "msg-relay", sourceMessageBody: "帮我推进当前线程", sourceMessageSentAt: "2026-04-21T09:10:00.000Z", }); assert.notEqual(execution.desktopMirror.sourceMessageBody, execution.args.at(-1)); }); test("conversation reply preflight fails closed when target cwd is missing", async () => { const missingFolder = "/tmp/boss-local-agent-missing-workdir"; const stateDbPath = await createCodexStateDb([ { id: "019d-thread-real", cwd: "/Users/kris/code/boss", title: "Real thread", }, ]); const result = await prepareCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", codexStateDbPath: stateDbPath, }, { taskType: "conversation_reply", executionPrompt: "请回复用户", targetCodexThreadRef: "019d-thread-real", targetCodexFolderRef: missingFolder, }, "/tmp/reply.txt", ); assert.equal(result.ok, false); assert.equal(result.error.code, "LOCAL_AGENT_CODEX_WORKDIR_INVALID"); assert.match(result.error.message, /LOCAL_AGENT_CODEX_WORKDIR_INVALID/); assert.match(result.error.message, /missing-workdir/); }); test("conversation reply preflight accepts session-only Codex threads when state db is stale", async () => { const root = await ensureRuntimeRoot(); const validCwd = path.join(root, "session-only-project"); const sessionsDir = path.join(root, "sessions-only", "2026", "05", "02"); const threadId = "019d-session-only-task"; await mkdir(validCwd, { recursive: true }); await mkdir(sessionsDir, { recursive: true }); await writeFile( path.join(sessionsDir, `rollout-2026-05-02T10-10-00-${threadId}.jsonl`), `${JSON.stringify({ timestamp: "2026-05-02T02:10:00.000Z", type: "session_meta", payload: { id: threadId, cwd: validCwd, }, })}\n`, "utf8", ); const stateDbPath = await createCodexStateDb([ { id: "019d-thread-other", cwd: validCwd, title: "Other thread", }, ]); const result = await prepareCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", codexStateDbPath: stateDbPath, codexSessionsDir: path.join(root, "sessions-only"), }, { taskType: "conversation_reply", executionPrompt: "请回复用户", targetCodexThreadRef: threadId, targetCodexFolderRef: validCwd, }, "/tmp/reply.txt", ); assert.equal(result.ok, true); assert.equal(result.execution.mode, "resume"); assert.equal(result.execution.cwd, validCwd); assert.deepEqual(result.execution.args.slice(-2), [threadId, "请回复用户"]); }); test("dispatch execution preflight fails closed when target thread ref is missing", async () => { const result = await prepareCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", }, { taskType: "dispatch_execution", executionPrompt: "请执行群聊任务", targetCodexFolderRef: "/Users/kris/code/boss", }, "/tmp/reply.txt", ); assert.equal(result.ok, false); assert.equal(result.error.code, "LOCAL_AGENT_CODEX_THREAD_BINDING_MISSING"); assert.match(result.error.message, /LOCAL_AGENT_CODEX_THREAD_BINDING_MISSING/); }); test("conversation reply preflight fails closed when target thread ref is stale in local Codex state", async () => { const root = await ensureRuntimeRoot(); const validCwd = path.join(root, "project-stale"); await mkdir(validCwd, { recursive: true }); const stateDbPath = await createCodexStateDb([ { id: "019d-thread-other", cwd: validCwd, title: "Other thread", }, ]); const result = await prepareCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", codexStateDbPath: stateDbPath, }, { taskType: "conversation_reply", executionPrompt: "请回复用户", targetCodexThreadRef: "019d-thread-stale", targetCodexFolderRef: validCwd, }, "/tmp/reply.txt", ); assert.equal(result.ok, false); assert.equal(result.error.code, "LOCAL_AGENT_CODEX_THREAD_BINDING_STALE"); assert.match(result.error.message, /LOCAL_AGENT_CODEX_THREAD_BINDING_STALE/); }); test("conversation reply preflight fails closed when target cwd mismatches the live Codex thread binding", async () => { const root = await ensureRuntimeRoot(); const liveCwd = path.join(root, "project-live"); const staleCwd = path.join(root, "project-stale-mismatch"); await mkdir(liveCwd, { recursive: true }); await mkdir(staleCwd, { recursive: true }); const stateDbPath = await createCodexStateDb([ { id: "019d-thread-live", cwd: liveCwd, title: "Live thread", }, ]); const result = await prepareCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", codexStateDbPath: stateDbPath, }, { taskType: "conversation_reply", executionPrompt: "请回复用户", targetCodexThreadRef: "019d-thread-live", targetCodexFolderRef: staleCwd, }, "/tmp/reply.txt", ); assert.equal(result.ok, false); assert.equal(result.error.code, "LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH"); assert.match(result.error.message, /LOCAL_AGENT_CODEX_THREAD_BINDING_MISMATCH/); assert.match(result.error.message, /project-live/); }); test("conversation reply preflight fails closed when target Codex thread is read-only", async () => { const root = await ensureRuntimeRoot(); const readonlyCwd = path.join(root, "project-readonly"); await mkdir(readonlyCwd, { recursive: true }); const stateDbPath = await createCodexStateDb([ { id: "019d-thread-readonly", cwd: readonlyCwd, title: "Read-only thread", sandboxPolicy: '{"type":"read-only"}', }, ]); const result = await prepareCodexTaskExecution( { masterAgentWorkdir: "/Users/kris/code/boss", masterAgentSandbox: "workspace-write", codexStateDbPath: stateDbPath, }, { taskType: "conversation_reply", executionPrompt: "请继续开发", targetCodexThreadRef: "019d-thread-readonly", targetCodexFolderRef: readonlyCwd, }, "/tmp/reply.txt", ); assert.equal(result.ok, false); assert.equal(result.error.code, "LOCAL_AGENT_CODEX_THREAD_READ_ONLY"); assert.match(result.error.message, /LOCAL_AGENT_CODEX_THREAD_READ_ONLY/); assert.match(result.error.message, /read-only/); });