218 lines
7.1 KiB
TypeScript
218 lines
7.1 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 getParticipantsRoute: (typeof import("../src/app/api/v1/projects/[projectId]/participants/route"))["GET"];
|
|
let updateParticipantsRoute: (typeof import("../src/app/api/v1/projects/[projectId]/participants/route"))["POST"];
|
|
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
|
let createProjectGroupChat: (typeof import("../src/lib/boss-data"))["createProjectGroupChat"];
|
|
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-group-repair-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
|
|
const [participantsModule, data, auth] = await Promise.all([
|
|
import("../src/app/api/v1/projects/[projectId]/participants/route.ts"),
|
|
import("../src/lib/boss-data.ts"),
|
|
import("../src/lib/boss-auth.ts"),
|
|
]);
|
|
|
|
getParticipantsRoute = participantsModule.GET;
|
|
updateParticipantsRoute = participantsModule.POST;
|
|
createAuthSession = data.createAuthSession;
|
|
createProjectGroupChat = data.createProjectGroupChat;
|
|
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 });
|
|
}
|
|
});
|
|
|
|
async function createAuthedRequest(url: string, method: "GET" | "POST", body?: unknown) {
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
return new NextRequest(url, {
|
|
method,
|
|
headers: {
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
...(body ? { "content-type": "application/json" } : {}),
|
|
},
|
|
body: body ? JSON.stringify(body) : undefined,
|
|
});
|
|
}
|
|
|
|
async function ensureTwoSingleThreadProjects() {
|
|
const state = await readState();
|
|
const singles = state.projects.filter((project) => project.id !== "master-agent" && !project.isGroup);
|
|
if (singles.length >= 2) {
|
|
return singles;
|
|
}
|
|
|
|
assert.ok(singles[0], "expected seeded single-thread project");
|
|
const seed = singles[0];
|
|
const clone = {
|
|
...seed,
|
|
id: "repair-thread-clone",
|
|
name: "Repair Thread Clone",
|
|
deviceIds: ["mac-studio"],
|
|
threadMeta: {
|
|
...seed.threadMeta,
|
|
projectId: "repair-thread-clone",
|
|
threadId: "repair-thread-clone",
|
|
threadDisplayName: "维修回归线程",
|
|
folderName: "repair-folder",
|
|
codexThreadRef: "repair-thread-clone",
|
|
codexFolderRef: "repair-folder",
|
|
},
|
|
};
|
|
|
|
await writeState({
|
|
...state,
|
|
projects: [...state.projects, clone],
|
|
});
|
|
const nextState = await readState();
|
|
return nextState.projects.filter((project) => project.id !== "master-agent" && !project.isGroup);
|
|
}
|
|
|
|
test("GET /api/v1/projects/[projectId]/participants marks dirty groups as repair-required", async () => {
|
|
await setup();
|
|
const singles = await ensureTwoSingleThreadProjects();
|
|
const groupProject = await createProjectGroupChat({
|
|
sourceProjectId: singles[0].id,
|
|
memberProjectIds: [singles[1].id],
|
|
createdBy: "17600003315",
|
|
});
|
|
|
|
const state = await readState();
|
|
await writeState({
|
|
...state,
|
|
projects: state.projects.map((project) =>
|
|
project.id === groupProject.id
|
|
? {
|
|
...project,
|
|
groupMembers: [
|
|
{
|
|
projectId: "master-agent",
|
|
deviceId: "mac-studio",
|
|
threadId: "master-agent-thread",
|
|
threadDisplayName: "主 Agent 汇总",
|
|
folderName: "主控线程",
|
|
},
|
|
],
|
|
}
|
|
: project,
|
|
),
|
|
});
|
|
|
|
const response = await getParticipantsRoute(
|
|
await createAuthedRequest(
|
|
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/participants`,
|
|
"GET",
|
|
),
|
|
{ params: Promise.resolve({ projectId: groupProject.id }) },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
|
|
const payload = (await response.json()) as {
|
|
ok: boolean;
|
|
repairRequired: boolean;
|
|
validParticipantCount: number;
|
|
invalidParticipantCount: number;
|
|
participants: Array<{ status: string; canOpenProject: boolean }>;
|
|
};
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.repairRequired, true);
|
|
assert.equal(payload.validParticipantCount, 0);
|
|
assert.equal(payload.invalidParticipantCount, 1);
|
|
assert.equal(payload.participants[0]?.status, "invalid_target");
|
|
assert.equal(payload.participants[0]?.canOpenProject, true);
|
|
});
|
|
|
|
test("POST /api/v1/projects/[projectId]/participants replaces dirty members with real thread participants", async () => {
|
|
await setup();
|
|
const singles = await ensureTwoSingleThreadProjects();
|
|
const groupProject = await createProjectGroupChat({
|
|
sourceProjectId: singles[0].id,
|
|
memberProjectIds: [singles[1].id],
|
|
createdBy: "17600003315",
|
|
});
|
|
|
|
const state = await readState();
|
|
await writeState({
|
|
...state,
|
|
projects: state.projects.map((project) =>
|
|
project.id === groupProject.id
|
|
? {
|
|
...project,
|
|
groupMembers: [
|
|
{
|
|
projectId: "master-agent",
|
|
deviceId: "mac-studio",
|
|
threadId: "master-agent-thread",
|
|
threadDisplayName: "主 Agent 汇总",
|
|
folderName: "主控线程",
|
|
},
|
|
],
|
|
}
|
|
: project,
|
|
),
|
|
});
|
|
|
|
const targetIds = singles.slice(0, 2).map((project) => project.id);
|
|
const response = await updateParticipantsRoute(
|
|
await createAuthedRequest(
|
|
`http://127.0.0.1:3000/api/v1/projects/${groupProject.id}/participants`,
|
|
"POST",
|
|
{ memberProjectIds: targetIds },
|
|
),
|
|
{ params: Promise.resolve({ projectId: groupProject.id }) },
|
|
);
|
|
assert.equal(response.status, 200);
|
|
|
|
const payload = (await response.json()) as {
|
|
ok: boolean;
|
|
repairRequired: boolean;
|
|
participants: Array<{ projectId: string; status: string }>;
|
|
};
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.repairRequired, false);
|
|
assert.deepEqual(
|
|
payload.participants.map((participant) => participant.projectId).sort(),
|
|
[...targetIds].sort(),
|
|
);
|
|
assert.ok(payload.participants.every((participant) => participant.status === "active"));
|
|
|
|
const nextState = await readState();
|
|
const nextGroup = nextState.projects.find((project) => project.id === groupProject.id);
|
|
assert.ok(nextGroup, "expected repaired group to remain present");
|
|
assert.deepEqual(
|
|
nextGroup?.groupMembers.map((member) => member.projectId).sort(),
|
|
[...targetIds].sort(),
|
|
);
|
|
const repairNotice = nextGroup?.messages.find(
|
|
(message) =>
|
|
message.kind === "system_notice" &&
|
|
message.body.includes("已更新群成员"),
|
|
);
|
|
assert.ok(repairNotice, "expected a group repair system notice");
|
|
});
|