958 lines
33 KiB
TypeScript
958 lines
33 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, writeFile } from "node:fs/promises";
|
|
import { execFile as execFileCallback } from "node:child_process";
|
|
import { promisify } from "node:util";
|
|
import { NextRequest } from "next/server";
|
|
|
|
let runtimeRoot = "";
|
|
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
|
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
|
|
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
|
let getProjectAgentControls: (typeof import("../src/lib/boss-data"))["getProjectAgentControls"];
|
|
let getProjectDetailView: (typeof import("../src/lib/boss-projections"))["getProjectDetailView"];
|
|
let getProjectRoute: (typeof import("../src/app/api/v1/projects/[projectId]/route"))["GET"];
|
|
let getAgentControlsRoute: (typeof import("../src/app/api/v1/projects/[projectId]/agent-controls/route"))["GET"];
|
|
let postAgentControlsRoute: (typeof import("../src/app/api/v1/projects/[projectId]/agent-controls/route"))["POST"];
|
|
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
|
let AUTH_SESSION_COOKIE = "";
|
|
const execFile = promisify(execFileCallback);
|
|
|
|
async function setup() {
|
|
if (runtimeRoot) return;
|
|
|
|
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-controls-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
|
|
const [data, projections, projectRouteModule, agentControlsRouteModule, auth] = await Promise.all([
|
|
import("../src/lib/boss-data.ts"),
|
|
import("../src/lib/boss-projections.ts"),
|
|
import("../src/app/api/v1/projects/[projectId]/route.ts"),
|
|
import("../src/app/api/v1/projects/[projectId]/agent-controls/route.ts"),
|
|
import("../src/lib/boss-auth.ts"),
|
|
]);
|
|
|
|
readState = data.readState;
|
|
writeState = data.writeState;
|
|
updateProjectAgentControls = data.updateProjectAgentControls;
|
|
getProjectAgentControls = data.getProjectAgentControls;
|
|
getProjectDetailView = projections.getProjectDetailView;
|
|
getProjectRoute = projectRouteModule.GET;
|
|
getAgentControlsRoute = agentControlsRouteModule.GET;
|
|
postAgentControlsRoute = agentControlsRouteModule.POST;
|
|
createAuthSession = data.createAuthSession;
|
|
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
|
|
}
|
|
|
|
async function resetMasterAgentControls() {
|
|
await setup();
|
|
const state = await readState();
|
|
const project = state.projects.find((item) => item.id === "master-agent");
|
|
assert.ok(project, "expected seeded master-agent project");
|
|
delete project.agentControls;
|
|
state.userProjectAgentControls = state.userProjectAgentControls.filter(
|
|
(item) => item.projectId !== "master-agent",
|
|
);
|
|
await writeState(state);
|
|
}
|
|
|
|
test.beforeEach(async () => {
|
|
await resetMasterAgentControls();
|
|
});
|
|
|
|
test.after(async () => {
|
|
if (runtimeRoot) {
|
|
await rm(runtimeRoot, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("master-agent 会话可保存并读取模型与推理强度覆盖", async () => {
|
|
await setup();
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
});
|
|
|
|
const controls = await getProjectAgentControls("master-agent");
|
|
assert.equal(controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(controls?.reasoningEffortOverride, "high");
|
|
|
|
const state = await readState();
|
|
const project = state.projects.find((item) => item.id === "master-agent");
|
|
assert.equal(project?.agentControls?.modelOverride, "gpt-5.4");
|
|
assert.equal(project?.agentControls?.reasoningEffortOverride, "high");
|
|
|
|
const detail = getProjectDetailView(state, "master-agent");
|
|
assert.equal(detail?.agentControls?.modelOverride, "gpt-5.4");
|
|
assert.equal(detail?.agentControls?.reasoningEffortOverride, "high");
|
|
});
|
|
|
|
test("master-agent 对话控制路由可读写并回显到项目详情", async () => {
|
|
await setup();
|
|
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-agent-controls-"));
|
|
const scriptPath = path.join(tempDir, "claw-runtime.mjs");
|
|
await writeFile(scriptPath, "console.log('ok');\n", "utf8");
|
|
const previousEnv = {
|
|
BOSS_CLAW_ENABLED: process.env.BOSS_CLAW_ENABLED,
|
|
BOSS_CLAW_COMMAND: process.env.BOSS_CLAW_COMMAND,
|
|
BOSS_CLAW_ARGS: process.env.BOSS_CLAW_ARGS,
|
|
BOSS_CLAW_WORKDIR: process.env.BOSS_CLAW_WORKDIR,
|
|
};
|
|
process.env.BOSS_CLAW_ENABLED = "true";
|
|
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
|
process.env.BOSS_CLAW_ARGS = scriptPath;
|
|
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
|
|
|
try {
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const headers = {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
};
|
|
|
|
const postResponse = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "medium",
|
|
backendOverride: "claw-runtime",
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
assert.equal(postResponse.status, 200);
|
|
|
|
const postPayload = (await postResponse.json()) as {
|
|
ok: boolean;
|
|
controls: {
|
|
modelOverride?: string;
|
|
reasoningEffortOverride?: string;
|
|
backendOverride?: string;
|
|
updatedAt: string;
|
|
} | null;
|
|
};
|
|
assert.equal(postPayload.ok, true);
|
|
assert.equal(postPayload.controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(postPayload.controls?.reasoningEffortOverride, "medium");
|
|
assert.equal(postPayload.controls?.backendOverride, "claw-runtime");
|
|
|
|
const getResponse = await getAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "GET",
|
|
headers,
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
assert.equal(getResponse.status, 200);
|
|
|
|
const getPayload = (await getResponse.json()) as {
|
|
ok: boolean;
|
|
controls: {
|
|
modelOverride?: string;
|
|
reasoningEffortOverride?: string;
|
|
backendOverride?: string;
|
|
updatedAt: string;
|
|
} | null;
|
|
};
|
|
assert.equal(getPayload.ok, true);
|
|
assert.equal(getPayload.controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(getPayload.controls?.reasoningEffortOverride, "medium");
|
|
assert.equal(getPayload.controls?.backendOverride, "claw-runtime");
|
|
|
|
const projectResponse = await getProjectRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent", {
|
|
method: "GET",
|
|
headers,
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
assert.equal(projectResponse.status, 200);
|
|
|
|
const projectPayload = (await projectResponse.json()) as {
|
|
ok: boolean;
|
|
agentControls: {
|
|
modelOverride?: string;
|
|
reasoningEffortOverride?: string;
|
|
backendOverride?: string;
|
|
updatedAt: string;
|
|
} | null;
|
|
};
|
|
assert.equal(projectPayload.ok, true);
|
|
assert.equal(projectPayload.agentControls?.modelOverride, "gpt-5.4");
|
|
assert.equal(projectPayload.agentControls?.reasoningEffortOverride, "medium");
|
|
assert.equal(projectPayload.agentControls?.backendOverride, "claw-runtime");
|
|
} finally {
|
|
if (previousEnv.BOSS_CLAW_ENABLED === undefined) delete process.env.BOSS_CLAW_ENABLED;
|
|
else process.env.BOSS_CLAW_ENABLED = previousEnv.BOSS_CLAW_ENABLED;
|
|
if (previousEnv.BOSS_CLAW_COMMAND === undefined) delete process.env.BOSS_CLAW_COMMAND;
|
|
else process.env.BOSS_CLAW_COMMAND = previousEnv.BOSS_CLAW_COMMAND;
|
|
if (previousEnv.BOSS_CLAW_ARGS === undefined) delete process.env.BOSS_CLAW_ARGS;
|
|
else process.env.BOSS_CLAW_ARGS = previousEnv.BOSS_CLAW_ARGS;
|
|
if (previousEnv.BOSS_CLAW_WORKDIR === undefined) delete process.env.BOSS_CLAW_WORKDIR;
|
|
else process.env.BOSS_CLAW_WORKDIR = previousEnv.BOSS_CLAW_WORKDIR;
|
|
await rm(tempDir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("master-agent 对话控制按当前账号隔离,不会串到其他用户", async () => {
|
|
await setup();
|
|
|
|
const adminSession = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
const memberSession = await createAuthSession({
|
|
account: "18800001111",
|
|
role: "member",
|
|
displayName: "普通成员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const adminHeaders = {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${adminSession.sessionToken}`,
|
|
};
|
|
const memberHeaders = {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${memberSession.sessionToken}`,
|
|
};
|
|
|
|
await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers: adminHeaders,
|
|
body: JSON.stringify({
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
const adminGet = await getAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "GET",
|
|
headers: adminHeaders,
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
const memberGet = await getAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "GET",
|
|
headers: memberHeaders,
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
const adminPayload = (await adminGet.json()) as { controls: { modelOverride?: string; reasoningEffortOverride?: string } | null };
|
|
const memberPayload = (await memberGet.json()) as { controls: { modelOverride?: string; reasoningEffortOverride?: string } | null };
|
|
|
|
assert.equal(adminPayload.controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(adminPayload.controls?.reasoningEffortOverride, "high");
|
|
assert.equal(memberPayload.controls, null);
|
|
});
|
|
|
|
test("master-agent 对话控制路由单字段更新不会清掉另一字段", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const headers = {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
};
|
|
|
|
await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
const response = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({
|
|
reasoningEffortOverride: "low",
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const payload = (await response.json()) as {
|
|
ok: boolean;
|
|
controls: {
|
|
modelOverride?: string;
|
|
reasoningEffortOverride?: string;
|
|
} | null;
|
|
};
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(payload.controls?.reasoningEffortOverride, "low");
|
|
});
|
|
|
|
test("master-agent 对话控制 POST 清空后仍稳定回传 controls null", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const headers = {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
};
|
|
|
|
await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
const clearResponse = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({
|
|
modelOverride: null,
|
|
reasoningEffortOverride: null,
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
assert.equal(clearResponse.status, 200);
|
|
const clearPayload = (await clearResponse.json()) as {
|
|
ok: boolean;
|
|
controls: unknown;
|
|
};
|
|
assert.equal(clearPayload.ok, true);
|
|
assert.equal(Object.prototype.hasOwnProperty.call(clearPayload, "controls"), true);
|
|
assert.equal(clearPayload.controls, null);
|
|
});
|
|
|
|
test("非 master-agent 项目详情不应回传 agentControls 字段", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const response = await getProjectRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/boss-console", {
|
|
method: "GET",
|
|
headers: {
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
},
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "boss-console" }) },
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
const payload = (await response.json()) as Record<string, unknown>;
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(Object.prototype.hasOwnProperty.call(payload, "agentControls"), false);
|
|
});
|
|
|
|
test("master-agent 对话控制 POST 允许当前用户修改自己的 master-agent 会话配置", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "viewer-0001",
|
|
role: "member",
|
|
displayName: "普通成员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const response = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
},
|
|
body: JSON.stringify({
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "low",
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
assert.equal(response.status, 200);
|
|
|
|
const payload = (await response.json()) as {
|
|
ok: boolean;
|
|
controls: { modelOverride?: string; reasoningEffortOverride?: string } | null;
|
|
};
|
|
assert.equal(payload.ok, true);
|
|
assert.equal(payload.controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(payload.controls?.reasoningEffortOverride, "low");
|
|
|
|
const controls = await getProjectAgentControls("master-agent", "viewer-0001");
|
|
assert.equal(controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(controls?.reasoningEffortOverride, "low");
|
|
});
|
|
|
|
test("master-agent 对话控制 POST 会稳定拒绝非法 modelOverride", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const response = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
},
|
|
body: JSON.stringify({
|
|
modelOverride: 123,
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
assert.equal(response.status, 400);
|
|
|
|
const payload = (await response.json()) as { ok: boolean; message: string };
|
|
assert.equal(payload.ok, false);
|
|
assert.equal(payload.message, "INVALID_MODEL_OVERRIDE");
|
|
|
|
const controls = await getProjectAgentControls("master-agent");
|
|
assert.equal(controls, null);
|
|
});
|
|
|
|
test("master-agent 对话控制 POST 会稳定拒绝 malformed JSON 和空对象", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const headers = {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
};
|
|
|
|
const beforeState = await readState();
|
|
const beforeProject = beforeState.projects.find((item) => item.id === "master-agent");
|
|
assert.ok(beforeProject, "expected seeded master-agent project");
|
|
const beforeUpdatedAt = beforeProject.updatedAt;
|
|
const beforeThreadMetaUpdatedAt = beforeProject.threadMeta.updatedAt;
|
|
|
|
const malformedResponse = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: "{",
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
assert.equal(malformedResponse.status, 400);
|
|
assert.equal((await malformedResponse.json()).message, "INVALID_JSON_PAYLOAD");
|
|
|
|
const emptyObjectResponse = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify({}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
assert.equal(emptyObjectResponse.status, 400);
|
|
assert.equal((await emptyObjectResponse.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
|
|
|
|
const nullResponse = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: "null",
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
assert.equal(nullResponse.status, 400);
|
|
assert.equal((await nullResponse.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
|
|
|
|
const arrayResponse = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: "[]",
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
assert.equal(arrayResponse.status, 400);
|
|
assert.equal((await arrayResponse.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
|
|
|
|
const primitiveResponse = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers,
|
|
body: "1",
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
assert.equal(primitiveResponse.status, 400);
|
|
assert.equal((await primitiveResponse.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
|
|
|
|
const controls = await getProjectAgentControls("master-agent");
|
|
assert.equal(controls, null);
|
|
|
|
const state = await readState();
|
|
const project = state.projects.find((item) => item.id === "master-agent");
|
|
assert.equal(project?.agentControls, undefined);
|
|
assert.equal(project?.updatedAt, beforeUpdatedAt);
|
|
assert.equal(project?.threadMeta.updatedAt, beforeThreadMetaUpdatedAt);
|
|
});
|
|
|
|
test("master-agent 对话控制 helper 会安全忽略非法 modelOverride 输入", async () => {
|
|
await setup();
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
});
|
|
|
|
const beforeState = await readState();
|
|
const beforeProject = beforeState.projects.find((item) => item.id === "master-agent");
|
|
assert.ok(beforeProject, "expected seeded master-agent project");
|
|
const beforeUpdatedAt = beforeProject.updatedAt;
|
|
const beforeThreadMetaUpdatedAt = beforeProject.threadMeta.updatedAt;
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
updateProjectAgentControls("master-agent", {
|
|
modelOverride: { bad: true } as never,
|
|
reasoningEffortOverride: "medium",
|
|
}),
|
|
/INVALID_MODEL_OVERRIDE/,
|
|
);
|
|
|
|
const controls = await getProjectAgentControls("master-agent");
|
|
assert.equal(controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(controls?.reasoningEffortOverride, "high");
|
|
|
|
const afterState = await readState();
|
|
const afterProject = afterState.projects.find((item) => item.id === "master-agent");
|
|
assert.equal(afterProject?.updatedAt, beforeUpdatedAt);
|
|
assert.equal(afterProject?.threadMeta.updatedAt, beforeThreadMetaUpdatedAt);
|
|
});
|
|
|
|
test("master-agent 对话控制 helper 重复提交或重复清空不应刷新更新时间", async () => {
|
|
await setup();
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
});
|
|
|
|
const beforeRepeatState = await readState();
|
|
const beforeRepeatProject = beforeRepeatState.projects.find((item) => item.id === "master-agent");
|
|
assert.ok(beforeRepeatProject, "expected seeded master-agent project");
|
|
const beforeRepeatUpdatedAt = beforeRepeatProject.updatedAt;
|
|
const beforeRepeatThreadMetaUpdatedAt = beforeRepeatProject.threadMeta.updatedAt;
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
});
|
|
|
|
const afterRepeatState = await readState();
|
|
const afterRepeatProject = afterRepeatState.projects.find((item) => item.id === "master-agent");
|
|
assert.equal(afterRepeatProject?.updatedAt, beforeRepeatUpdatedAt);
|
|
assert.equal(afterRepeatProject?.threadMeta.updatedAt, beforeRepeatThreadMetaUpdatedAt);
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: undefined,
|
|
reasoningEffortOverride: undefined,
|
|
});
|
|
|
|
const afterClearState = await readState();
|
|
const afterClearProject = afterClearState.projects.find((item) => item.id === "master-agent");
|
|
const clearedUpdatedAt = afterClearProject?.updatedAt;
|
|
const clearedThreadMetaUpdatedAt = afterClearProject?.threadMeta.updatedAt;
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: undefined,
|
|
reasoningEffortOverride: undefined,
|
|
});
|
|
|
|
const afterRepeatClearState = await readState();
|
|
const afterRepeatClearProject = afterRepeatClearState.projects.find((item) => item.id === "master-agent");
|
|
assert.equal(afterRepeatClearProject?.updatedAt, clearedUpdatedAt);
|
|
assert.equal(afterRepeatClearProject?.threadMeta.updatedAt, clearedThreadMetaUpdatedAt);
|
|
assert.equal(await getProjectAgentControls("master-agent"), null);
|
|
});
|
|
|
|
test("master-agent 对话控制 helper 单字段更新不会清掉另一字段", async () => {
|
|
await setup();
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
});
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
reasoningEffortOverride: "low",
|
|
});
|
|
|
|
const controls = await getProjectAgentControls("master-agent");
|
|
assert.equal(controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(controls?.reasoningEffortOverride, "low");
|
|
});
|
|
|
|
test("master-agent 对话控制可清空为未覆盖状态", async () => {
|
|
await setup();
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
});
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: undefined,
|
|
reasoningEffortOverride: undefined,
|
|
});
|
|
|
|
const controls = await getProjectAgentControls("master-agent");
|
|
assert.equal(controls, null);
|
|
|
|
const state = await readState();
|
|
const project = state.projects.find((item) => item.id === "master-agent");
|
|
assert.equal(project?.agentControls, undefined);
|
|
|
|
const detail = getProjectDetailView(state, "master-agent");
|
|
assert.equal(detail?.agentControls, null);
|
|
});
|
|
|
|
test("GET /agent-controls returns 404 for missing project", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const response = await getAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/missing-project/agent-controls", {
|
|
method: "GET",
|
|
headers: {
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
},
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "missing-project" }) },
|
|
);
|
|
|
|
assert.equal(response.status, 404);
|
|
assert.equal((await response.json()).message, "PROJECT_NOT_FOUND");
|
|
});
|
|
|
|
test(
|
|
"GET /agent-controls returns 404 when master-agent record is missing from state",
|
|
{ concurrency: false },
|
|
async () => {
|
|
await setup();
|
|
|
|
const script = String.raw`
|
|
import { mkdtemp, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { NextRequest } from "next/server";
|
|
|
|
(async () => {
|
|
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-missing-"));
|
|
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
|
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
|
|
|
const dataModule = await import("./src/lib/boss-data.ts");
|
|
const authModule = await import("./src/lib/boss-auth.ts");
|
|
const routeModule = await import("./src/app/api/v1/projects/[projectId]/agent-controls/route.ts");
|
|
const data = dataModule.default ?? dataModule;
|
|
const auth = authModule.default ?? authModule;
|
|
const route = routeModule.default ?? routeModule;
|
|
|
|
const session = await data.createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const state = await data.readState();
|
|
state.projects = state.projects.filter((item) => item.id !== "master-agent");
|
|
await writeFile(process.env.BOSS_STATE_FILE!, JSON.stringify(state, null, 2), "utf8");
|
|
|
|
if (await data.hasPersistedProject("master-agent")) {
|
|
throw new Error("expected raw state to miss master-agent");
|
|
}
|
|
|
|
const response = await route.GET(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "GET",
|
|
headers: {
|
|
cookie: auth.AUTH_SESSION_COOKIE + "=" + session.sessionToken,
|
|
},
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
const text = await response.text();
|
|
if (response.status !== 404) {
|
|
throw new Error("expected 404, got " + response.status + ": " + text);
|
|
}
|
|
if (!text.includes("PROJECT_NOT_FOUND")) {
|
|
throw new Error("expected PROJECT_NOT_FOUND, got " + text);
|
|
}
|
|
console.log(text);
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
`;
|
|
|
|
const tsxBin = path.resolve("node_modules/.bin/tsx");
|
|
const { stdout } = await execFile(tsxBin, ["--eval", script], {
|
|
cwd: process.cwd(),
|
|
env: { ...process.env },
|
|
encoding: "utf8",
|
|
maxBuffer: 1024 * 1024,
|
|
});
|
|
|
|
assert.match(stdout, /PROJECT_NOT_FOUND/);
|
|
},
|
|
);
|
|
|
|
test("GET /agent-controls 在未显式设置 BOSS_STATE_FILE 时仍可正常读取 controls", async () => {
|
|
const runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-default-state-"));
|
|
const childEnv = { ...process.env };
|
|
delete childEnv.BOSS_STATE_FILE;
|
|
const script = String.raw`
|
|
import { NextRequest } from "next/server";
|
|
|
|
(async () => {
|
|
const runtimeRoot = process.env.BOSS_RUNTIME_ROOT;
|
|
if (!runtimeRoot) {
|
|
throw new Error("missing BOSS_RUNTIME_ROOT");
|
|
}
|
|
delete process.env.BOSS_STATE_FILE;
|
|
|
|
const dataModule = await import("./src/lib/boss-data.ts");
|
|
const authModule = await import("./src/lib/boss-auth.ts");
|
|
const routeModule = await import("./src/app/api/v1/projects/[projectId]/agent-controls/route.ts");
|
|
const data = dataModule.default ?? dataModule;
|
|
const auth = authModule.default ?? authModule;
|
|
const route = routeModule.default ?? routeModule;
|
|
|
|
await data.updateProjectAgentControls("master-agent", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "medium",
|
|
});
|
|
|
|
const session = await data.createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const response = await route.GET(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "GET",
|
|
headers: {
|
|
cookie: auth.AUTH_SESSION_COOKIE + "=" + session.sessionToken,
|
|
},
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
const payload = await response.json();
|
|
if (response.status !== 200) {
|
|
throw new Error("expected 200, got " + response.status + ": " + JSON.stringify(payload));
|
|
}
|
|
if (!payload.ok || payload.controls?.modelOverride !== "gpt-5.4" || payload.controls?.reasoningEffortOverride !== "medium") {
|
|
throw new Error("unexpected payload: " + JSON.stringify(payload));
|
|
}
|
|
console.log("OK");
|
|
})().catch((error) => {
|
|
console.error(error);
|
|
process.exit(1);
|
|
});
|
|
`;
|
|
|
|
const tsxBin = path.resolve("node_modules/.bin/tsx");
|
|
const { stdout } = await execFile(tsxBin, ["--eval", script], {
|
|
cwd: process.cwd(),
|
|
env: { ...childEnv, BOSS_RUNTIME_ROOT: runtimeRoot },
|
|
encoding: "utf8",
|
|
maxBuffer: 1024 * 1024,
|
|
});
|
|
|
|
assert.match(stdout, /OK/);
|
|
});
|
|
|
|
test("GET /agent-controls rejects ordinary projects", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const response = await getAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/boss-console/agent-controls", {
|
|
method: "GET",
|
|
headers: {
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
},
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "boss-console" }) },
|
|
);
|
|
|
|
assert.equal(response.status, 404);
|
|
assert.equal((await response.json()).message, "PROJECT_NOT_FOUND");
|
|
});
|
|
|
|
test("POST /agent-controls rejects unknown-key payload and preserves controls", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
await updateProjectAgentControls("master-agent", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "high",
|
|
});
|
|
const beforeState = await readState();
|
|
const beforeProject = beforeState.projects.find((item) => item.id === "master-agent");
|
|
assert.ok(beforeProject, "expected seeded master-agent project");
|
|
const beforeUpdatedAt = beforeProject.updatedAt;
|
|
|
|
const response = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
},
|
|
body: JSON.stringify({
|
|
unexpectedKey: "value",
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
assert.equal(response.status, 400);
|
|
assert.equal((await response.json()).message, "INVALID_AGENT_CONTROLS_PAYLOAD");
|
|
|
|
const controls = await getProjectAgentControls("master-agent");
|
|
assert.equal(controls?.modelOverride, "gpt-5.4");
|
|
assert.equal(controls?.reasoningEffortOverride, "high");
|
|
|
|
const afterState = await readState();
|
|
const afterProject = afterState.projects.find((item) => item.id === "master-agent");
|
|
assert.equal(afterProject?.updatedAt, beforeUpdatedAt);
|
|
});
|
|
|
|
test("master-agent 对话控制 POST 会稳定拒绝非法 backendOverride", async () => {
|
|
await setup();
|
|
|
|
const session = await createAuthSession({
|
|
account: "17600003315",
|
|
role: "highest_admin",
|
|
displayName: "Boss 超级管理员",
|
|
loginMethod: "password",
|
|
});
|
|
|
|
const response = await postAgentControlsRoute(
|
|
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
|
},
|
|
body: JSON.stringify({
|
|
backendOverride: "bad-backend",
|
|
}),
|
|
}),
|
|
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
|
);
|
|
|
|
assert.equal(response.status, 400);
|
|
const payload = (await response.json()) as { ok: boolean; message?: string };
|
|
assert.equal(payload.ok, false);
|
|
assert.equal(payload.message, "INVALID_BACKEND_OVERRIDE");
|
|
});
|
|
|
|
test("master-agent controls helper 不会写入普通项目", async () => {
|
|
await setup();
|
|
|
|
await assert.rejects(
|
|
() =>
|
|
updateProjectAgentControls("boss-console", {
|
|
modelOverride: "gpt-5.4",
|
|
reasoningEffortOverride: "low",
|
|
}),
|
|
/MASTER_AGENT_CONTROLS_SCOPE_RESTRICTED/,
|
|
);
|
|
|
|
const state = await readState();
|
|
const project = state.projects.find((item) => item.id === "boss-console");
|
|
assert.equal(project?.agentControls, undefined);
|
|
assert.equal(await getProjectAgentControls("boss-console"), null);
|
|
});
|