import test from "node:test"; import assert from "node:assert/strict"; import os from "node:os"; import path from "node:path"; import { mkdtemp, rm } from "node:fs/promises"; let runtimeRoot = ""; let readState: (typeof import("../src/lib/boss-data"))["readState"]; let writeState: (typeof import("../src/lib/boss-data"))["writeState"]; let detectProjectExecutionConflict: (typeof import("../src/lib/boss-data"))["detectProjectExecutionConflict"]; let applyProjectConflictDecision: (typeof import("../src/lib/boss-data"))["applyProjectConflictDecision"]; let queueMasterAgentTask: (typeof import("../src/lib/boss-data"))["queueMasterAgentTask"]; let claimNextMasterAgentTask: (typeof import("../src/lib/boss-data"))["claimNextMasterAgentTask"]; let completeMasterAgentTask: (typeof import("../src/lib/boss-data"))["completeMasterAgentTask"]; let updateDevice: (typeof import("../src/lib/boss-data"))["updateDevice"]; let upsertDeviceHeartbeat: (typeof import("../src/lib/boss-data"))["upsertDeviceHeartbeat"]; async function setup() { if (runtimeRoot) return; runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-conflict-")); process.env.BOSS_RUNTIME_ROOT = runtimeRoot; process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); const data = await import("../src/lib/boss-data.ts"); readState = data.readState; writeState = data.writeState; detectProjectExecutionConflict = data.detectProjectExecutionConflict; applyProjectConflictDecision = data.applyProjectConflictDecision; queueMasterAgentTask = data.queueMasterAgentTask; claimNextMasterAgentTask = data.claimNextMasterAgentTask; completeMasterAgentTask = data.completeMasterAgentTask; updateDevice = data.updateDevice; upsertDeviceHeartbeat = data.upsertDeviceHeartbeat; } test.after(async () => { if (runtimeRoot) { await rm(runtimeRoot, { recursive: true, force: true }); } }); test.beforeEach(async () => { await setup(); await rm(runtimeRoot, { recursive: true, force: true }); }); function buildProjectFolderKey(project: Awaited>["projects"][number]) { const folderRef = (project.threadMeta.codexFolderRef?.trim() || project.threadMeta.folderName.trim()).toLowerCase(); return `${project.deviceIds[0]}:${folderRef}`; } async function getCliProject() { const state = await readState(); let project = state.projects.find( (item) => !item.isGroup && item.id !== "master-agent" && item.deviceIds.includes("mac-studio"), ); if (!project) { project = { id: "thread-ui", name: "Boss UI", pinned: false, deviceIds: ["mac-studio"], preview: "线程执行中", updatedAt: "2026-04-06T10:00:00.000Z", lastMessageAt: "2026-04-06T10:00:00.000Z", isGroup: false, threadMeta: { projectId: "thread-ui", threadId: "thread-ui-main", threadDisplayName: "Boss UI 主线程", folderName: "boss", activityIconCount: 1, updatedAt: "2026-04-06T10:00:00.000Z", codexThreadRef: "thread-ui-main", codexFolderRef: "boss", }, groupMembers: [], createdByAgent: true, collaborationMode: "development", approvalState: "not_required", unreadCount: 0, riskLevel: "medium", contextBudgetPct: 64, contextBudgetLabel: "64%", messages: [], goals: [], versions: [], }; state.projects.push(project); await writeState(state); } return project; } test("detectProjectExecutionConflict blocks cli execution when the same folder has new external activity", async () => { await setup(); const state = await readState(); state.projectExecutionPolicies = []; await writeState(state); const result = await detectProjectExecutionConflict({ deviceId: "mac-studio", folderKey: "mac-studio:boss", projectId: "thread-ui", executionMode: "cli", activityAt: "2026-04-06T10:05:00.000Z", externalActivityAt: "2026-04-06T10:04:00.000Z", }); assert.equal(result.blocked, true); assert.equal(result.policy.allowPolicy, "forbid"); assert.equal(result.policy.conflictState, "blocked"); }); test("allow_once only clears the active folder conflict after a single execution", async () => { await setup(); await applyProjectConflictDecision({ deviceId: "mac-studio", folderKey: "mac-studio:boss", projectId: "thread-ui", decision: "allow_once", }); let result = await detectProjectExecutionConflict({ deviceId: "mac-studio", folderKey: "mac-studio:boss", projectId: "thread-ui", executionMode: "cli", activityAt: "2026-04-06T10:10:00.000Z", externalActivityAt: "2026-04-06T10:09:00.000Z", }); assert.equal(result.blocked, false); assert.equal(result.policy.allowPolicy, "allow_once"); result = await detectProjectExecutionConflict({ deviceId: "mac-studio", folderKey: "mac-studio:boss", projectId: "thread-ui", executionMode: "cli", activityAt: "2026-04-06T10:20:00.000Z", externalActivityAt: "2026-04-06T10:19:00.000Z", }); assert.equal(result.blocked, false); assert.equal(result.policy.allowPolicy, "allow_once"); }); test("allow_always applies only to the active folder and does not unlock other folders on the same device", async () => { await setup(); await applyProjectConflictDecision({ deviceId: "mac-studio", folderKey: "mac-studio:boss", projectId: "thread-ui", decision: "allow_always", }); const allowed = await detectProjectExecutionConflict({ deviceId: "mac-studio", folderKey: "mac-studio:boss", projectId: "thread-ui", executionMode: "cli", activityAt: "2026-04-06T10:30:00.000Z", externalActivityAt: "2026-04-06T10:29:00.000Z", }); assert.equal(allowed.blocked, false); assert.equal(allowed.policy.allowPolicy, "allow_always"); const blocked = await detectProjectExecutionConflict({ deviceId: "mac-studio", folderKey: "mac-studio:talking", projectId: "thread-talking", executionMode: "cli", activityAt: "2026-04-06T10:31:00.000Z", externalActivityAt: "2026-04-06T10:30:00.000Z", }); assert.equal(blocked.blocked, true); assert.equal(blocked.policy.allowPolicy, "forbid"); }); test("claimNextMasterAgentTask keeps conversation replies queued when the device prefers gui mode", async () => { await setup(); const project = await getCliProject(); await updateDevice("mac-studio", { preferredExecutionMode: "gui", }); const task = await queueMasterAgentTask({ projectId: project.id, requestMessageId: "msg-preferred-gui", requestText: "继续推进当前线程任务", executionPrompt: "请继续推进当前线程任务", requestedBy: "Boss 超级管理员", requestedByAccount: "krisolo", deviceId: "mac-studio", taskType: "conversation_reply", targetProjectId: project.id, targetThreadId: project.threadMeta.threadId, targetThreadDisplayName: project.threadMeta.threadDisplayName, targetCodexThreadRef: project.threadMeta.codexThreadRef, targetCodexFolderRef: project.threadMeta.codexFolderRef, }); const claimed = await claimNextMasterAgentTask("mac-studio"); assert.equal(claimed, null); const state = await readState(); const queued = state.masterAgentTasks.find((item) => item.taskId === task.taskId); assert.equal(queued?.status, "queued"); }); test("heartbeat external activity on an active cli folder blocks the next claim until the user explicitly allows it", async () => { await setup(); const project = await getCliProject(); const folderKey = buildProjectFolderKey(project); const recentExternalActivityAt = new Date(Date.now() - 60_000).toISOString(); const firstTask = await queueMasterAgentTask({ projectId: project.id, requestMessageId: "msg-first", requestText: "先推进一轮", executionPrompt: "请先推进一轮", requestedBy: "Boss 超级管理员", requestedByAccount: "krisolo", deviceId: "mac-studio", taskType: "conversation_reply", targetProjectId: project.id, targetThreadId: project.threadMeta.threadId, targetThreadDisplayName: project.threadMeta.threadDisplayName, targetCodexThreadRef: project.threadMeta.codexThreadRef, targetCodexFolderRef: project.threadMeta.codexFolderRef, }); const claimedFirst = await claimNextMasterAgentTask("mac-studio"); assert.equal(claimedFirst?.taskId, firstTask.taskId); await upsertDeviceHeartbeat({ deviceId: "mac-studio", name: "Mac Studio", avatar: "M", account: "krisolo", status: "online", quota5h: 72, quota7d: 86, projects: [project.threadMeta.folderName], projectCandidates: [ { folderName: project.threadMeta.folderName, folderRef: project.threadMeta.codexFolderRef, threadId: project.threadMeta.threadId, threadDisplayName: project.threadMeta.threadDisplayName, codexFolderRef: project.threadMeta.codexFolderRef, codexThreadRef: project.threadMeta.codexThreadRef, lastActiveAt: recentExternalActivityAt, suggestedImport: true, }, ], }); let state = await readState(); let policy = state.projectExecutionPolicies.find((item) => item.folderKey === folderKey); assert.ok(policy, "expected heartbeat to persist a scoped conflict policy"); assert.equal(policy?.activeCliExecution, true); assert.equal(policy?.conflictState, "blocked"); assert.equal(policy?.recentExternalActivityAt, recentExternalActivityAt); const secondTask = await queueMasterAgentTask({ projectId: project.id, requestMessageId: "msg-second", requestText: "继续推进第二轮", executionPrompt: "请继续推进第二轮", requestedBy: "Boss 超级管理员", requestedByAccount: "krisolo", deviceId: "mac-studio", taskType: "conversation_reply", targetProjectId: project.id, targetThreadId: project.threadMeta.threadId, targetThreadDisplayName: project.threadMeta.threadDisplayName, targetCodexThreadRef: project.threadMeta.codexThreadRef, targetCodexFolderRef: project.threadMeta.codexFolderRef, }); const blockedClaim = await claimNextMasterAgentTask("mac-studio"); assert.equal(blockedClaim, null); await applyProjectConflictDecision({ deviceId: "mac-studio", folderKey, projectId: project.id, decision: "allow_once", }); const allowedClaim = await claimNextMasterAgentTask("mac-studio"); assert.equal(allowedClaim?.taskId, secondTask.taskId); state = await readState(); policy = state.projectExecutionPolicies.find((item) => item.folderKey === folderKey); assert.equal(policy?.allowPolicy, "allow_once"); await completeMasterAgentTask({ taskId: secondTask.taskId, deviceId: "mac-studio", status: "completed", replyBody: "第二轮已完成", targetProjectId: project.id, targetThreadId: project.threadMeta.threadId, }); state = await readState(); policy = state.projectExecutionPolicies.find((item) => item.folderKey === folderKey); assert.ok(policy, "expected scoped policy to remain after consuming allow_once"); assert.equal(policy?.allowPolicy, "forbid"); assert.equal(policy?.activeCliExecution, false); assert.equal(policy?.conflictState, "blocked"); }); test("stale blocked policy does not keep queued conversation replies stuck forever", async () => { await setup(); const project = await getCliProject(); const folderKey = buildProjectFolderKey(project); const state = await readState(); state.projectExecutionPolicies = [ { deviceId: "mac-studio", folderKey, projectId: project.id, allowPolicy: "forbid", conflictState: "blocked", recentExternalActivityAt: "2026-04-06T09:30:00.000Z", updatedAt: "2026-04-06T09:30:00.000Z", }, ]; await writeState(state); const queuedTask = await queueMasterAgentTask({ projectId: project.id, requestMessageId: "msg-stale-policy", requestText: "继续推进这个线程", executionPrompt: "请继续推进这个线程", requestedBy: "Boss 超级管理员", requestedByAccount: "krisolo", deviceId: "mac-studio", taskType: "conversation_reply", targetProjectId: project.id, targetThreadId: project.threadMeta.threadId, targetThreadDisplayName: project.threadMeta.threadDisplayName, targetCodexThreadRef: project.threadMeta.codexThreadRef, targetCodexFolderRef: project.threadMeta.codexFolderRef, }); const claimed = await claimNextMasterAgentTask("mac-studio"); assert.equal(claimed?.taskId, queuedTask.taskId); const nextState = await readState(); const policy = nextState.projectExecutionPolicies.find((item) => item.folderKey === folderKey); assert.ok(policy, "expected stale scoped policy to remain in state"); assert.equal(policy?.conflictState, "none"); assert.equal(policy?.activeCliExecution, true); assert.equal(policy?.recentExternalActivityAt, undefined); }); test("claimNextMasterAgentTask reclaims stale running conversation replies for the same device", async () => { await setup(); const project = await getCliProject(); const task = await queueMasterAgentTask({ projectId: project.id, requestMessageId: "msg-stale-running-reply", requestText: "请继续推进当前线程", executionPrompt: "请继续推进当前线程", requestedBy: "Boss 超级管理员", requestedByAccount: "krisolo", deviceId: "mac-studio", taskType: "conversation_reply", targetProjectId: project.id, targetThreadId: project.threadMeta.threadId, targetThreadDisplayName: project.threadMeta.threadDisplayName, targetCodexThreadRef: project.threadMeta.codexThreadRef, targetCodexFolderRef: project.threadMeta.codexFolderRef, }); const initialClaim = await claimNextMasterAgentTask("mac-studio"); assert.equal(initialClaim?.taskId, task.taskId); const state = await readState(); const runningTask = state.masterAgentTasks.find((item) => item.taskId === task.taskId); assert.equal(runningTask?.status, "running"); runningTask!.claimedAt = "2026-04-01T00:00:00.000Z"; await writeState(state); const reclaimed = await claimNextMasterAgentTask("mac-studio"); assert.equal(reclaimed?.taskId, task.taskId); const nextState = await readState(); const reclaimedTask = nextState.masterAgentTasks.find((item) => item.taskId === task.taskId); assert.equal(reclaimedTask?.status, "running"); assert.notEqual(reclaimedTask?.claimedAt, "2026-04-01T00:00:00.000Z"); }); test("claimNextMasterAgentTask does not automatically reclaim stale running dispatch_execution tasks", async () => { await setup(); const project = await getCliProject(); const task = await queueMasterAgentTask({ projectId: project.id, requestMessageId: "msg-stale-running-dispatch", requestText: "请执行修复任务", executionPrompt: "请执行修复任务", requestedBy: "Boss 超级管理员", requestedByAccount: "krisolo", deviceId: "mac-studio", taskType: "dispatch_execution", dispatchExecutionId: "dispatch-exec-stale-1", targetProjectId: project.id, targetThreadId: project.threadMeta.threadId, targetThreadDisplayName: project.threadMeta.threadDisplayName, targetCodexThreadRef: project.threadMeta.codexThreadRef, targetCodexFolderRef: project.threadMeta.codexFolderRef, }); const initialClaim = await claimNextMasterAgentTask("mac-studio"); assert.equal(initialClaim?.taskId, task.taskId); const state = await readState(); const runningTask = state.masterAgentTasks.find((item) => item.taskId === task.taskId); assert.equal(runningTask?.status, "running"); runningTask!.claimedAt = "2026-04-01T00:00:00.000Z"; await writeState(state); const reclaimed = await claimNextMasterAgentTask("mac-studio"); assert.equal(reclaimed, null); const nextState = await readState(); const unchangedTask = nextState.masterAgentTasks.find((item) => item.taskId === task.taskId); assert.equal(unchangedTask?.status, "running"); assert.equal(unchangedTask?.claimedAt, "2026-04-01T00:00:00.000Z"); });