fix: keep imported thread candidates distinct
This commit is contained in:
@@ -2021,7 +2021,7 @@ function buildDeviceImportCandidateId(input: {
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join("-");
|
||||
return `import-${slugify(signature)}`;
|
||||
return `import-${slugifyWithHash(signature)}`;
|
||||
}
|
||||
|
||||
function normalizeDeviceImportCandidate(
|
||||
@@ -2316,6 +2316,12 @@ function slugify(value: string) {
|
||||
.slice(0, 48) || `item-${randomBytes(2).toString("hex")}`;
|
||||
}
|
||||
|
||||
function slugifyWithHash(value: string) {
|
||||
const base = slugify(value).slice(0, 40);
|
||||
const hash = createHash("sha1").update(value).digest("hex").slice(0, 8);
|
||||
return `${base || "item"}-${hash}`;
|
||||
}
|
||||
|
||||
const aiRolePriority: Record<AiAccountRole, number> = {
|
||||
primary: 0,
|
||||
backup: 1,
|
||||
@@ -7144,8 +7150,8 @@ function parseDeviceImportResolutionReply(
|
||||
function buildImportedThreadProject(device: Device, candidate: DeviceImportCandidate) {
|
||||
const projectId =
|
||||
candidate.codexThreadRef?.trim() && candidate.codexFolderRef?.trim()
|
||||
? slugify(`${device.id}-${candidate.codexFolderRef}-${candidate.codexThreadRef}`)
|
||||
: slugify(`${device.id}-${candidate.folderName}-${candidate.threadId}`);
|
||||
? slugifyWithHash(`${device.id}-${candidate.codexFolderRef}-${candidate.codexThreadRef}`)
|
||||
: slugifyWithHash(`${device.id}-${candidate.folderName}-${candidate.threadId}`);
|
||||
const now = nowIso();
|
||||
return normalizeProject({
|
||||
id: projectId,
|
||||
|
||||
141
tests/device-import-candidate-id-regression.test.ts
Normal file
141
tests/device-import-candidate-id-regression.test.ts
Normal file
@@ -0,0 +1,141 @@
|
||||
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: "17600003315",
|
||||
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: "17600003315",
|
||||
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: "17600003315",
|
||||
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"],
|
||||
);
|
||||
});
|
||||
Reference in New Issue
Block a user