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"; import { NextRequest } from "next/server"; let runtimeRoot = ""; let postMessageRoute: (typeof import("../src/app/api/v1/projects/[projectId]/messages/route"))["POST"]; let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"]; let readState: (typeof import("../src/lib/boss-data"))["readState"]; let writeState: (typeof import("../src/lib/boss-data"))["writeState"]; let AUTH_SESSION_COOKIE = ""; let baseState: Awaited>; async function setup() { if (runtimeRoot) return; runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-thread-preflight-")); process.env.BOSS_RUNTIME_ROOT = runtimeRoot; process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); const [messageModule, data, auth] = await Promise.all([ import("../src/app/api/v1/projects/[projectId]/messages/route.ts"), import("../src/lib/boss-data.ts"), import("../src/lib/boss-auth.ts"), ]); postMessageRoute = messageModule.POST; createAuthSession = data.createAuthSession; readState = data.readState; writeState = data.writeState; baseState = structuredClone(await readState()); AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE; } test.after(async () => { if (runtimeRoot) { await rm(runtimeRoot, { recursive: true, force: true }); } }); test.beforeEach(async () => { await setup(); await writeState(structuredClone(baseState)); }); function buildSingleThreadProject(projectId: string) { return { id: projectId, name: "测试线程", pinned: false, systemPinned: false, deviceIds: ["mac-studio"], preview: "测试线程等待继续处理。", updatedAt: "2026-04-04T11:30:00+08:00", lastMessageAt: "2026-04-04T11:30:00+08:00", isGroup: false, threadMeta: { projectId, threadId: "thread-preflight", threadDisplayName: "测试线程", folderName: "测试项目", activityIconCount: 0, updatedAt: "2026-04-04T11:30:00+08:00", codexThreadRef: "thread-preflight", codexFolderRef: "preflight-project", }, groupMembers: [], createdByAgent: true, collaborationMode: "development" as const, approvalState: "not_required" as const, unreadCount: 0, riskLevel: "low" as const, messages: [], goals: [], versions: [], }; } async function createAuthedRequest(projectId: string, body: { body: string }) { const session = await createAuthSession({ account: "krisolo", role: "highest_admin", displayName: "Boss 超级管理员", loginMethod: "password", }); return new NextRequest(`http://127.0.0.1:3000/api/v1/projects/${projectId}/messages`, { method: "POST", headers: { "content-type": "application/json", cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`, }, body: JSON.stringify(body), }); } test("single-thread message rejects projects without a real codex thread binding", async () => { await setup(); const state = await readState(); const singleProject = buildSingleThreadProject("preflight-thread"); await writeState({ ...state, projects: state.projects.concat(singleProject), }); const nextState = await readState(); await writeState({ ...nextState, projects: nextState.projects.map((project) => project.id === singleProject.id ? { ...project, threadMeta: { ...project.threadMeta, codexThreadRef: undefined, }, } : project, ), }); const response = await postMessageRoute( await createAuthedRequest(singleProject.id, { body: "请继续处理这个线程" }), { params: Promise.resolve({ projectId: singleProject.id }) }, ); assert.equal(response.status, 400); const payload = (await response.json()) as { ok: boolean; code: string; message: string }; assert.equal(payload.ok, false); assert.equal(payload.code, "THREAD_BINDING_REQUIRED"); assert.equal(payload.message, "当前线程还没有绑定真实 Codex 线程,请先重新导入该线程后再试。"); const finalState = await readState(); const queuedTask = finalState.masterAgentTasks.find( (task) => task.projectId === singleProject.id && task.taskType === "conversation_reply", ); assert.equal(queuedTask, undefined); });