215 lines
7.5 KiB
TypeScript
215 lines
7.5 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";
|
||
|
||
let runtimeRoot = "";
|
||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
|
||
let appendAppLog: (typeof import("../src/lib/boss-data"))["appendAppLog"];
|
||
|
||
const leakedPrompt = [
|
||
"管理员全局主提示词:",
|
||
"你是 Boss 控制台的主 Agent。",
|
||
"默认只说和当前问题直接相关的判断、动作和风险。",
|
||
"",
|
||
"用户私有主提示词:",
|
||
"默认中文回复。",
|
||
"",
|
||
"当前对话附加提示词:",
|
||
"同步项目目标和版本记录后记得告诉我。",
|
||
"",
|
||
"当前消息:",
|
||
"同步完成记得要和我说,以后也是这样。",
|
||
].join("\n");
|
||
|
||
async function setup() {
|
||
if (runtimeRoot) {
|
||
return;
|
||
}
|
||
|
||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-runtime-leak-redaction-"));
|
||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||
|
||
const data = await import("../src/lib/boss-data.ts");
|
||
readState = data.readState;
|
||
writeState = data.writeState;
|
||
appendAppLog = data.appendAppLog;
|
||
}
|
||
|
||
test.after(async () => {
|
||
if (runtimeRoot) {
|
||
await rm(runtimeRoot, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("读取已有状态时会清洗历史提示词泄漏内容", async () => {
|
||
await setup();
|
||
|
||
const state = await readState();
|
||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||
assert.ok(masterProject, "expected a master-agent project");
|
||
|
||
masterProject.messages.push({
|
||
id: "msg-leak-1",
|
||
sender: "ops",
|
||
senderLabel: "主 Agent Relay",
|
||
body: `Master Codex Node 执行失败:\n${leakedPrompt}`,
|
||
sentAt: "2026-04-19T08:35:01.079Z",
|
||
kind: "text",
|
||
});
|
||
masterProject.preview = leakedPrompt;
|
||
|
||
state.masterAgentTasks.unshift({
|
||
taskId: "task-leak-1",
|
||
projectId: "master-agent",
|
||
taskType: "conversation_reply",
|
||
requestMessageId: "msg-user-1",
|
||
requestText: "同步完成记得要和我说,以后也是这样。",
|
||
executionPrompt: leakedPrompt,
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
deviceId: "mac-studio",
|
||
status: "failed",
|
||
requestedAt: "2026-04-19T08:35:01.079Z",
|
||
errorMessage: leakedPrompt,
|
||
});
|
||
|
||
state.appLogs.unshift({
|
||
logId: "log-leak-1",
|
||
deviceId: "mac-studio",
|
||
projectId: "master-agent",
|
||
level: "error",
|
||
source: "local_agent",
|
||
category: "local_agent.master_agent_task_failed",
|
||
message: "Master Codex Node 执行主 Agent 任务失败:task-leak-1",
|
||
detail: leakedPrompt,
|
||
mirroredToProject: true,
|
||
createdAt: "2026-04-19T08:35:01.079Z",
|
||
});
|
||
|
||
await writeState(state);
|
||
|
||
const nextState = await readState();
|
||
const nextMasterProject = nextState.projects.find((project) => project.id === "master-agent");
|
||
assert.ok(nextMasterProject, "expected a reloaded master-agent project");
|
||
|
||
const leakedMessage = nextMasterProject.messages.find((message) => message.id === "msg-leak-1");
|
||
assert.ok(leakedMessage, "expected the historical message to remain");
|
||
assert.equal(
|
||
/管理员全局主提示词:|用户私有主提示词:|当前对话附加提示词:/.test(leakedMessage.body),
|
||
false,
|
||
);
|
||
assert.match(leakedMessage.body, /已拦截内部执行日志|原始内容已隐藏/);
|
||
assert.equal(
|
||
/管理员全局主提示词:|用户私有主提示词:|当前对话附加提示词:/.test(
|
||
nextMasterProject.preview ?? "",
|
||
),
|
||
false,
|
||
);
|
||
|
||
const sanitizedTask = nextState.masterAgentTasks.find((task) => task.taskId === "task-leak-1");
|
||
assert.equal(sanitizedTask?.errorMessage, "MASTER_CODEX_NODE_OUTPUT_LEAKED");
|
||
|
||
const sanitizedLog = nextState.appLogs.find((log) => log.logId === "log-leak-1");
|
||
assert.ok(sanitizedLog, "expected the historical app log to remain");
|
||
assert.equal(
|
||
/管理员全局主提示词:|用户私有主提示词:|当前对话附加提示词:/.test(
|
||
sanitizedLog?.detail ?? "",
|
||
),
|
||
false,
|
||
);
|
||
assert.match(sanitizedLog?.detail ?? "", /已拦截内部执行日志|原始内容不再展示/);
|
||
});
|
||
|
||
test("local agent infrastructure failures stay out of master agent chat", async () => {
|
||
await setup();
|
||
|
||
const state = await readState();
|
||
const device = state.devices[0];
|
||
assert.ok(device, "expected a seeded device");
|
||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||
assert.ok(masterProject, "expected a master-agent project");
|
||
const beforeMessageCount = masterProject.messages.length;
|
||
await writeState(state);
|
||
|
||
const entry = await appendAppLog({
|
||
deviceId: device.id,
|
||
level: "error",
|
||
source: "local_agent",
|
||
category: "local_agent.master_agent_task_failed",
|
||
message: "Master Codex Node 执行失败:task-demo",
|
||
detail: "Permission denied",
|
||
mirrorToMaster: true,
|
||
});
|
||
|
||
assert.equal(entry.mirroredToProject, false);
|
||
|
||
const nextState = await readState();
|
||
const nextMasterProject = nextState.projects.find((project) => project.id === "master-agent");
|
||
assert.ok(nextMasterProject, "expected a reloaded master-agent project");
|
||
assert.equal(nextMasterProject.messages.length, beforeMessageCount);
|
||
assert.ok(
|
||
nextState.appLogs.some((log) => log.logId === entry.logId && log.category === "local_agent.master_agent_task_failed"),
|
||
"expected the operational log to remain available outside chat",
|
||
);
|
||
});
|
||
|
||
test("读取已有状态时会把历史 Codex App Server 错误码转成人类可读说明", async () => {
|
||
await setup();
|
||
|
||
const state = await readState();
|
||
state.projects.push({
|
||
id: "project-runtime-error-redaction",
|
||
name: "juyuwan",
|
||
pinned: false,
|
||
systemPinned: false,
|
||
deviceIds: ["mac-studio"],
|
||
preview: "juyuwan 执行失败:CODEX_APP_SERVER_TURN_INTERRUPTED",
|
||
updatedAt: "2026-06-07T14:20:00+08:00",
|
||
lastMessageAt: "2026-06-07T14:20:00+08:00",
|
||
isGroup: false,
|
||
threadMeta: {
|
||
projectId: "project-runtime-error-redaction",
|
||
threadId: "thread-runtime-error-redaction",
|
||
threadDisplayName: "juyuwan",
|
||
folderName: "juyuwan",
|
||
activityIconCount: 0,
|
||
updatedAt: "2026-06-07T14:20:00+08:00",
|
||
codexThreadRef: "019e9b84-decc-7510-b84f-57c5a27de0e3",
|
||
codexFolderRef: "juyuwan",
|
||
},
|
||
groupMembers: [],
|
||
createdByAgent: true,
|
||
collaborationMode: "development",
|
||
approvalState: "not_required",
|
||
unreadCount: 1,
|
||
riskLevel: "low",
|
||
messages: [
|
||
{
|
||
id: "msg-runtime-error-redaction",
|
||
sender: "ops",
|
||
senderLabel: "juyuwan",
|
||
body: "juyuwan 执行失败:CODEX_APP_SERVER_TURN_INTERRUPTED",
|
||
sentAt: "2026-06-07T14:20:00+08:00",
|
||
kind: "text",
|
||
},
|
||
],
|
||
goals: [],
|
||
versions: [],
|
||
});
|
||
await writeState(state);
|
||
|
||
const nextState = await readState();
|
||
const project = nextState.projects.find((item) => item.id === "project-runtime-error-redaction");
|
||
assert.ok(project, "expected a reloaded project");
|
||
const message = project.messages.find((item) => item.id === "msg-runtime-error-redaction");
|
||
assert.ok(message, "expected the historical message to remain");
|
||
assert.equal(message.body.includes("CODEX_APP_SERVER_TURN_INTERRUPTED"), false);
|
||
assert.equal(project.preview.includes("CODEX_APP_SERVER_TURN_INTERRUPTED"), false);
|
||
assert.match(message.body, /Codex 桌面线程本轮被中断/);
|
||
assert.match(project.preview, /Codex 桌面线程本轮被中断/);
|
||
});
|