450 lines
16 KiB
TypeScript
450 lines
16 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 data: typeof import("../src/lib/boss-data.ts");
|
||
let postProgress: (typeof import("../src/app/api/v1/master-agent/tasks/[taskId]/progress/route.ts"))["POST"];
|
||
|
||
async function setup() {
|
||
if (runtimeRoot) return;
|
||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-task-progress-route-"));
|
||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||
|
||
const [dataModule, routeModule] = await Promise.all([
|
||
import("../src/lib/boss-data.ts"),
|
||
import("../src/app/api/v1/master-agent/tasks/[taskId]/progress/route.ts"),
|
||
]);
|
||
data = dataModule;
|
||
postProgress = routeModule.POST;
|
||
}
|
||
|
||
test.beforeEach(async () => {
|
||
await setup();
|
||
await rm(runtimeRoot, { recursive: true, force: true });
|
||
});
|
||
|
||
test.after(async () => {
|
||
if (runtimeRoot) await rm(runtimeRoot, { recursive: true, force: true });
|
||
});
|
||
|
||
test("POST task progress accepts device-token updates and keeps task running", async () => {
|
||
const task = await data.queueMasterAgentTask({
|
||
taskId: "route-progress-task",
|
||
projectId: "group-progress-test",
|
||
taskType: "dispatch_execution",
|
||
requestMessageId: "msg-route-progress",
|
||
requestText: "让目标线程继续开发",
|
||
executionPrompt: "让目标线程继续开发",
|
||
requestedBy: "krisolo",
|
||
requestedByAccount: "krisolo",
|
||
deviceId: "mac-studio",
|
||
targetProjectId: "master-agent",
|
||
targetThreadId: "master-agent-thread",
|
||
});
|
||
await data.claimNextMasterAgentTask("mac-studio");
|
||
|
||
const response = await postProgress(
|
||
new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-boss-device-token": "boss-mac-studio-token",
|
||
},
|
||
body: JSON.stringify({
|
||
deviceId: "mac-studio",
|
||
status: "running",
|
||
executionProgress: {
|
||
steps: [
|
||
{ text: "连接 Codex App Server", status: "done" },
|
||
{ text: "启动目标线程 turn", status: "running" },
|
||
],
|
||
branch: { additions: 12, deletions: 1 },
|
||
},
|
||
}),
|
||
}),
|
||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||
);
|
||
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.ok, true);
|
||
assert.equal(payload.task.status, "running");
|
||
assert.equal(payload.task.completedAt, undefined);
|
||
|
||
const state = await data.readState();
|
||
const progressMessage = state.projects
|
||
.find((project) => project.id === "master-agent")
|
||
?.messages.find((message) => message.executionProgress?.taskId === task.taskId);
|
||
assert.equal(progressMessage?.executionProgress?.status, "running");
|
||
assert.equal(progressMessage?.executionProgress?.steps[0]?.text, "连接 Codex App Server");
|
||
assert.equal(progressMessage?.executionProgress?.branch?.additions, 12);
|
||
});
|
||
|
||
test("POST task progress preserves Codex approval, warning, and file-change summaries", async () => {
|
||
const task = await data.queueMasterAgentTask({
|
||
taskId: "route-progress-approval-task",
|
||
projectId: "group-progress-test",
|
||
taskType: "dispatch_execution",
|
||
requestMessageId: "msg-route-progress-approval",
|
||
requestText: "让目标线程继续开发并回写审批状态",
|
||
executionPrompt: "让目标线程继续开发并回写审批状态",
|
||
requestedBy: "krisolo",
|
||
requestedByAccount: "krisolo",
|
||
deviceId: "mac-studio",
|
||
targetProjectId: "master-agent",
|
||
targetThreadId: "master-agent-thread",
|
||
});
|
||
await data.claimNextMasterAgentTask("mac-studio");
|
||
|
||
const response = await postProgress(
|
||
new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-boss-device-token": "boss-mac-studio-token",
|
||
},
|
||
body: JSON.stringify({
|
||
deviceId: "mac-studio",
|
||
status: "running",
|
||
executionProgress: {
|
||
steps: [{ text: "等待 Codex 审批事件", status: "running" }],
|
||
approvals: [
|
||
{
|
||
id: "cmd-approval-1",
|
||
kind: "command",
|
||
label: "命令执行审批",
|
||
status: "resolved",
|
||
detail: "需要确认命令执行",
|
||
},
|
||
],
|
||
warnings: [
|
||
{
|
||
id: "guardian-warning-1",
|
||
message: "检测到需要用户确认的命令执行。",
|
||
severity: "warning",
|
||
},
|
||
],
|
||
fileChanges: [
|
||
{
|
||
id: "file-change-1",
|
||
path: "src/app/page.tsx",
|
||
kind: "update",
|
||
status: "updated",
|
||
},
|
||
],
|
||
},
|
||
}),
|
||
}),
|
||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||
);
|
||
|
||
assert.equal(response.status, 200);
|
||
|
||
const state = await data.readState();
|
||
const progress = state.projects
|
||
.find((project) => project.id === "master-agent")
|
||
?.messages.find((message) => message.executionProgress?.taskId === task.taskId)
|
||
?.executionProgress;
|
||
assert.equal(progress?.approvals?.[0]?.label, "命令执行审批");
|
||
assert.equal(progress?.warnings?.[0]?.message, "检测到需要用户确认的命令执行。");
|
||
assert.equal(progress?.fileChanges?.[0]?.path, "src/app/page.tsx");
|
||
});
|
||
|
||
test("POST task progress preserves Codex thread status and realtime summaries", async () => {
|
||
const task = await data.queueMasterAgentTask({
|
||
taskId: "route-progress-realtime-task",
|
||
projectId: "group-progress-test",
|
||
taskType: "dispatch_execution",
|
||
requestMessageId: "msg-route-progress-realtime",
|
||
requestText: "让目标线程继续开发并回写实时状态",
|
||
executionPrompt: "让目标线程继续开发并回写实时状态",
|
||
requestedBy: "krisolo",
|
||
requestedByAccount: "krisolo",
|
||
deviceId: "mac-studio",
|
||
targetProjectId: "master-agent",
|
||
targetThreadId: "master-agent-thread",
|
||
});
|
||
await data.claimNextMasterAgentTask("mac-studio");
|
||
|
||
const response = await postProgress(
|
||
new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-boss-device-token": "boss-mac-studio-token",
|
||
},
|
||
body: JSON.stringify({
|
||
deviceId: "mac-studio",
|
||
status: "running",
|
||
executionProgress: {
|
||
steps: [{ text: "监听 Codex realtime 事件", status: "running" }],
|
||
threadStatus: {
|
||
type: "active",
|
||
activeFlags: ["waitingOnApproval", "waitingOnUserInput"],
|
||
waitingOnApproval: true,
|
||
waitingOnUserInput: true,
|
||
},
|
||
realtime: {
|
||
status: "streaming",
|
||
sessionId: "rt-session-1",
|
||
version: "v2",
|
||
transcriptRole: "assistant",
|
||
transcriptPreview: "正在分析 Codex App Server 实时事件。",
|
||
audioChunkCount: 1,
|
||
itemCount: 1,
|
||
},
|
||
},
|
||
}),
|
||
}),
|
||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||
);
|
||
|
||
assert.equal(response.status, 200);
|
||
|
||
const state = await data.readState();
|
||
const progress = state.projects
|
||
.find((project) => project.id === "master-agent")
|
||
?.messages.find((message) => message.executionProgress?.taskId === task.taskId)
|
||
?.executionProgress;
|
||
assert.equal(progress?.threadStatus?.type, "active");
|
||
assert.equal(progress?.threadStatus?.waitingOnApproval, true);
|
||
assert.equal(progress?.threadStatus?.waitingOnUserInput, true);
|
||
assert.equal(progress?.realtime?.status, "streaming");
|
||
assert.equal(progress?.realtime?.transcriptPreview, "正在分析 Codex App Server 实时事件。");
|
||
assert.equal(progress?.realtime?.audioChunkCount, 1);
|
||
});
|
||
|
||
test("POST task progress preserves Codex runtime status summaries", async () => {
|
||
const task = await data.queueMasterAgentTask({
|
||
taskId: "route-progress-runtime-task",
|
||
projectId: "group-progress-test",
|
||
taskType: "dispatch_execution",
|
||
requestMessageId: "msg-route-progress-runtime",
|
||
requestText: "让目标线程继续开发并回写运行状态",
|
||
executionPrompt: "让目标线程继续开发并回写运行状态",
|
||
requestedBy: "krisolo",
|
||
requestedByAccount: "krisolo",
|
||
deviceId: "mac-studio",
|
||
targetProjectId: "master-agent",
|
||
targetThreadId: "master-agent-thread",
|
||
});
|
||
await data.claimNextMasterAgentTask("mac-studio");
|
||
|
||
const response = await postProgress(
|
||
new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-boss-device-token": "boss-mac-studio-token",
|
||
},
|
||
body: JSON.stringify({
|
||
deviceId: "mac-studio",
|
||
status: "running",
|
||
executionProgress: {
|
||
steps: [{ text: "同步 Codex 运行状态", status: "running" }],
|
||
modelRoute: {
|
||
fromModel: "gpt-5.4-mini",
|
||
toModel: "gpt-5.4",
|
||
reason: "highRiskCyberActivity",
|
||
},
|
||
tokenUsage: {
|
||
totalTokens: 3000,
|
||
inputTokens: 2200,
|
||
cachedInputTokens: 300,
|
||
outputTokens: 650,
|
||
reasoningOutputTokens: 150,
|
||
modelContextWindow: 200000,
|
||
contextPercent: 2,
|
||
},
|
||
mcpServers: [
|
||
{
|
||
name: "github",
|
||
status: "failed",
|
||
error: "token=[redacted] failed to start",
|
||
},
|
||
],
|
||
remoteControl: {
|
||
status: "connected",
|
||
serverName: "Mac Studio",
|
||
environmentId: "env-prod",
|
||
installationId: "install-secret-should-not-persist",
|
||
},
|
||
},
|
||
}),
|
||
}),
|
||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||
);
|
||
|
||
assert.equal(response.status, 200);
|
||
|
||
const state = await data.readState();
|
||
const progress = state.projects
|
||
.find((project) => project.id === "master-agent")
|
||
?.messages.find((message) => message.executionProgress?.taskId === task.taskId)
|
||
?.executionProgress;
|
||
assert.equal(progress?.modelRoute?.toModel, "gpt-5.4");
|
||
assert.equal(progress?.tokenUsage?.contextPercent, 2);
|
||
assert.equal(progress?.mcpServers?.[0]?.name, "github");
|
||
assert.equal(progress?.mcpServers?.[0]?.error, "token=[redacted] failed to start");
|
||
assert.equal(progress?.remoteControl?.status, "connected");
|
||
assert.equal(JSON.stringify(progress).includes("install-secret-should-not-persist"), false);
|
||
});
|
||
|
||
test("POST task progress preserves Codex thread goal, settings, and compaction summaries", async () => {
|
||
const task = await data.queueMasterAgentTask({
|
||
taskId: "route-progress-thread-config-task",
|
||
projectId: "group-progress-test",
|
||
taskType: "dispatch_execution",
|
||
requestMessageId: "msg-route-progress-thread-config",
|
||
requestText: "让目标线程同步目标和设置",
|
||
executionPrompt: "让目标线程同步目标和设置",
|
||
requestedBy: "krisolo",
|
||
requestedByAccount: "krisolo",
|
||
deviceId: "mac-studio",
|
||
targetProjectId: "master-agent",
|
||
targetThreadId: "master-agent-thread",
|
||
});
|
||
await data.claimNextMasterAgentTask("mac-studio");
|
||
|
||
const response = await postProgress(
|
||
new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-boss-device-token": "boss-mac-studio-token",
|
||
},
|
||
body: JSON.stringify({
|
||
deviceId: "mac-studio",
|
||
status: "running",
|
||
executionProgress: {
|
||
steps: [{ text: "同步 Codex 线程配置", status: "running" }],
|
||
threadGoal: {
|
||
objective: "完成 App Server 线程目标同步",
|
||
status: "active",
|
||
tokenBudget: 120000,
|
||
tokensUsed: 4800,
|
||
timeUsedSeconds: 600,
|
||
},
|
||
threadSettings: {
|
||
model: "gpt-5.5",
|
||
modelProvider: "openai",
|
||
approvalPolicy: "on-request",
|
||
approvalsReviewer: "user",
|
||
sandboxPolicy: "workspaceWrite",
|
||
permissionProfile: ":workspace",
|
||
serviceTier: "fast",
|
||
effort: "low",
|
||
summary: "concise",
|
||
collaborationMode: "plan",
|
||
personality: "pragmatic",
|
||
cwd: "/Users/kris/code/boss/secret-project",
|
||
},
|
||
compaction: {
|
||
status: "completed",
|
||
message: "上下文已压缩",
|
||
turnId: "turn-secret-should-not-persist",
|
||
},
|
||
},
|
||
}),
|
||
}),
|
||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||
);
|
||
|
||
assert.equal(response.status, 200);
|
||
|
||
const state = await data.readState();
|
||
const progress = state.projects
|
||
.find((project) => project.id === "master-agent")
|
||
?.messages.find((message) => message.executionProgress?.taskId === task.taskId)
|
||
?.executionProgress;
|
||
assert.equal(progress?.threadGoal?.objective, "完成 App Server 线程目标同步");
|
||
assert.equal(progress?.threadSettings?.model, "gpt-5.5");
|
||
assert.equal(progress?.threadSettings?.sandboxPolicy, "workspaceWrite");
|
||
assert.equal(progress?.compaction?.status, "completed");
|
||
const serialized = JSON.stringify(progress);
|
||
assert.equal(serialized.includes("/Users/kris"), false);
|
||
assert.equal(serialized.includes("turn-secret-should-not-persist"), false);
|
||
});
|
||
|
||
test("POST task progress preserves Codex account, quota, verification, and notice summaries", async () => {
|
||
const task = await data.queueMasterAgentTask({
|
||
taskId: "route-progress-account-notices-task",
|
||
projectId: "group-progress-test",
|
||
taskType: "dispatch_execution",
|
||
requestMessageId: "msg-route-progress-account-notices",
|
||
requestText: "让目标线程同步账号和告警",
|
||
executionPrompt: "让目标线程同步账号和告警",
|
||
requestedBy: "krisolo",
|
||
requestedByAccount: "krisolo",
|
||
deviceId: "mac-studio",
|
||
targetProjectId: "master-agent",
|
||
targetThreadId: "master-agent-thread",
|
||
});
|
||
await data.claimNextMasterAgentTask("mac-studio");
|
||
|
||
const response = await postProgress(
|
||
new NextRequest(`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/progress`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-boss-device-token": "boss-mac-studio-token",
|
||
},
|
||
body: JSON.stringify({
|
||
deviceId: "mac-studio",
|
||
status: "running",
|
||
executionProgress: {
|
||
steps: [{ text: "同步 Codex 账号运行态", status: "running" }],
|
||
accountStatus: {
|
||
authMode: "chatgpt",
|
||
planType: "team",
|
||
limitId: "codex",
|
||
limitName: "Codex",
|
||
usedPercent: 88,
|
||
windowDurationMins: 180,
|
||
resetsAt: 1770003600,
|
||
creditsBalance: "120.5",
|
||
hasCredits: true,
|
||
unlimitedCredits: false,
|
||
accessToken: "sk-secret-should-not-persist",
|
||
},
|
||
modelVerification: {
|
||
verifications: ["trustedAccessForCyber"],
|
||
turnId: "turn-secret-should-not-persist",
|
||
},
|
||
warnings: [
|
||
{
|
||
id: "config-warning-1",
|
||
message: "项目配置已忽略:openai_base_url 不能放在项目配置里",
|
||
severity: "warning",
|
||
path: "/Users/kris/code/boss/.codex/config.toml",
|
||
},
|
||
],
|
||
},
|
||
}),
|
||
}),
|
||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||
);
|
||
|
||
assert.equal(response.status, 200);
|
||
|
||
const state = await data.readState();
|
||
const progress = state.projects
|
||
.find((project) => project.id === "master-agent")
|
||
?.messages.find((message) => message.executionProgress?.taskId === task.taskId)
|
||
?.executionProgress;
|
||
assert.equal(progress?.accountStatus?.authMode, "chatgpt");
|
||
assert.equal(progress?.accountStatus?.planType, "team");
|
||
assert.equal(progress?.accountStatus?.usedPercent, 88);
|
||
assert.equal(progress?.modelVerification?.verifications?.[0], "trustedAccessForCyber");
|
||
assert.equal(progress?.warnings?.[0]?.message, "项目配置已忽略:openai_base_url 不能放在项目配置里");
|
||
const serialized = JSON.stringify(progress);
|
||
assert.equal(serialized.includes("/Users/kris"), false);
|
||
assert.equal(serialized.includes("sk-secret-should-not-persist"), false);
|
||
assert.equal(serialized.includes("turn-secret-should-not-persist"), false);
|
||
});
|