142 lines
4.9 KiB
TypeScript
142 lines
4.9 KiB
TypeScript
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 createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
|
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
|
let deviceHeartbeatRoute: (typeof import("../src/app/api/device-heartbeat/route"))["POST"];
|
|
let AUTH_SESSION_COOKIE = "";
|
|
|
|
async function setup() {
|
|
if (runtimeRoot) return;
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-device-import-candidate-id-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
|
|
const [heartbeatModule, data, auth] = await Promise.all([
|
|
import("../src/app/api/device-heartbeat/route.ts"),
|
|
import("../src/lib/boss-data.ts"),
|
|
import("../src/lib/boss-auth.ts"),
|
|
]);
|
|
|
|
deviceHeartbeatRoute = heartbeatModule.POST;
|
|
createAuthSession = data.createAuthSession;
|
|
readState = data.readState;
|
|
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
|
|
}
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) {
|
|
await rm(runtimeRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
async function createAuthedSessionCookie() {
|
|
const session = await createAuthSession({
|
|
account: "krisolo",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
return `${AUTH_SESSION_COOKIE}=${session.sessionToken}`;
|
|
}
|
|
|
|
test("auto-sync import keeps long-prefix project candidates distinct", async () => {
|
|
await setup();
|
|
const cookie = await createAuthedSessionCookie();
|
|
const state = await readState();
|
|
state.devices = [
|
|
{
|
|
id: "mac-studio",
|
|
name: "Mac Studio",
|
|
avatar: "M",
|
|
account: "krisolo",
|
|
source: "production",
|
|
status: "online",
|
|
projects: [],
|
|
quota5h: 68,
|
|
quota7d: 81,
|
|
lastSeenAt: "2026-04-03T10:00:00.000Z",
|
|
endpoint: "mac://kris.local",
|
|
token: "boss-mac-studio-token",
|
|
note: "本机 Codex 主节点",
|
|
},
|
|
];
|
|
|
|
const response = await deviceHeartbeatRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/device-heartbeat", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie,
|
|
},
|
|
body: JSON.stringify({
|
|
deviceId: "mac-studio",
|
|
token: "boss-mac-studio-token",
|
|
name: "Mac Studio",
|
|
avatar: "M",
|
|
account: "krisolo",
|
|
status: "online",
|
|
quota5h: 68,
|
|
quota7d: 81,
|
|
projects: [],
|
|
endpoint: "mac://kris.local",
|
|
projectCandidates: [
|
|
{
|
|
folderName: "AItoukui",
|
|
folderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode/AItoukui",
|
|
threadId: "019d4efb-3485-7b21-86dc-b70d3f4adf68",
|
|
threadDisplayName: "拉取代码并验证交接文档和本地启动状态",
|
|
codexFolderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode/AItoukui",
|
|
codexThreadRef: "019d4efb-3485-7b21-86dc-b70d3f4adf68",
|
|
lastActiveAt: "2026-04-02T20:50:36.000Z",
|
|
suggestedImport: true,
|
|
},
|
|
{
|
|
folderName: "500Gcode",
|
|
folderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode",
|
|
threadId: "019d4e19-1d27-77d3-bc54-2a1c80c7431b",
|
|
threadDisplayName: "本机运行3.5B模型并搭建Web演示",
|
|
codexFolderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode",
|
|
codexThreadRef: "019d4e19-1d27-77d3-bc54-2a1c80c7431b",
|
|
lastActiveAt: "2026-04-02T20:50:36.000Z",
|
|
suggestedImport: true,
|
|
},
|
|
{
|
|
folderName: "figma",
|
|
folderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode/figma",
|
|
threadId: "019d4e1d-03b8-7610-b427-c4ffd234aed4",
|
|
threadDisplayName: "配置Figma账号并启用UI制作Skill",
|
|
codexFolderRef: "/Volumes/Macintosh HD/Users/kris/500Gcode/figma",
|
|
codexThreadRef: "019d4e1d-03b8-7610-b427-c4ffd234aed4",
|
|
lastActiveAt: "2026-04-02T20:50:36.000Z",
|
|
suggestedImport: true,
|
|
},
|
|
],
|
|
}),
|
|
}),
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const payload = (await response.json()) as {
|
|
importDraft?: { candidates: Array<{ candidateId: string }>; appliedProjectNames: string[] };
|
|
};
|
|
|
|
const candidateIds = payload.importDraft?.candidates.map((candidate) => candidate.candidateId) ?? [];
|
|
assert.equal(new Set(candidateIds).size, 3);
|
|
|
|
const nextState = await readState();
|
|
const importedProjects = nextState.projects.filter((project) =>
|
|
["AItoukui", "500Gcode", "figma"].includes(project.threadMeta.folderName),
|
|
);
|
|
assert.equal(importedProjects.length, 3);
|
|
assert.deepEqual(
|
|
importedProjects.map((project) => project.threadMeta.folderName).sort(),
|
|
["500Gcode", "AItoukui", "figma"],
|
|
);
|
|
});
|