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 postRoute: (typeof import("../src/app/api/v1/projects/[projectId]/thread-metadata/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 = ""; async function setup() { if (runtimeRoot) return; runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-thread-metadata-")); process.env.BOSS_RUNTIME_ROOT = runtimeRoot; process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json"); const [route, data, auth] = await Promise.all([ import("../src/app/api/v1/projects/[projectId]/thread-metadata/route.ts"), import("../src/lib/boss-data.ts"), import("../src/lib/boss-auth.ts"), ]); postRoute = route.POST; createAuthSession = data.createAuthSession; readState = data.readState; writeState = data.writeState; 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 rm(runtimeRoot, { recursive: true, force: true }); }); async function createAuthedRequest(projectId: string, body: unknown) { 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}/thread-metadata`, { method: "POST", headers: { "content-type": "application/json", cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`, }, body: JSON.stringify(body), }); } function buildThreadProject() { return { id: "metadata-project", name: "元数据同步线程", pinned: false, systemPinned: false, deviceIds: ["mac-studio"], preview: "", updatedAt: "2026-06-03T13:30:00+08:00", lastMessageAt: "2026-06-03T13:30:00+08:00", isGroup: false, threadMeta: { projectId: "metadata-project", threadId: "metadata-thread", threadDisplayName: "元数据同步线程", folderName: "boss", codexFolderRef: "/Users/kris/code/boss", codexThreadRef: "codex-metadata-thread", updatedAt: "2026-06-03T13:30:00+08:00", }, groupMembers: [], createdByAgent: false, collaborationMode: "development" as const, approvalState: "not_required" as const, unreadCount: 0, riskLevel: "low" as const, messages: [], goals: [], versions: [], }; } test("POST /thread-metadata queues a controlled Codex thread metadata update task", async () => { const state = await readState(); const project = buildThreadProject(); state.projects = [ ...state.projects.filter((item) => item.id === "master-agent"), project, ]; await writeState(state); const response = await postRoute( await createAuthedRequest(project.id, { gitInfo: { sha: "0186ef7", branch: "codex/wechat-native-ui-rollback", originUrl: "https://git.hyzq.site/krisolo/boss.git", }, reason: "同步当前 Gitea 分支信息。", }), { params: Promise.resolve({ projectId: project.id }) }, ); const payload = await response.json(); assert.equal(response.status, 200); assert.equal(payload.ok, true); assert.equal(payload.task.intentCategory, "thread_metadata_sync"); assert.deepEqual(payload.task.threadMetadataGitInfo, { sha: "0186ef7", branch: "codex/wechat-native-ui-rollback", originUrl: "https://git.hyzq.site/krisolo/boss.git", }); assert.equal(payload.task.threadMetadataReason, "同步当前 Gitea 分支信息。"); assert.equal(payload.task.targetProjectId, project.id); assert.equal(payload.task.targetCodexThreadRef, "codex-metadata-thread"); const persisted = (await readState()).masterAgentTasks.find( (task) => task.taskId === payload.task.taskId, ); assert.equal(persisted?.status, "queued"); assert.equal(persisted?.intentCategory, "thread_metadata_sync"); assert.deepEqual(persisted?.threadMetadataGitInfo, { sha: "0186ef7", branch: "codex/wechat-native-ui-rollback", originUrl: "https://git.hyzq.site/krisolo/boss.git", }); }); test("POST /thread-metadata rejects empty git metadata patches", async () => { const state = await readState(); const project = buildThreadProject(); state.projects = [ ...state.projects.filter((item) => item.id === "master-agent"), project, ]; await writeState(state); const response = await postRoute( await createAuthedRequest(project.id, { gitInfo: {} }), { params: Promise.resolve({ projectId: project.id }) }, ); const payload = await response.json(); assert.equal(response.status, 400); assert.equal(payload.ok, false); assert.equal(payload.message, "THREAD_METADATA_GIT_INFO_REQUIRED"); } );