feat: refine mobile master agent sync and chat rendering
This commit is contained in:
292
tests/ai-account-routes.test.ts
Normal file
292
tests/ai-account-routes.test.ts
Normal file
@@ -0,0 +1,292 @@
|
||||
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 createAccountRoute: (typeof import("../src/app/api/v1/accounts/route"))["POST"];
|
||||
let updateAccountRoute: (typeof import("../src/app/api/v1/accounts/[accountId]/route"))["PATCH"];
|
||||
let validateDraftAccountRoute: (typeof import("../src/app/api/v1/accounts/validate-draft/route"))["POST"];
|
||||
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
||||
let AUTH_SESSION_COOKIE = "";
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) {
|
||||
return;
|
||||
}
|
||||
|
||||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-ai-account-routes-"));
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
|
||||
const [accountsModule, accountDetailModule, validateDraftModule, dataModule, authModule] = await Promise.all([
|
||||
import("../src/app/api/v1/accounts/route.ts"),
|
||||
import("../src/app/api/v1/accounts/[accountId]/route.ts"),
|
||||
import("../src/app/api/v1/accounts/validate-draft/route.ts"),
|
||||
import("../src/lib/boss-data.ts"),
|
||||
import("../src/lib/boss-auth.ts"),
|
||||
]);
|
||||
|
||||
createAccountRoute = accountsModule.POST;
|
||||
updateAccountRoute = accountDetailModule.PATCH;
|
||||
validateDraftAccountRoute = validateDraftModule.POST;
|
||||
createAuthSession = dataModule.createAuthSession;
|
||||
AUTH_SESSION_COOKIE = authModule.AUTH_SESSION_COOKIE;
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
if (runtimeRoot) {
|
||||
await rm(runtimeRoot, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
async function createAuthedJsonRequest(url: string, method: "POST" | "PATCH", body: Record<string, unknown>) {
|
||||
const session = await createAuthSession({
|
||||
account: "17600003315",
|
||||
role: "highest_admin",
|
||||
displayName: "Boss 超级管理员",
|
||||
loginMethod: "password",
|
||||
});
|
||||
|
||||
return new NextRequest(url, {
|
||||
method,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
||||
},
|
||||
body: JSON.stringify(body),
|
||||
});
|
||||
}
|
||||
|
||||
test("POST /api/v1/accounts accepts 环宇智擎 accounts", async () => {
|
||||
await setup();
|
||||
|
||||
const response = await createAccountRoute(
|
||||
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts", "POST", {
|
||||
label: "主 GPT",
|
||||
role: "primary",
|
||||
provider: "hyzq_api",
|
||||
displayName: "环宇智擎主链路",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "sk-hyzq-demo-123456",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
account: {
|
||||
provider: string;
|
||||
providerLabel: string;
|
||||
apiBaseUrl?: string;
|
||||
isActive: boolean;
|
||||
};
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.account.provider, "hyzq_api");
|
||||
assert.equal(payload.account.providerLabel, "环宇智擎 API");
|
||||
assert.equal(payload.account.apiBaseUrl, "https://api.hyzq2046.com/v1");
|
||||
assert.equal(payload.account.isActive, true);
|
||||
});
|
||||
|
||||
test("PATCH /api/v1/accounts/[accountId] accepts GLM accounts", async () => {
|
||||
await setup();
|
||||
|
||||
const createResponse = await createAccountRoute(
|
||||
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts", "POST", {
|
||||
label: "备用 GPT",
|
||||
role: "backup",
|
||||
provider: "custom_api",
|
||||
displayName: "临时备用链路",
|
||||
model: "temp-model",
|
||||
apiBaseUrl: "https://gateway.example.com/v1",
|
||||
apiKey: "sk-temp-demo-123456",
|
||||
enabled: true,
|
||||
}),
|
||||
);
|
||||
const createPayload = (await createResponse.json()) as { account: { accountId: string } };
|
||||
|
||||
const response = await updateAccountRoute(
|
||||
await createAuthedJsonRequest(
|
||||
`http://127.0.0.1:3000/api/v1/accounts/${createPayload.account.accountId}`,
|
||||
"PATCH",
|
||||
{
|
||||
label: "备用 GPT",
|
||||
role: "backup",
|
||||
provider: "glm_api",
|
||||
displayName: "GLM 备用账号",
|
||||
model: "glm-4.5",
|
||||
apiKey: "sk-glm-demo-123456",
|
||||
enabled: true,
|
||||
},
|
||||
),
|
||||
{ params: Promise.resolve({ accountId: createPayload.account.accountId }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
account: {
|
||||
provider: string;
|
||||
providerLabel: string;
|
||||
apiBaseUrl?: string;
|
||||
displayName: string;
|
||||
};
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.account.provider, "glm_api");
|
||||
assert.equal(payload.account.providerLabel, "GLM API");
|
||||
assert.equal(payload.account.apiBaseUrl, "https://open.bigmodel.cn/api/paas/v4");
|
||||
assert.equal(payload.account.displayName, "GLM 备用账号");
|
||||
});
|
||||
|
||||
test("POST /api/v1/accounts/validate-draft probes API draft and returns available models", async () => {
|
||||
await setup();
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input) => {
|
||||
if (typeof input === "string" && input === "https://api.hyzq2046.com/v1/responses") {
|
||||
return new Response(JSON.stringify({ output_text: "连接正常" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-hyzq-draft-validate",
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await validateDraftAccountRoute(
|
||||
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts/validate-draft", "POST", {
|
||||
provider: "hyzq_api",
|
||||
apiKey: "sk-hyzq-demo-123456",
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
status: string;
|
||||
requestId?: string;
|
||||
availableModels: string[];
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.status, "ready");
|
||||
assert.equal(payload.requestId, "req-hyzq-draft-validate");
|
||||
assert.deepEqual(payload.availableModels, ["gpt-5.4-mini", "gpt-5.4"]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /api/v1/accounts/validate-draft prefers provider returned models over static defaults", async () => {
|
||||
await setup();
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input) => {
|
||||
if (typeof input === "string" && input === "https://api.openai.com/v1/responses") {
|
||||
return new Response(JSON.stringify({ output_text: "连接正常" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-openai-draft-validate",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (typeof input === "string" && input === "https://api.openai.com/v1/models") {
|
||||
return new Response(JSON.stringify({
|
||||
data: [
|
||||
{ id: "gpt-5.4" },
|
||||
{ id: "gpt-4.1" },
|
||||
],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await validateDraftAccountRoute(
|
||||
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts/validate-draft", "POST", {
|
||||
provider: "openai_api",
|
||||
apiKey: "sk-openai-demo-123456",
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
availableModels: string[];
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.deepEqual(payload.availableModels, ["gpt-5.4", "gpt-4.1"]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /api/v1/accounts/validate-draft falls back to compatible models for custom api", async () => {
|
||||
await setup();
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input) => {
|
||||
if (typeof input === "string" && input === "https://gateway.example.com/v1/chat/completions") {
|
||||
return new Response(JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: "连接正常",
|
||||
},
|
||||
},
|
||||
],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-custom-draft-validate",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (typeof input === "string" && input === "https://gateway.example.com/v1/models") {
|
||||
return new Response(JSON.stringify({ data: [] }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await validateDraftAccountRoute(
|
||||
await createAuthedJsonRequest("http://127.0.0.1:3000/api/v1/accounts/validate-draft", "POST", {
|
||||
provider: "custom_api",
|
||||
apiKey: "sk-custom-demo-123456",
|
||||
apiBaseUrl: "https://gateway.example.com/v1",
|
||||
}),
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
message: string;
|
||||
availableModels: string[];
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.match(payload.message, /兜底/);
|
||||
assert.deepEqual(payload.availableModels, ["gpt-5.4-mini", "gpt-5.4", "gpt-5.1", "gpt-4.1"]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
@@ -118,3 +118,190 @@ test("validateAiAccountConnection probes aliyun qwen backup accounts through the
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("validateAiAccountConnection honors custom api base url overrides for OpenAI accounts", async () => {
|
||||
await setup();
|
||||
|
||||
await saveAiAccount(Object.assign({
|
||||
accountId: "openai-api-primary",
|
||||
label: "主 GPT",
|
||||
role: "primary" as const,
|
||||
provider: "openai_api" as const,
|
||||
displayName: "OpenAI 平台账号",
|
||||
accountIdentifier: "openai-demo",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "sk-openai-demo-123456",
|
||||
enabled: true,
|
||||
}, {
|
||||
apiBaseUrl: "https://gateway.example.com/openai/v1",
|
||||
}));
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input) => {
|
||||
if (typeof input === "string" && input === "https://gateway.example.com/openai/v1/responses") {
|
||||
return new Response(JSON.stringify({ output_text: "连接正常" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-openai-custom-endpoint",
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await validateAiAccountConnection("openai-api-primary");
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.status, "ready");
|
||||
assert.match(result.message, /连接正常/);
|
||||
assert.equal(result.requestId, "req-openai-custom-endpoint");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("validateAiAccountConnection uses the default 环宇智擎 endpoint for primary API accounts", async () => {
|
||||
await setup();
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "hyzq-primary",
|
||||
label: "主 GPT",
|
||||
role: "primary",
|
||||
provider: "hyzq_api",
|
||||
displayName: "环宇智擎主链路",
|
||||
accountIdentifier: "hyzq-primary-demo",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "sk-hyzq-demo-123456",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input) => {
|
||||
if (typeof input === "string" && input === "https://api.hyzq2046.com/v1/responses") {
|
||||
return new Response(JSON.stringify({ output_text: "连接正常" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-hyzq-validate",
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await validateAiAccountConnection("hyzq-primary");
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.status, "ready");
|
||||
assert.equal(result.requestId, "req-hyzq-validate");
|
||||
assert.match(result.message, /连接正常/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("validateAiAccountConnection probes GLM accounts through chat completions", async () => {
|
||||
await setup();
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "glm-backup",
|
||||
label: "备用 GPT",
|
||||
role: "backup",
|
||||
provider: "glm_api",
|
||||
displayName: "GLM 备用账号",
|
||||
accountIdentifier: "glm-demo",
|
||||
model: "glm-4.5",
|
||||
apiKey: "sk-glm-demo-123456",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input) => {
|
||||
if (typeof input === "string" && input === "https://open.bigmodel.cn/api/paas/v4/chat/completions") {
|
||||
return new Response(JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: "连接正常",
|
||||
},
|
||||
},
|
||||
],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-glm-validate",
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await validateAiAccountConnection("glm-backup");
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.status, "ready");
|
||||
assert.equal(result.requestId, "req-glm-validate");
|
||||
assert.match(result.message, /连接正常/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("validateAiAccountConnection falls back to generic models for custom api accounts", async () => {
|
||||
await setup();
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "custom-backup",
|
||||
label: "备用 GPT",
|
||||
role: "backup",
|
||||
provider: "custom_api",
|
||||
displayName: "自定义兼容 API",
|
||||
accountIdentifier: "custom-demo",
|
||||
model: "gpt-5.4",
|
||||
apiBaseUrl: "https://gateway.example.com/v1",
|
||||
apiKey: "sk-custom-demo-123456",
|
||||
enabled: true,
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input) => {
|
||||
if (typeof input === "string" && input === "https://gateway.example.com/v1/chat/completions") {
|
||||
return new Response(JSON.stringify({
|
||||
choices: [
|
||||
{
|
||||
message: {
|
||||
content: "连接正常",
|
||||
},
|
||||
},
|
||||
],
|
||||
}), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-custom-validate",
|
||||
},
|
||||
});
|
||||
}
|
||||
if (typeof input === "string" && input === "https://gateway.example.com/v1/models") {
|
||||
return new Response(JSON.stringify({ data: [] }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
},
|
||||
});
|
||||
}
|
||||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await validateAiAccountConnection("custom-backup");
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.status, "ready");
|
||||
assert.match(result.message, /兜底/);
|
||||
assert.deepEqual(result.availableModels, ["gpt-5.4", "gpt-5.4-mini", "gpt-5.1", "gpt-4.1"]);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
52
tests/chat-markdown.test.ts
Normal file
52
tests/chat-markdown.test.ts
Normal file
@@ -0,0 +1,52 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import path from "node:path";
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { parseChatMarkdown } from "../src/lib/chat-markdown";
|
||||
|
||||
const testsDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
|
||||
async function readWorkspaceFile(relativePath: string) {
|
||||
return readFile(path.join(testsDir, "..", relativePath), "utf8");
|
||||
}
|
||||
|
||||
test("parseChatMarkdown recognizes section labels and preserves reading order", () => {
|
||||
const blocks = parseChatMarkdown(
|
||||
"项目目标:完成 Boss 真机回归\n当前进度:已完成 UI 调整\n下一步:推送到 Gitea",
|
||||
);
|
||||
|
||||
assert.equal(blocks.length, 3);
|
||||
assert.deepEqual(
|
||||
blocks.map((block) => block.kind),
|
||||
["label", "label", "label"],
|
||||
);
|
||||
assert.equal(blocks[0]?.label, "项目目标");
|
||||
assert.equal(blocks[0]?.text, "完成 Boss 真机回归");
|
||||
assert.equal(blocks[1]?.label, "当前进度");
|
||||
assert.equal(blocks[2]?.label, "下一步");
|
||||
});
|
||||
|
||||
test("parseChatMarkdown keeps bullets, ordered items, quotes, and fenced code distinct", () => {
|
||||
const blocks = parseChatMarkdown(
|
||||
"# 标题\n\n- 第一项\n1. 第二项\n> 引用\n```ts\nconst ok = true;\n```",
|
||||
);
|
||||
|
||||
assert.deepEqual(
|
||||
blocks.map((block) => block.kind),
|
||||
["heading", "bullet", "ordered", "quote", "code"],
|
||||
);
|
||||
assert.equal(blocks[0]?.text, "标题");
|
||||
assert.equal(blocks[1]?.text, "第一项");
|
||||
assert.equal(blocks[2]?.order, "1.");
|
||||
assert.equal(blocks[3]?.text, "引用");
|
||||
assert.match(blocks[4]?.text ?? "", /const ok = true;/);
|
||||
});
|
||||
|
||||
test("ChatBubble renders parsed markdown blocks instead of raw plain text bodies", async () => {
|
||||
const source = await readWorkspaceFile("src/components/app-ui.tsx");
|
||||
|
||||
assert.match(source, /import \{ parseChatMarkdown(?:, type ChatMarkdownBlock)? \} from "@\/lib\/chat-markdown"/);
|
||||
assert.match(source, /const blocks = parseChatMarkdown\(body\);/);
|
||||
assert.match(source, /function ChatBubbleMarkdown/);
|
||||
});
|
||||
@@ -216,6 +216,7 @@ test("heartbeat external activity on an active cli folder blocks the next claim
|
||||
|
||||
const project = await getCliProject();
|
||||
const folderKey = buildProjectFolderKey(project);
|
||||
const recentExternalActivityAt = new Date(Date.now() - 60_000).toISOString();
|
||||
|
||||
const firstTask = await queueMasterAgentTask({
|
||||
projectId: project.id,
|
||||
@@ -252,7 +253,7 @@ test("heartbeat external activity on an active cli folder blocks the next claim
|
||||
threadDisplayName: project.threadMeta.threadDisplayName,
|
||||
codexFolderRef: project.threadMeta.codexFolderRef,
|
||||
codexThreadRef: project.threadMeta.codexThreadRef,
|
||||
lastActiveAt: "2026-04-06T11:05:00.000Z",
|
||||
lastActiveAt: recentExternalActivityAt,
|
||||
suggestedImport: true,
|
||||
},
|
||||
],
|
||||
@@ -263,7 +264,7 @@ test("heartbeat external activity on an active cli folder blocks the next claim
|
||||
assert.ok(policy, "expected heartbeat to persist a scoped conflict policy");
|
||||
assert.equal(policy?.activeCliExecution, true);
|
||||
assert.equal(policy?.conflictState, "blocked");
|
||||
assert.equal(policy?.recentExternalActivityAt, "2026-04-06T11:05:00.000Z");
|
||||
assert.equal(policy?.recentExternalActivityAt, recentExternalActivityAt);
|
||||
|
||||
const secondTask = await queueMasterAgentTask({
|
||||
projectId: project.id,
|
||||
@@ -314,3 +315,49 @@ test("heartbeat external activity on an active cli folder blocks the next claim
|
||||
assert.equal(policy?.activeCliExecution, false);
|
||||
assert.equal(policy?.conflictState, "blocked");
|
||||
});
|
||||
|
||||
test("stale blocked policy does not keep queued conversation replies stuck forever", async () => {
|
||||
await setup();
|
||||
|
||||
const project = await getCliProject();
|
||||
const folderKey = buildProjectFolderKey(project);
|
||||
const state = await readState();
|
||||
state.projectExecutionPolicies = [
|
||||
{
|
||||
deviceId: "mac-studio",
|
||||
folderKey,
|
||||
projectId: project.id,
|
||||
allowPolicy: "forbid",
|
||||
conflictState: "blocked",
|
||||
recentExternalActivityAt: "2026-04-06T09:30:00.000Z",
|
||||
updatedAt: "2026-04-06T09:30:00.000Z",
|
||||
},
|
||||
];
|
||||
await writeState(state);
|
||||
|
||||
const queuedTask = await queueMasterAgentTask({
|
||||
projectId: project.id,
|
||||
requestMessageId: "msg-stale-policy",
|
||||
requestText: "继续推进这个线程",
|
||||
executionPrompt: "请继续推进这个线程",
|
||||
requestedBy: "Boss 超级管理员",
|
||||
requestedByAccount: "17600003315",
|
||||
deviceId: "mac-studio",
|
||||
taskType: "conversation_reply",
|
||||
targetProjectId: project.id,
|
||||
targetThreadId: project.threadMeta.threadId,
|
||||
targetThreadDisplayName: project.threadMeta.threadDisplayName,
|
||||
targetCodexThreadRef: project.threadMeta.codexThreadRef,
|
||||
targetCodexFolderRef: project.threadMeta.codexFolderRef,
|
||||
});
|
||||
|
||||
const claimed = await claimNextMasterAgentTask("mac-studio");
|
||||
assert.equal(claimed?.taskId, queuedTask.taskId);
|
||||
|
||||
const nextState = await readState();
|
||||
const policy = nextState.projectExecutionPolicies.find((item) => item.folderKey === folderKey);
|
||||
assert.ok(policy, "expected stale scoped policy to remain in state");
|
||||
assert.equal(policy?.conflictState, "none");
|
||||
assert.equal(policy?.activeCliExecution, true);
|
||||
assert.equal(policy?.recentExternalActivityAt, undefined);
|
||||
});
|
||||
|
||||
@@ -131,6 +131,28 @@ test("master-agent 会话可保存并读取模型与推理强度覆盖", async (
|
||||
assert.equal(detail?.agentControls?.reasoningEffortOverride, "high");
|
||||
});
|
||||
|
||||
test("master-agent 会话可保存并读取快速反应与深度思考模型映射", async () => {
|
||||
await setup();
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
fastModelOverride: "gpt-4.1",
|
||||
deepModelOverride: "gpt-5.1",
|
||||
});
|
||||
|
||||
const controls = await getProjectAgentControls("master-agent");
|
||||
assert.equal(controls?.fastModelOverride, "gpt-4.1");
|
||||
assert.equal(controls?.deepModelOverride, "gpt-5.1");
|
||||
|
||||
const state = await readState();
|
||||
const project = state.projects.find((item) => item.id === "master-agent");
|
||||
assert.equal(project?.agentControls?.fastModelOverride, "gpt-4.1");
|
||||
assert.equal(project?.agentControls?.deepModelOverride, "gpt-5.1");
|
||||
|
||||
const detail = getProjectDetailView(state, "master-agent");
|
||||
assert.equal(detail?.agentControls?.fastModelOverride, "gpt-4.1");
|
||||
assert.equal(detail?.agentControls?.deepModelOverride, "gpt-5.1");
|
||||
});
|
||||
|
||||
test("master-agent 对话控制路由可读写并回显到项目详情", async () => {
|
||||
await setup();
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-agent-controls-"));
|
||||
@@ -167,6 +189,8 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
body: JSON.stringify({
|
||||
modelOverride: "gpt-5.4",
|
||||
reasoningEffortOverride: "medium",
|
||||
fastModelOverride: "gpt-4.1",
|
||||
deepModelOverride: "gpt-5.1",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
@@ -179,6 +203,8 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
controls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
fastModelOverride?: string;
|
||||
deepModelOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
@@ -186,6 +212,8 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
assert.equal(postPayload.ok, true);
|
||||
assert.equal(postPayload.controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(postPayload.controls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(postPayload.controls?.fastModelOverride, "gpt-4.1");
|
||||
assert.equal(postPayload.controls?.deepModelOverride, "gpt-5.1");
|
||||
assert.equal(postPayload.controls?.backendOverride, "claw-runtime");
|
||||
|
||||
const getResponse = await getAgentControlsRoute(
|
||||
@@ -202,6 +230,8 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
controls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
fastModelOverride?: string;
|
||||
deepModelOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
@@ -209,6 +239,8 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
assert.equal(getPayload.ok, true);
|
||||
assert.equal(getPayload.controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(getPayload.controls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(getPayload.controls?.fastModelOverride, "gpt-4.1");
|
||||
assert.equal(getPayload.controls?.deepModelOverride, "gpt-5.1");
|
||||
assert.equal(getPayload.controls?.backendOverride, "claw-runtime");
|
||||
|
||||
const projectResponse = await getProjectRoute(
|
||||
@@ -225,6 +257,8 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
agentControls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
fastModelOverride?: string;
|
||||
deepModelOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
@@ -232,6 +266,8 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
assert.equal(projectPayload.ok, true);
|
||||
assert.equal(projectPayload.agentControls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(projectPayload.agentControls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(projectPayload.agentControls?.fastModelOverride, "gpt-4.1");
|
||||
assert.equal(projectPayload.agentControls?.deepModelOverride, "gpt-5.1");
|
||||
assert.equal(projectPayload.agentControls?.backendOverride, "claw-runtime");
|
||||
} finally {
|
||||
if (previousEnv.BOSS_CLAW_ENABLED === undefined) delete process.env.BOSS_CLAW_ENABLED;
|
||||
|
||||
@@ -147,7 +147,7 @@ test("主 Agent 执行 prompt 会明确声明管理员全局提示词不可覆
|
||||
await createUserMasterMemory({
|
||||
account: "17600003315",
|
||||
scope: "project",
|
||||
projectId: "boss-console",
|
||||
projectId: "boss-main",
|
||||
title: "boss 项目进度",
|
||||
content: "boss 项目当前按项目聚合加线程下钻展示。",
|
||||
memoryType: "project_progress",
|
||||
@@ -175,7 +175,7 @@ test("主 Agent 执行 prompt 会明确声明管理员全局提示词不可覆
|
||||
"管理员全局主提示词:\n系统级主提示词",
|
||||
"用户私有主提示词:\n用户私有主提示词",
|
||||
"当前对话附加提示词:\n当前对话提示词",
|
||||
"项目记忆:\n- [boss-console] boss 项目进度: boss 项目当前按项目聚合加线程下钻展示。",
|
||||
"项目记忆:\n- [boss-main] boss 项目进度: boss 项目当前按项目聚合加线程下钻展示。",
|
||||
"当前消息:\n继续推进 boss 项目的会话归档逻辑",
|
||||
].join("\n\n"),
|
||||
);
|
||||
|
||||
@@ -11,6 +11,7 @@ let saveAiAccount: (typeof import("../src/lib/boss-data"))["saveAiAccount"];
|
||||
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSession"];
|
||||
let appendProjectMessages: (typeof import("../src/lib/boss-data"))["appendProjectMessages"];
|
||||
let AUTH_SESSION_COOKIE = "";
|
||||
|
||||
async function setup() {
|
||||
@@ -33,6 +34,7 @@ async function setup() {
|
||||
updateProjectAgentControls = data.updateProjectAgentControls;
|
||||
readState = data.readState;
|
||||
createAuthSession = data.createAuthSession;
|
||||
appendProjectMessages = data.appendProjectMessages;
|
||||
AUTH_SESSION_COOKIE = auth.AUTH_SESSION_COOKIE;
|
||||
}
|
||||
|
||||
@@ -77,6 +79,40 @@ test.beforeEach(async () => {
|
||||
await mkdir(runtimeRoot, { recursive: true });
|
||||
});
|
||||
|
||||
test("appendProjectMessages 可以一次写入用户消息和主 Agent 本地快反回复", async () => {
|
||||
const messages = await appendProjectMessages({
|
||||
projectId: "master-agent",
|
||||
messages: [
|
||||
{
|
||||
senderLabel: "Boss 超级管理员",
|
||||
body: "你现在是什么模型",
|
||||
kind: "text",
|
||||
},
|
||||
{
|
||||
sender: "master",
|
||||
senderLabel: "主 Agent · gpt-5.4-mini",
|
||||
body: "当前主 Agent 是快速反应模式。",
|
||||
kind: "text",
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
assert.equal(messages.length, 2);
|
||||
assert.equal(messages[0]?.sender, "user");
|
||||
assert.equal(messages[1]?.sender, "master");
|
||||
|
||||
const state = await readState();
|
||||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||||
assert.equal(
|
||||
masterProject?.messages.find((message) => message.id === messages[0]?.id)?.body,
|
||||
"你现在是什么模型",
|
||||
);
|
||||
assert.equal(
|
||||
masterProject?.messages.find((message) => message.id === messages[1]?.id)?.body,
|
||||
"当前主 Agent 是快速反应模式。",
|
||||
);
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/master-agent/messages 快速返回队列态并在异步实际回复时继承当前会话覆盖", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-master-agent-queue",
|
||||
@@ -165,6 +201,478 @@ test("POST /api/v1/projects/master-agent/messages 快速返回队列态并在异
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/master-agent/messages 在快速反应模式下会对简单问题走同步快路径", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-master-agent-fast-sync",
|
||||
label: "API 容灾",
|
||||
role: "api_fallback",
|
||||
provider: "openai_api",
|
||||
displayName: "OpenAI API 快反测试",
|
||||
model: "gpt-5.4",
|
||||
apiKey: "sk-test-openai-fast-sync",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "用于 master-agent 快速反应同步路径测试。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
modelOverride: "gpt-4.1",
|
||||
reasoningEffortOverride: "low",
|
||||
fastModelOverride: "gpt-4.1",
|
||||
deepModelOverride: "gpt-5.4",
|
||||
});
|
||||
|
||||
const fetchCalls: Array<{ url: string; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body ?? null;
|
||||
fetchCalls.push({ url: String(input), body });
|
||||
return new Response(JSON.stringify({ output_text: "快速反应正常。" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-master-agent-fast-sync",
|
||||
},
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "请说:快速反应正常。",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
masterReply?: { accountId?: string; requestId?: string; autoEscalated?: boolean } | null;
|
||||
replyMessage?: { sender?: string; body?: string } | null;
|
||||
};
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
assert.equal(payload.masterReply?.accountId, "openai-master-agent-fast-sync");
|
||||
assert.equal(payload.masterReply?.requestId, "req-master-agent-fast-sync");
|
||||
assert.equal(payload.masterReply?.autoEscalated, false);
|
||||
assert.equal(payload.replyMessage?.sender, "master");
|
||||
assert.match(payload.replyMessage?.body ?? "", /快速反应正常/);
|
||||
|
||||
const nextState = await readState();
|
||||
const queuedTask = nextState.masterAgentTasks.find((item) => item.requestText === "请说:快速反应正常。");
|
||||
assert.equal(queuedTask, undefined);
|
||||
const masterProject = nextState.projects.find((project) => project.id === "master-agent");
|
||||
const reply = masterProject?.messages.at(-1);
|
||||
assert.ok(reply, "expected the sync reply to be written back immediately");
|
||||
assert.match(reply?.body ?? "", /快速反应正常/);
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
const requestBody = fetchCalls[0]?.body as {
|
||||
model?: string;
|
||||
reasoning?: { effort?: string };
|
||||
};
|
||||
assert.equal(requestBody?.model, "gpt-4.1");
|
||||
assert.equal(requestBody?.reasoning?.effort, "low");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/master-agent/messages 对模型状态类问题会本地秒回且不调用模型", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-local-status",
|
||||
label: "主 GPT",
|
||||
role: "primary",
|
||||
provider: "master_codex_node",
|
||||
displayName: "在线 Master Codex Node",
|
||||
nodeId: "mac-studio",
|
||||
nodeLabel: "Mac Studio",
|
||||
model: "gpt-5.4",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "在线主节点。",
|
||||
});
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "hyzq-fast-local-status",
|
||||
label: "环宇快反",
|
||||
role: "backup",
|
||||
provider: "hyzq_api",
|
||||
displayName: "环宇智擎 API",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "hyzq-fast-local-status-key",
|
||||
enabled: true,
|
||||
setActive: false,
|
||||
loginStatusNote: "环宇智擎快反账号。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
modelOverride: "gpt-5.4-mini",
|
||||
reasoningEffortOverride: "low",
|
||||
fastModelOverride: "gpt-5.4-mini",
|
||||
deepModelOverride: "gpt-5.4",
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalled = true;
|
||||
throw new Error("model call should not happen for local status replies");
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "你现在是什么模型",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
masterReply?: { accountId?: string; requestId?: string; effectiveModel?: string } | null;
|
||||
replyMessage?: { sender?: string; body?: string } | null;
|
||||
};
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
assert.equal(payload.masterReply?.requestId, "local-fast-path");
|
||||
assert.equal(payload.masterReply?.effectiveModel, "gpt-5.4-mini");
|
||||
assert.equal(payload.replyMessage?.sender, "master");
|
||||
assert.match(payload.replyMessage?.body ?? "", /gpt-5.4-mini/);
|
||||
assert.equal(fetchCalled, false);
|
||||
|
||||
const nextState = await readState();
|
||||
const reply = nextState.projects.find((project) => project.id === "master-agent")?.messages.at(-1);
|
||||
assert.match(reply?.body ?? "", /gpt-5.4-mini/);
|
||||
assert.match(reply?.body ?? "", /快速反应/);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/master-agent/messages 对可用模型查询会本地秒回并返回模式配置", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "hyzq-fast-local-list",
|
||||
label: "环宇快反",
|
||||
role: "primary",
|
||||
provider: "hyzq_api",
|
||||
displayName: "环宇智擎 API",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "hyzq-fast-local-list-key",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "环宇智擎快反账号。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
modelOverride: "gpt-5.4-mini",
|
||||
reasoningEffortOverride: "low",
|
||||
fastModelOverride: "gpt-5.4-mini",
|
||||
deepModelOverride: "gpt-5.4",
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalled = true;
|
||||
throw new Error("model call should not happen for local model listing replies");
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "有哪些模型可用",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
masterReply?: { requestId?: string } | null;
|
||||
replyMessage?: { body?: string } | null;
|
||||
};
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
assert.equal(payload.masterReply?.requestId, "local-fast-path");
|
||||
assert.match(payload.replyMessage?.body ?? "", /gpt-5\.4-mini/);
|
||||
assert.match(payload.replyMessage?.body ?? "", /gpt-5\.4/);
|
||||
assert.match(payload.replyMessage?.body ?? "", /gpt-5\.1/);
|
||||
assert.match(payload.replyMessage?.body ?? "", /gpt-4\.1/);
|
||||
assert.equal(fetchCalled, false);
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/master-agent/messages 对深度思考切换请求会本地更新 controls 并秒回", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "hyzq-fast-local-switch",
|
||||
label: "环宇快反",
|
||||
role: "primary",
|
||||
provider: "hyzq_api",
|
||||
displayName: "环宇智擎 API",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "hyzq-fast-local-switch-key",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "环宇智擎快反账号。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
modelOverride: "gpt-5.4-mini",
|
||||
reasoningEffortOverride: "low",
|
||||
fastModelOverride: "gpt-5.4-mini",
|
||||
deepModelOverride: "gpt-5.4",
|
||||
});
|
||||
|
||||
const originalFetch = globalThis.fetch;
|
||||
let fetchCalled = false;
|
||||
globalThis.fetch = (async () => {
|
||||
fetchCalled = true;
|
||||
throw new Error("model call should not happen for local mode switching replies");
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "切到深度思考",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
masterReply?: { requestId?: string; effectiveModel?: string; effectiveReasoningEffort?: string } | null;
|
||||
replyMessage?: { body?: string } | null;
|
||||
};
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
assert.equal(payload.masterReply?.requestId, "local-fast-path");
|
||||
assert.equal(payload.masterReply?.effectiveModel, "gpt-5.4");
|
||||
assert.equal(payload.masterReply?.effectiveReasoningEffort, "high");
|
||||
assert.match(payload.replyMessage?.body ?? "", /深度思考/);
|
||||
assert.match(payload.replyMessage?.body ?? "", /gpt-5\.4/);
|
||||
assert.equal(fetchCalled, false);
|
||||
|
||||
const controls = await readState().then((state) =>
|
||||
state.userProjectAgentControls.find(
|
||||
(entry) => entry.projectId === "master-agent" && entry.account === "17600003315",
|
||||
)?.controls,
|
||||
);
|
||||
assert.equal(controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(controls?.reasoningEffortOverride, "high");
|
||||
assert.equal(controls?.fastModelOverride, "gpt-5.4-mini");
|
||||
assert.equal(controls?.deepModelOverride, "gpt-5.4");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/master-agent/messages 在主节点在线时也会优先用环宇智擎执行快速反应", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-online-fast",
|
||||
label: "主 GPT",
|
||||
role: "primary",
|
||||
provider: "master_codex_node",
|
||||
displayName: "在线 Master Codex Node",
|
||||
nodeId: "mac-studio",
|
||||
nodeLabel: "Mac Studio",
|
||||
model: "gpt-5.4",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "在线主节点。",
|
||||
});
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "hyzq-fast-backup",
|
||||
label: "环宇快反",
|
||||
role: "backup",
|
||||
provider: "hyzq_api",
|
||||
displayName: "环宇智擎 API",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "hyzq-fast-key",
|
||||
enabled: true,
|
||||
setActive: false,
|
||||
loginStatusNote: "环宇智擎快反账号。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
modelOverride: "gpt-5.4-mini",
|
||||
reasoningEffortOverride: "low",
|
||||
fastModelOverride: "gpt-5.4-mini",
|
||||
deepModelOverride: "gpt-5.4",
|
||||
});
|
||||
|
||||
const fetchCalls: Array<{ url: string; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body ?? null;
|
||||
fetchCalls.push({ url: String(input), body });
|
||||
return new Response(JSON.stringify({ output_text: "环宇快反正常。" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-master-agent-hyzq-fast",
|
||||
},
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "请说:环宇快反正常。",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
masterReply?: { accountId?: string; requestId?: string } | null;
|
||||
};
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task ?? null, null);
|
||||
assert.equal(payload.masterReplyState, "completed");
|
||||
assert.equal(payload.masterReply?.accountId, "hyzq-fast-backup");
|
||||
assert.equal(payload.masterReply?.requestId, "req-master-agent-hyzq-fast");
|
||||
|
||||
const nextState = await readState();
|
||||
const queuedTask = nextState.masterAgentTasks.find((item) => item.requestText === "请说:环宇快反正常。");
|
||||
assert.equal(queuedTask, undefined);
|
||||
const reply = nextState.projects.find((project) => project.id === "master-agent")?.messages.at(-1);
|
||||
assert.match(reply?.body ?? "", /环宇快反正常/);
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0]?.url, "https://api.hyzq2046.com/v1/responses");
|
||||
const requestBody = fetchCalls[0]?.body as {
|
||||
model?: string;
|
||||
reasoning?: { effort?: string };
|
||||
};
|
||||
assert.equal(requestBody?.model, "gpt-5.4-mini");
|
||||
assert.equal(requestBody?.reasoning?.effort, "low");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/master-agent/messages 在主节点在线时复杂快反请求会自动升档到环宇智擎深度队列", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-online-deep",
|
||||
label: "主 GPT",
|
||||
role: "primary",
|
||||
provider: "master_codex_node",
|
||||
displayName: "在线 Master Codex Node",
|
||||
nodeId: "mac-studio",
|
||||
nodeLabel: "Mac Studio",
|
||||
model: "gpt-5.4",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "在线主节点。",
|
||||
});
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "hyzq-deep-backup",
|
||||
label: "环宇深思",
|
||||
role: "backup",
|
||||
provider: "hyzq_api",
|
||||
displayName: "环宇智擎 API",
|
||||
model: "gpt-5.4-mini",
|
||||
apiKey: "hyzq-deep-key",
|
||||
enabled: true,
|
||||
setActive: false,
|
||||
loginStatusNote: "环宇智擎深思账号。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
modelOverride: "gpt-5.4-mini",
|
||||
reasoningEffortOverride: "low",
|
||||
fastModelOverride: "gpt-5.4-mini",
|
||||
deepModelOverride: "gpt-5.4",
|
||||
});
|
||||
|
||||
const fetchCalls: Array<{ url: string; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body ?? null;
|
||||
fetchCalls.push({ url: String(input), body });
|
||||
return new Response(JSON.stringify({ output_text: "已经升档到环宇深度思考。" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-master-agent-hyzq-deep",
|
||||
},
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "请深入分析主 Agent 快速反应链路的性能瓶颈,并给出实现方案、风险和回归测试建议。",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string; taskType: string; status: string } | null;
|
||||
masterReplyState?: "queued" | "running" | "completed" | null;
|
||||
masterReply?: { accountId?: string; autoEscalated?: boolean } | null;
|
||||
};
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.masterReplyState, "queued");
|
||||
assert.equal(payload.masterReply?.accountId, "hyzq-deep-backup");
|
||||
assert.equal(payload.masterReply?.autoEscalated, true);
|
||||
assert.equal(payload.task?.taskType, "conversation_reply");
|
||||
|
||||
await waitFor(async () => {
|
||||
const state = await readState();
|
||||
const task = state.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
|
||||
return task?.status === "completed";
|
||||
});
|
||||
|
||||
const nextState = await readState();
|
||||
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
|
||||
assert.equal(task?.accountId, "hyzq-deep-backup");
|
||||
assert.equal(task?.deviceId, "master-agent-openai");
|
||||
assert.equal(task?.replyBody, "已经升档到环宇深度思考。");
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
assert.equal(fetchCalls[0]?.url, "https://api.hyzq2046.com/v1/responses");
|
||||
const requestBody = fetchCalls[0]?.body as {
|
||||
model?: string;
|
||||
reasoning?: { effort?: string };
|
||||
};
|
||||
assert.equal(requestBody?.model, "gpt-5.4");
|
||||
assert.equal(requestBody?.reasoning?.effort, "high");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("master-agent enqueue 在主节点离线时会自动切到 OpenAI 后台队列而不是挂到本机设备队列", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-offline",
|
||||
|
||||
17
tests/master-agent-model-options.test.ts
Normal file
17
tests/master-agent-model-options.test.ts
Normal file
@@ -0,0 +1,17 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { getMasterAgentModelOptions } from "../src/lib/master-agent-model-options";
|
||||
|
||||
test("主 Agent 模型选项会明确暴露快反和深思模型", () => {
|
||||
assert.deepEqual(
|
||||
getMasterAgentModelOptions(),
|
||||
["gpt-5.4-mini", "gpt-5.4", "gpt-5.1", "gpt-4.1"],
|
||||
);
|
||||
});
|
||||
|
||||
test("主 Agent 模型选项会保留当前自定义模型", () => {
|
||||
assert.deepEqual(
|
||||
getMasterAgentModelOptions("gpt-4.1-mini"),
|
||||
["gpt-4.1-mini", "gpt-5.4-mini", "gpt-5.4", "gpt-5.1", "gpt-4.1"],
|
||||
);
|
||||
});
|
||||
@@ -9,6 +9,7 @@ let replyToMasterAgentUserMessage: (typeof import("../src/lib/boss-master-agent"
|
||||
let saveAiAccount: (typeof import("../src/lib/boss-data"))["saveAiAccount"];
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let updateAiAccountHealth: (typeof import("../src/lib/boss-data"))["updateAiAccountHealth"];
|
||||
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
@@ -26,6 +27,18 @@ async function setup() {
|
||||
saveAiAccount = data.saveAiAccount;
|
||||
readState = data.readState;
|
||||
updateAiAccountHealth = data.updateAiAccountHealth;
|
||||
updateProjectAgentControls = data.updateProjectAgentControls;
|
||||
}
|
||||
|
||||
async function waitFor(predicate: () => Promise<boolean>, timeoutMs = 5_000) {
|
||||
const startedAt = Date.now();
|
||||
while (Date.now() - startedAt < timeoutMs) {
|
||||
if (await predicate()) {
|
||||
return;
|
||||
}
|
||||
await new Promise((resolve) => setTimeout(resolve, 50));
|
||||
}
|
||||
throw new Error("waitFor timed out");
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
@@ -322,6 +335,74 @@ test("replyToMasterAgentUserMessage retries the next ready API backup when the f
|
||||
}
|
||||
});
|
||||
|
||||
test("replyToMasterAgentUserMessage 在快速反应模式遇到复杂请求时会自动切到深度思考模型并排队执行", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "openai-primary-smart-upgrade",
|
||||
label: "OpenAI 主控",
|
||||
role: "primary",
|
||||
provider: "openai_api",
|
||||
displayName: "OpenAI 主账号",
|
||||
model: "gpt-5.4",
|
||||
apiKey: "sk-openai-smart-upgrade",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "主 OpenAI 账号。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
modelOverride: "gpt-4.1",
|
||||
reasoningEffortOverride: "low",
|
||||
fastModelOverride: "gpt-4.1",
|
||||
deepModelOverride: "gpt-5.4",
|
||||
});
|
||||
|
||||
const fetchCalls: Array<{ url: string; body: unknown }> = [];
|
||||
const originalFetch = globalThis.fetch;
|
||||
globalThis.fetch = (async (input, init) => {
|
||||
const body = typeof init?.body === "string" ? JSON.parse(init.body) : init?.body ?? null;
|
||||
fetchCalls.push({ url: String(input), body });
|
||||
return new Response(JSON.stringify({ output_text: "已经自动切到深度思考模型。" }), {
|
||||
status: 200,
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
"x-request-id": "req-master-smart-upgrade",
|
||||
},
|
||||
});
|
||||
}) as typeof fetch;
|
||||
|
||||
try {
|
||||
const result = await replyToMasterAgentUserMessage({
|
||||
requestMessageId: "msg-master-smart-upgrade",
|
||||
requestText: "请深入分析当前主 Agent 架构,并给出分阶段实现方案、风险和回归测试建议。",
|
||||
requestedBy: "Boss 超级管理员",
|
||||
requestedByAccount: "17600003315",
|
||||
mode: "smart",
|
||||
});
|
||||
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(result.accountId, "openai-primary-smart-upgrade");
|
||||
assert.equal(result.masterReplyState, "queued");
|
||||
assert.equal(result.autoEscalated, true);
|
||||
assert.ok(result.taskId, "expected a queued task after smart deep-upgrade");
|
||||
|
||||
await waitFor(async () => {
|
||||
const state = await readState();
|
||||
const task = state.masterAgentTasks.find((item) => item.taskId === result.taskId);
|
||||
return task?.status === "completed";
|
||||
});
|
||||
|
||||
assert.equal(fetchCalls.length, 1);
|
||||
const requestBody = fetchCalls[0]?.body as {
|
||||
model?: string;
|
||||
reasoning?: { effort?: string };
|
||||
};
|
||||
assert.equal(requestBody?.model, "gpt-5.4");
|
||||
assert.equal(requestBody?.reasoning?.effort, "high");
|
||||
} finally {
|
||||
globalThis.fetch = originalFetch;
|
||||
}
|
||||
});
|
||||
|
||||
test("replyToMasterAgentUserMessage falls back to a ready backup master node account when API backends are unavailable", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-offline",
|
||||
|
||||
@@ -12,8 +12,10 @@ let updateMasterAgentPromptPolicy: (typeof import("../src/lib/boss-data"))["upda
|
||||
let updateUserMasterPrompt: (typeof import("../src/lib/boss-data"))["updateUserMasterPrompt"];
|
||||
let createUserMasterMemory: (typeof import("../src/lib/boss-data"))["createUserMasterMemory"];
|
||||
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
||||
let appendProjectMessage: (typeof import("../src/lib/boss-data"))["appendProjectMessage"];
|
||||
let resolveMasterAgentExecutionConfig: (typeof import("../src/lib/boss-master-agent"))["resolveMasterAgentExecutionConfig"];
|
||||
let replyToMasterAgentUserMessage: (typeof import("../src/lib/boss-master-agent"))["replyToMasterAgentUserMessage"];
|
||||
let completeMasterAgentTask: (typeof import("../src/lib/boss-data"))["completeMasterAgentTask"];
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
@@ -34,8 +36,10 @@ async function setup() {
|
||||
updateUserMasterPrompt = data.updateUserMasterPrompt;
|
||||
createUserMasterMemory = data.createUserMasterMemory;
|
||||
updateProjectAgentControls = data.updateProjectAgentControls;
|
||||
appendProjectMessage = data.appendProjectMessage;
|
||||
resolveMasterAgentExecutionConfig = masterAgent.resolveMasterAgentExecutionConfig;
|
||||
replyToMasterAgentUserMessage = masterAgent.replyToMasterAgentUserMessage;
|
||||
completeMasterAgentTask = data.completeMasterAgentTask;
|
||||
}
|
||||
|
||||
test.after(async () => {
|
||||
@@ -377,3 +381,87 @@ test("主 Agent 执行 prompt 在没有线程状态文档和进展事件时才
|
||||
assert.ok(queuedTask?.executionPrompt.includes("关键时刻深拉线程兜底:"));
|
||||
assert.ok(queuedTask?.executionPrompt.includes("深拉兜底目标"));
|
||||
});
|
||||
|
||||
test("项目理解同步 prompt 强制线程先基于本地文档和代码汇总,并允许回写版本记录摘要", async () => {
|
||||
await setup();
|
||||
|
||||
const state = await readState();
|
||||
state.projects.push({
|
||||
id: "understanding-sync-thread",
|
||||
name: "项目理解同步线程",
|
||||
pinned: false,
|
||||
deviceIds: ["mac-studio"],
|
||||
preview: "等待同步",
|
||||
updatedAt: "2026-04-04T18:00:00+08:00",
|
||||
lastMessageAt: "2026-04-04T18:00:00+08:00",
|
||||
isGroup: false,
|
||||
threadMeta: {
|
||||
projectId: "understanding-sync-thread",
|
||||
threadId: "thread-understanding-sync",
|
||||
threadDisplayName: "项目理解同步线程",
|
||||
folderName: "理解同步",
|
||||
activityIconCount: 1,
|
||||
updatedAt: "2026-04-04T18:00:00+08:00",
|
||||
lastObservedCodexActivityAt: "2026-04-04T18:00:00+08:00",
|
||||
codexThreadRef: "thread-understanding-sync",
|
||||
codexFolderRef: "understanding-sync-folder",
|
||||
},
|
||||
groupMembers: [],
|
||||
createdByAgent: true,
|
||||
collaborationMode: "development",
|
||||
approvalState: "not_required",
|
||||
unreadCount: 0,
|
||||
riskLevel: "low",
|
||||
messages: [],
|
||||
goals: [],
|
||||
versions: [],
|
||||
});
|
||||
const project = state.projects.find((item) => item.id === "understanding-sync-thread");
|
||||
assert.ok(project, "expected seeded understanding-sync-thread project");
|
||||
project!.versions = [];
|
||||
project!.threadMeta.lastObservedCodexActivityAt = "2026-04-04T18:00:00+08:00";
|
||||
await writeState(state);
|
||||
await updateProjectAgentControls("understanding-sync-thread", {
|
||||
takeoverEnabled: true,
|
||||
});
|
||||
|
||||
await appendProjectMessage({
|
||||
projectId: "understanding-sync-thread",
|
||||
sender: "device",
|
||||
senderLabel: "项目理解同步线程",
|
||||
body: "已根据本地开发文档补齐线程状态同步。",
|
||||
kind: "text",
|
||||
});
|
||||
const queuedTask = (await readState()).masterAgentTasks.find(
|
||||
(task) =>
|
||||
task.projectId === "master-agent" &&
|
||||
task.projectUnderstandingTargetProjectId === "understanding-sync-thread",
|
||||
);
|
||||
assert.ok(queuedTask, "expected project understanding sync task");
|
||||
assert.match(queuedTask!.executionPrompt, /先基于当前项目本地可见的开发文档和实际代码进行汇总/);
|
||||
assert.match(queuedTask!.executionPrompt, /优先检查 README、docs、架构文档、版本记录和最近改动的关键代码文件/);
|
||||
assert.match(queuedTask!.executionPrompt, /"versionRecord": "一句中文版本记录摘要"/);
|
||||
|
||||
await completeMasterAgentTask({
|
||||
taskId: queuedTask!.taskId,
|
||||
deviceId: "mac-studio",
|
||||
status: "completed",
|
||||
replyBody: JSON.stringify({
|
||||
projectGoal: "完成审计对话线程状态同步",
|
||||
currentProgress: "已切到线程状态文档优先",
|
||||
technicalArchitecture: "Next.js 控制面配合 Android 原生客户端",
|
||||
currentBlockers: "",
|
||||
recommendedNextStep: "继续补强版本记录同步",
|
||||
versionRecord: "项目理解同步已改成先读本地文档和代码,再回写结构化摘要。",
|
||||
}),
|
||||
});
|
||||
|
||||
const refreshed = await readState();
|
||||
const refreshedProject = refreshed.projects.find((item) => item.id === "understanding-sync-thread");
|
||||
assert.ok(refreshedProject, "expected refreshed project");
|
||||
assert.equal(
|
||||
refreshedProject!.projectUnderstanding?.projectGoal,
|
||||
"完成审计对话线程状态同步",
|
||||
);
|
||||
assert.equal(refreshedProject!.versions[0]?.summary, "项目理解同步已改成先读本地文档和代码,再回写结构化摘要。");
|
||||
});
|
||||
|
||||
@@ -10,6 +10,9 @@ let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
|
||||
let toggleGoal: (typeof import("../src/lib/boss-data"))["toggleGoal"];
|
||||
let updateGoalText: (typeof import("../src/lib/boss-data"))["updateGoalText"];
|
||||
let createGoal: (typeof import("../src/lib/boss-data"))["createGoal"];
|
||||
let updateProjectAgentControls: (typeof import("../src/lib/boss-data"))["updateProjectAgentControls"];
|
||||
let completeMasterAgentTask: (typeof import("../src/lib/boss-data"))["completeMasterAgentTask"];
|
||||
let forceProjectUnderstandingSyncTask: (typeof import("../src/lib/boss-data"))["forceProjectUnderstandingSyncTask"];
|
||||
let subscribeBossEvents: (typeof import("../src/lib/boss-events"))["subscribeBossEvents"];
|
||||
|
||||
async function setup() {
|
||||
@@ -28,6 +31,9 @@ async function setup() {
|
||||
toggleGoal = data.toggleGoal;
|
||||
updateGoalText = data.updateGoalText;
|
||||
createGoal = data.createGoal;
|
||||
updateProjectAgentControls = data.updateProjectAgentControls;
|
||||
completeMasterAgentTask = data.completeMasterAgentTask;
|
||||
forceProjectUnderstandingSyncTask = data.forceProjectUnderstandingSyncTask;
|
||||
subscribeBossEvents = events.subscribeBossEvents;
|
||||
}
|
||||
|
||||
@@ -111,3 +117,56 @@ test("createGoal publishes project goal refresh marker for the project", async (
|
||||
assert.equal(latest.payload.projectId, "project-goal-events");
|
||||
assert.equal(latest.payload.note, "project_goals.updated");
|
||||
});
|
||||
|
||||
test("project understanding sync completion also publishes project goal refresh marker", async () => {
|
||||
const events: Array<{ event: string; payload: { projectId?: string; note?: string } }> = [];
|
||||
const unsubscribe = subscribeBossEvents((event, payload) => {
|
||||
events.push({ event, payload });
|
||||
});
|
||||
|
||||
const state = await readState();
|
||||
const existingProject = state.projects.find((project) => project.id !== "master-agent");
|
||||
assert.ok(existingProject);
|
||||
const project = structuredClone(existingProject);
|
||||
project.id = "project-goal-understanding-sync";
|
||||
project.name = "项目目标理解同步测试";
|
||||
project.goals = [];
|
||||
project.versions = [];
|
||||
project.messages = [];
|
||||
project.lastMessageAt = "2026-04-07T10:00:00.000Z";
|
||||
project.threadMeta.lastObservedCodexActivityAt = "2026-04-07T10:00:00.000Z";
|
||||
state.projects = state.projects.filter((item) => item.id !== project.id);
|
||||
state.projects.unshift(project);
|
||||
await writeState(state);
|
||||
await updateProjectAgentControls(project.id, { takeoverEnabled: true });
|
||||
|
||||
const queuedTask = await forceProjectUnderstandingSyncTask({
|
||||
projectId: project.id,
|
||||
observedActivityAt: "2026-04-07T10:00:00.000Z",
|
||||
reason: "thread_reply",
|
||||
});
|
||||
assert.ok(queuedTask);
|
||||
|
||||
await completeMasterAgentTask({
|
||||
taskId: queuedTask!.taskId,
|
||||
deviceId: "mac-studio",
|
||||
status: "completed",
|
||||
replyBody: JSON.stringify({
|
||||
projectGoal: "完成项目目标和版本记录自动同步",
|
||||
currentProgress: "项目目标页需要展示主 Agent 最新核对结果",
|
||||
technicalArchitecture: "Boss Web 与 Android 共用文件账本状态",
|
||||
currentBlockers: "",
|
||||
recommendedNextStep: "继续优化聊天阅读排版",
|
||||
versionRecord: "已补项目目标/版本记录的自动同步链路。",
|
||||
}),
|
||||
});
|
||||
unsubscribe();
|
||||
|
||||
const goalRefreshEvent = events.find(
|
||||
(item) =>
|
||||
item.event === "conversation.updated" &&
|
||||
item.payload.projectId === project.id &&
|
||||
item.payload.note === "project_goals.updated",
|
||||
);
|
||||
assert.ok(goalRefreshEvent, "expected project understanding sync to publish a goal refresh marker");
|
||||
});
|
||||
|
||||
@@ -59,8 +59,36 @@ test("project conversation pages wire project-scoped realtime refresh", async ()
|
||||
);
|
||||
assert.match(
|
||||
versionsPage,
|
||||
/conversationUpdatedNotes=\{\["project_goals\.updated"\]\}/,
|
||||
"expected versions page to refresh only on project goal markers",
|
||||
/conversationUpdatedNotes=\{\["project_versions\.updated"\]\}/,
|
||||
"expected versions page to refresh only on project version markers",
|
||||
);
|
||||
});
|
||||
|
||||
test("project goal and version pages expose compact synced summaries", async () => {
|
||||
const [goalsPage, versionsPage] = await Promise.all([
|
||||
readWorkspaceFile("src/app/conversations/[projectId]/goals/page.tsx"),
|
||||
readWorkspaceFile("src/app/conversations/[projectId]/versions/page.tsx"),
|
||||
]);
|
||||
|
||||
assert.match(
|
||||
goalsPage,
|
||||
/project\.projectUnderstanding\?\.projectGoal/,
|
||||
"expected goals page to surface the latest synced project goal summary",
|
||||
);
|
||||
assert.match(
|
||||
goalsPage,
|
||||
/project\.projectUnderstanding\?\.currentProgress/,
|
||||
"expected goals page to surface the latest synced project progress summary",
|
||||
);
|
||||
assert.match(
|
||||
goalsPage,
|
||||
/project\.projectUnderstanding\?\.recommendedNextStep/,
|
||||
"expected goals page to surface the latest synced recommended next step",
|
||||
);
|
||||
assert.match(
|
||||
versionsPage,
|
||||
/PageNav title="版本记录"/,
|
||||
"expected versions page to use the compact title copy",
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -12,6 +12,7 @@ let createAuthSession: (typeof import("../src/lib/boss-data"))["createAuthSessio
|
||||
let readState: (typeof import("../src/lib/boss-data"))["readState"];
|
||||
let writeState: (typeof import("../src/lib/boss-data"))["writeState"];
|
||||
let AUTH_SESSION_COOKIE = "";
|
||||
const TEST_ACCOUNT = "17600003315";
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) {
|
||||
@@ -45,7 +46,7 @@ test.after(async () => {
|
||||
|
||||
async function createAuthedRequest(url: string, method: "POST", body: unknown) {
|
||||
const session = await createAuthSession({
|
||||
account: "17600003315",
|
||||
account: TEST_ACCOUNT,
|
||||
role: "highest_admin",
|
||||
displayName: "Boss 超级管理员",
|
||||
loginMethod: "password",
|
||||
@@ -120,10 +121,41 @@ async function ensureSingleThreadProject() {
|
||||
return findSingleThreadProject(nextState);
|
||||
}
|
||||
|
||||
async function setProjectTakeover(projectId: string, enabled: boolean) {
|
||||
const state = await readState();
|
||||
state.userProjectAgentControls = state.userProjectAgentControls.filter(
|
||||
(item) => !(item.projectId === projectId && item.account === TEST_ACCOUNT),
|
||||
);
|
||||
state.userProjectAgentControls.unshift({
|
||||
account: TEST_ACCOUNT,
|
||||
projectId,
|
||||
controls: {
|
||||
takeoverEnabled: enabled,
|
||||
updatedAt: enabled ? "2026-04-17T10:00:00.000Z" : "2026-04-17T10:10:00.000Z",
|
||||
},
|
||||
});
|
||||
await writeState(state);
|
||||
}
|
||||
|
||||
async function resetThreadExecutionState(projectId: string) {
|
||||
const state = await readState();
|
||||
const project = state.projects.find((item) => item.id === projectId);
|
||||
const targetDevice = project ? state.devices.find((device) => device.id === project.deviceIds[0]) : null;
|
||||
if (targetDevice) {
|
||||
targetDevice.preferredExecutionMode = "cli";
|
||||
}
|
||||
state.projectExecutionPolicies = state.projectExecutionPolicies.filter((policy) => policy.projectId !== projectId);
|
||||
state.userProjectAgentControls = state.userProjectAgentControls.filter(
|
||||
(item) => !(item.projectId === projectId && item.account === TEST_ACCOUNT),
|
||||
);
|
||||
await writeState(state);
|
||||
}
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages enqueues a conversation task for single-thread projects", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
|
||||
const response = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
@@ -157,17 +189,146 @@ test("POST /api/v1/projects/[projectId]/messages enqueues a conversation task fo
|
||||
assert.ok(task, "expected a queued conversation_reply task for the single-thread project");
|
||||
assert.equal(task?.targetProjectId, singleProject.id);
|
||||
assert.equal(task?.targetThreadId, singleProject.threadMeta.threadId);
|
||||
assert.equal(task?.targetCodexThreadRef, singleProject.threadMeta.codexThreadRef);
|
||||
assert.equal(task?.targetCodexFolderRef, singleProject.threadMeta.codexFolderRef);
|
||||
assert.ok(task?.executionPrompt?.includes("请同步一下当前阻塞情况"));
|
||||
assert.ok(task?.executionPrompt?.includes(singleProject.threadMeta.threadDisplayName));
|
||||
assert.ok(!task?.executionPrompt?.includes("threadProjectId:"), "thread prompt should not include project id labels");
|
||||
assert.ok(!task?.executionPrompt?.includes("folderName:"), "thread prompt should not include folder labels");
|
||||
assert.ok(!task?.executionPrompt?.includes("deviceIds:"), "thread prompt should not include device id labels");
|
||||
assert.equal(task?.relayViaMasterAgent, undefined);
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages routes takeover mode to master-agent conversation first", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, true);
|
||||
|
||||
const response = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
{ body: "请继续同步当前线程进展" },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
replyPresenter?: "thread" | "master";
|
||||
task?: { taskId: string; taskType: string; status: string } | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.replyPresenter, "master");
|
||||
assert.equal(payload.task?.taskType, "conversation_reply");
|
||||
|
||||
const nextState = await readState();
|
||||
const task = nextState.masterAgentTasks.find(
|
||||
(item) =>
|
||||
item.taskType === "conversation_reply" &&
|
||||
item.projectId === singleProject.id &&
|
||||
item.requestText === "请继续同步当前线程进展",
|
||||
);
|
||||
assert.ok(task, "expected a queued conversation_reply task for takeover mode");
|
||||
assert.equal(task?.relayViaMasterAgent, true);
|
||||
assert.equal(task?.targetProjectId, undefined);
|
||||
assert.equal(task?.targetThreadId, undefined);
|
||||
assert.equal(task?.targetCodexThreadRef, undefined);
|
||||
assert.equal(task?.targetCodexFolderRef, undefined);
|
||||
assert.ok(task?.executionPrompt?.includes("主 Agent"));
|
||||
assert.ok(task?.executionPrompt?.includes("协同接管"));
|
||||
assert.ok(task?.executionPrompt?.includes("先准确理解并确认用户意图"));
|
||||
});
|
||||
|
||||
test("takeover prompt asks master agent to sync verified goals and version records when user requests review", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, true);
|
||||
|
||||
const response = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
{ body: "核对一下项目目标和版本记录,确认后同步到顶部入口" },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string; taskType: string; status: string } | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task?.taskType, "conversation_reply");
|
||||
|
||||
const nextState = await readState();
|
||||
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
|
||||
assert.ok(task, "expected a queued takeover task");
|
||||
assert.equal(task?.relayViaMasterAgent, true);
|
||||
assert.match(task!.executionPrompt, /用户要求核对或更新项目目标、版本记录时/);
|
||||
assert.match(task!.executionPrompt, /先让当前线程基于本地开发文档和实际代码重新汇总/);
|
||||
assert.match(task!.executionPrompt, /自动同步到当前会话顶部的“项目目标”和“版本记录”入口/);
|
||||
|
||||
const understandingTask = nextState.masterAgentTasks.find(
|
||||
(item) =>
|
||||
item.projectId === "master-agent" &&
|
||||
item.projectUnderstandingTargetProjectId === singleProject.id &&
|
||||
item.status === "queued",
|
||||
);
|
||||
assert.ok(understandingTask, "expected a hidden project understanding sync task");
|
||||
assert.match(understandingTask!.executionPrompt, /先基于当前项目本地可见的开发文档和实际代码进行汇总/);
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages still lets takeover mode talk to master agent during gui conflict", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, true);
|
||||
|
||||
const state = await readState();
|
||||
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
|
||||
assert.ok(targetDevice, "expected a seeded target device");
|
||||
targetDevice.preferredExecutionMode = "gui";
|
||||
await writeState(state);
|
||||
|
||||
const response = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
{ body: "我先和你确认一下接下来怎么推进" },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
replyPresenter?: "thread" | "master";
|
||||
task?: { taskId: string; taskType: string; status: string } | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.replyPresenter, "master");
|
||||
assert.equal(payload.task?.taskType, "conversation_reply");
|
||||
|
||||
const nextState = await readState();
|
||||
const task = nextState.masterAgentTasks.find((item) => item.taskId === payload.task?.taskId);
|
||||
assert.ok(task, "expected takeover mode to queue a master-agent task");
|
||||
assert.equal(task?.relayViaMasterAgent, true);
|
||||
assert.equal(task?.targetProjectId, undefined);
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages blocks single-thread sends when the target device prefers gui mode", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await setProjectTakeover(singleProject.id, false);
|
||||
|
||||
const state = await readState();
|
||||
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
|
||||
@@ -227,6 +388,7 @@ test("POST /api/v1/projects/[projectId]/messages blocks single-thread sends when
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await setProjectTakeover(singleProject.id, false);
|
||||
|
||||
const state = await readState();
|
||||
const targetDevice = state.devices.find((device) => device.id === singleProject.deviceIds[0]);
|
||||
@@ -289,12 +451,139 @@ test("POST /api/v1/projects/[projectId]/messages blocks single-thread sends when
|
||||
assert.equal(queuedTask, undefined, "blocked send should not enqueue a conversation task");
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages blocks before queueing when recent codex activity exists without a stored policy", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, false);
|
||||
|
||||
const recentExternalActivityAt = new Date(Date.now() - 60_000).toISOString();
|
||||
const state = await readState();
|
||||
await writeState({
|
||||
...state,
|
||||
projects: state.projects.map((project) =>
|
||||
project.id === singleProject.id
|
||||
? {
|
||||
...project,
|
||||
threadMeta: {
|
||||
...project.threadMeta,
|
||||
lastObservedCodexActivityAt: recentExternalActivityAt,
|
||||
},
|
||||
}
|
||||
: project,
|
||||
),
|
||||
projectExecutionPolicies: state.projectExecutionPolicies.filter(
|
||||
(policy) => policy.projectId !== singleProject.id,
|
||||
),
|
||||
});
|
||||
|
||||
const response = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
{ body: "请看一下这个项目现在卡在哪里" },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
assert.equal(response.status, 409);
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
code?: string;
|
||||
executionConflict?: {
|
||||
projectId: string;
|
||||
preferredExecutionMode: "gui" | "cli";
|
||||
allowPolicy: "forbid" | "allow_once" | "allow_always";
|
||||
conflictState: "none" | "warning" | "blocked";
|
||||
reason: string;
|
||||
};
|
||||
};
|
||||
assert.equal(payload.ok, false);
|
||||
assert.equal(payload.code, "THREAD_EXECUTION_CONFLICT");
|
||||
assert.equal(payload.executionConflict?.projectId, singleProject.id);
|
||||
assert.equal(payload.executionConflict?.preferredExecutionMode, "cli");
|
||||
assert.equal(payload.executionConflict?.allowPolicy, "forbid");
|
||||
assert.equal(payload.executionConflict?.conflictState, "blocked");
|
||||
assert.equal(payload.executionConflict?.reason, "project_conflict_forbid");
|
||||
|
||||
const nextState = await readState();
|
||||
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
|
||||
const blockedMessage = updatedProject?.messages.find((message) =>
|
||||
message.body.includes("请看一下这个项目现在卡在哪里"),
|
||||
);
|
||||
assert.equal(blockedMessage, undefined, "blocked send should not append a local chat message");
|
||||
const queuedTask = nextState.masterAgentTasks.find(
|
||||
(item) =>
|
||||
item.taskType === "conversation_reply" &&
|
||||
item.projectId === singleProject.id &&
|
||||
item.requestText === "请看一下这个项目现在卡在哪里",
|
||||
);
|
||||
assert.equal(queuedTask, undefined, "blocked send should not enqueue a conversation task");
|
||||
});
|
||||
|
||||
test("POST /api/v1/projects/[projectId]/messages ignores stale scoped conflict policies", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
|
||||
const staleExternalActivityAt = new Date(Date.now() - 30 * 60_000).toISOString();
|
||||
const state = await readState();
|
||||
await writeState({
|
||||
...state,
|
||||
projects: state.projects.map((project) =>
|
||||
project.id === singleProject.id
|
||||
? {
|
||||
...project,
|
||||
threadMeta: {
|
||||
...project.threadMeta,
|
||||
lastObservedCodexActivityAt: staleExternalActivityAt,
|
||||
},
|
||||
}
|
||||
: project,
|
||||
),
|
||||
projectExecutionPolicies: [
|
||||
...state.projectExecutionPolicies.filter((policy) => policy.projectId !== singleProject.id),
|
||||
{
|
||||
deviceId: singleProject.deviceIds[0],
|
||||
folderKey: buildProjectFolderKey(singleProject),
|
||||
projectId: singleProject.id,
|
||||
allowPolicy: "forbid" as const,
|
||||
conflictState: "blocked" as const,
|
||||
recentExternalActivityAt: staleExternalActivityAt,
|
||||
updatedAt: staleExternalActivityAt,
|
||||
},
|
||||
],
|
||||
});
|
||||
|
||||
const response = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
{ body: "继续同步这个线程" },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string; taskType: string; status: string } | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.task?.taskType, "conversation_reply");
|
||||
assert.equal(payload.task?.status, "queued");
|
||||
});
|
||||
|
||||
test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread reply back to the single-thread project", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, false);
|
||||
|
||||
await postMessageRoute(
|
||||
const sendResponse = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
@@ -302,13 +591,13 @@ test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread re
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
const sendPayload = (await sendResponse.json()) as {
|
||||
task?: { taskId: string };
|
||||
};
|
||||
|
||||
const queuedState = await readState();
|
||||
const task = queuedState.masterAgentTasks.find(
|
||||
(item) =>
|
||||
item.taskType === "conversation_reply" &&
|
||||
item.projectId === singleProject.id &&
|
||||
item.targetProjectId === singleProject.id,
|
||||
(item) => item.taskId === sendPayload.task?.taskId,
|
||||
);
|
||||
assert.ok(task, "expected a queued conversation_reply task");
|
||||
|
||||
@@ -337,10 +626,66 @@ test("POST /api/v1/master-agent/tasks/[taskId]/complete writes the raw thread re
|
||||
assert.equal(mirroredReply?.sender, "device");
|
||||
});
|
||||
|
||||
test("POST /api/v1/master-agent/tasks/[taskId]/complete writes takeover master replies to the current project", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, true);
|
||||
|
||||
const sendResponse = await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/projects/${singleProject.id}/messages`,
|
||||
"POST",
|
||||
{ body: "托管后请帮我问一下当前阻塞" },
|
||||
),
|
||||
{ params: Promise.resolve({ projectId: singleProject.id }) },
|
||||
);
|
||||
const sendPayload = (await sendResponse.json()) as {
|
||||
task?: { taskId: string };
|
||||
};
|
||||
|
||||
const queuedState = await readState();
|
||||
const task = queuedState.masterAgentTasks.find(
|
||||
(item) => item.taskId === sendPayload.task?.taskId,
|
||||
);
|
||||
assert.ok(task, "expected a queued conversation_reply task");
|
||||
assert.equal(task?.relayViaMasterAgent, true);
|
||||
assert.equal(task?.targetProjectId, undefined);
|
||||
assert.equal(task?.targetThreadId, undefined);
|
||||
|
||||
await setProjectTakeover(singleProject.id, false);
|
||||
|
||||
const response = await completeMasterTaskRoute(
|
||||
await createAuthedRequest(
|
||||
`http://127.0.0.1:3000/api/v1/master-agent/tasks/${task.taskId}/complete`,
|
||||
"POST",
|
||||
{
|
||||
deviceId: task.deviceId,
|
||||
status: "completed",
|
||||
replyBody: "我先确认一下:你是希望我梳理当前阻塞后,再协调目标线程继续推进,对吗?",
|
||||
},
|
||||
),
|
||||
{ params: Promise.resolve({ taskId: task.taskId }) },
|
||||
);
|
||||
assert.equal(response.status, 200);
|
||||
|
||||
const nextState = await readState();
|
||||
const updatedProject = nextState.projects.find((project) => project.id === singleProject.id);
|
||||
const relayedReply = updatedProject?.messages.find((message) =>
|
||||
message.body.includes("我先确认一下:你是希望我梳理当前阻塞后,再协调目标线程继续推进,对吗?"),
|
||||
);
|
||||
assert.ok(relayedReply, "expected a master reply to be written back to the current project");
|
||||
assert.equal(relayedReply?.sender, "master");
|
||||
assert.match(relayedReply?.senderLabel ?? "", /主 Agent/);
|
||||
});
|
||||
|
||||
test("POST /api/v1/master-agent/tasks/[taskId]/complete blocks leaked thread environment diagnostics from the chat transcript", async () => {
|
||||
await setup();
|
||||
const singleProject = await ensureSingleThreadProject();
|
||||
assert.ok(singleProject, "expected a seeded single-thread project");
|
||||
await resetThreadExecutionState(singleProject.id);
|
||||
await setProjectTakeover(singleProject.id, false);
|
||||
|
||||
await postMessageRoute(
|
||||
await createAuthedRequest(
|
||||
|
||||
@@ -111,7 +111,7 @@ test("thread status documents and progress events normalize, sort, and trim corr
|
||||
);
|
||||
});
|
||||
|
||||
test("thread replies append lightweight progress events without queuing a fresh understanding sync", async () => {
|
||||
test("thread replies append lightweight progress events and skip redundant understanding sync when status document is fresh", async () => {
|
||||
await setup();
|
||||
|
||||
const state = (await readState()) as MutableBossState;
|
||||
|
||||
Reference in New Issue
Block a user