786 lines
26 KiB
TypeScript
786 lines
26 KiB
TypeScript
import test from "node:test";
|
||
import assert from "node:assert/strict";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
||
|
||
let runtimeRoot = "";
|
||
let replyToMasterAgentUserMessage: (typeof import("../src/lib/boss-master-agent"))["replyToMasterAgentUserMessage"];
|
||
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"];
|
||
let updateDevice: (typeof import("../src/lib/boss-data"))["updateDevice"];
|
||
let upsertDeviceHeartbeat: (typeof import("../src/lib/boss-data"))["upsertDeviceHeartbeat"];
|
||
let completeMasterAgentTask: (typeof import("../src/lib/boss-data"))["completeMasterAgentTask"];
|
||
|
||
async function setup() {
|
||
if (runtimeRoot) return;
|
||
|
||
runtimeRoot = await mkdtemp(path.join(os.tmpdir(), "boss-master-agent-fallback-"));
|
||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||
|
||
const [masterAgent, data] = await Promise.all([
|
||
import("../src/lib/boss-master-agent.ts"),
|
||
import("../src/lib/boss-data.ts"),
|
||
]);
|
||
|
||
replyToMasterAgentUserMessage = masterAgent.replyToMasterAgentUserMessage;
|
||
saveAiAccount = data.saveAiAccount;
|
||
readState = data.readState;
|
||
updateAiAccountHealth = data.updateAiAccountHealth;
|
||
updateProjectAgentControls = data.updateProjectAgentControls;
|
||
updateDevice = data.updateDevice;
|
||
upsertDeviceHeartbeat = data.upsertDeviceHeartbeat;
|
||
completeMasterAgentTask = data.completeMasterAgentTask;
|
||
}
|
||
|
||
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 () => {
|
||
if (runtimeRoot) {
|
||
await rm(runtimeRoot, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test.beforeEach(async () => {
|
||
await setup();
|
||
await rm(runtimeRoot, { recursive: true, force: true });
|
||
await mkdir(runtimeRoot, { recursive: true });
|
||
});
|
||
|
||
test("replyToMasterAgentUserMessage falls back to a runnable OpenAI API account when the master node is offline", async () => {
|
||
await updateDevice("mac-studio", {
|
||
capabilities: {
|
||
gui: { connected: false },
|
||
cli: { connected: false },
|
||
codexAppServer: { connected: false },
|
||
},
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "master-codex-primary",
|
||
label: "主 GPT",
|
||
role: "primary",
|
||
provider: "master_codex_node",
|
||
displayName: "Mac 上的 Master Codex Node",
|
||
nodeId: "offline-node",
|
||
nodeLabel: "离线节点",
|
||
model: "gpt-5.4",
|
||
enabled: true,
|
||
setActive: true,
|
||
loginStatusNote: "通过绑定的 Master Codex Node 对话。",
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "openai-backup",
|
||
label: "备用 GPT",
|
||
role: "backup",
|
||
provider: "openai_api",
|
||
displayName: "OpenAI API 备用账号",
|
||
accountIdentifier: "sk-demo",
|
||
model: "gpt-5.4",
|
||
apiKey: "sk-live-demo-123456",
|
||
enabled: true,
|
||
setActive: false,
|
||
loginStatusNote: "备用 API 账号。",
|
||
});
|
||
|
||
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: "主Agent链路正常。" }), {
|
||
status: 200,
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-request-id": "req-master-fallback",
|
||
},
|
||
});
|
||
}
|
||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||
}) as typeof fetch;
|
||
|
||
try {
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-master-fallback",
|
||
requestText: "请只回复:主Agent链路正常。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "openai-backup");
|
||
assert.equal(result.requestId, "req-master-fallback");
|
||
|
||
const state = await readState();
|
||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||
const reply = masterProject?.messages.at(-1);
|
||
assert.ok(reply, "expected a master-agent reply to be appended");
|
||
assert.equal(reply?.sender, "master");
|
||
assert.equal(reply?.senderLabel, "主 Agent · gpt-5.4");
|
||
assert.match(reply?.body ?? "", /主Agent链路正常/);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("replyToMasterAgentUserMessage can retry the same degraded API account when it is the only available backend", async () => {
|
||
await saveAiAccount({
|
||
accountId: "master-codex-primary",
|
||
label: "主 GPT",
|
||
role: "primary",
|
||
provider: "master_codex_node",
|
||
displayName: "Mac 上的 Master Codex Node",
|
||
nodeId: "offline-node",
|
||
nodeLabel: "离线节点",
|
||
model: "gpt-5.4",
|
||
enabled: true,
|
||
setActive: false,
|
||
loginStatusNote: "测试中显式模拟默认主节点离线。",
|
||
});
|
||
await updateAiAccountHealth({
|
||
accountId: "master-codex-primary",
|
||
status: "degraded",
|
||
lastError: "MASTER_CODEX_NODE_DEVICE_OFFLINE",
|
||
lastValidatedAt: new Date().toISOString(),
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "openai-primary-degraded",
|
||
label: "OpenAI 主控",
|
||
role: "primary",
|
||
provider: "openai_api",
|
||
displayName: "OpenAI 主账号",
|
||
model: "gpt-5.4",
|
||
apiKey: "sk-openai-only",
|
||
enabled: true,
|
||
setActive: true,
|
||
loginStatusNote: "唯一可用的 OpenAI 账号。",
|
||
});
|
||
await updateAiAccountHealth({
|
||
accountId: "openai-primary-degraded",
|
||
status: "degraded",
|
||
lastError: "temporary failure",
|
||
lastValidatedAt: new Date().toISOString(),
|
||
});
|
||
|
||
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: "仍然可以重试同一个 API 账号。" }), {
|
||
status: 200,
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-request-id": "req-openai-degraded-retry",
|
||
},
|
||
});
|
||
}
|
||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||
}) as typeof fetch;
|
||
|
||
try {
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-openai-degraded-retry",
|
||
requestText: "请只回复:仍然可以重试同一个 API 账号。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "openai-primary-degraded");
|
||
|
||
const state = await readState();
|
||
const account = state.aiAccounts.find((item) => item.accountId === "openai-primary-degraded");
|
||
assert.equal(account?.status, "ready");
|
||
assert.equal(account?.isActive, true);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("replyToMasterAgentUserMessage uses active DeepSeek API accounts directly", async () => {
|
||
await saveAiAccount({
|
||
accountId: "deepseek-primary",
|
||
label: "DeepSeek 主控",
|
||
role: "primary",
|
||
provider: "deepseek_api",
|
||
displayName: "DeepSeek V4 主链路",
|
||
model: "deepseek-v4-pro",
|
||
apiKey: "sk-deepseek-demo-123456",
|
||
enabled: true,
|
||
setActive: true,
|
||
loginStatusNote: "DeepSeek API 主链路。",
|
||
});
|
||
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = (async (input) => {
|
||
if (typeof input === "string" && input === "https://api.deepseek.com/chat/completions") {
|
||
return new Response(JSON.stringify({
|
||
choices: [
|
||
{
|
||
message: {
|
||
content: "DeepSeek 主链路正常。",
|
||
},
|
||
},
|
||
],
|
||
}), {
|
||
status: 200,
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-request-id": "req-deepseek-primary",
|
||
},
|
||
});
|
||
}
|
||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||
}) as typeof fetch;
|
||
|
||
try {
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-deepseek-primary",
|
||
requestText: "请只回复:DeepSeek 主链路正常。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
mode: "wait",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "deepseek-primary");
|
||
assert.equal(result.requestId, "req-deepseek-primary");
|
||
|
||
const state = await readState();
|
||
const reply = state.projects.find((project) => project.id === "master-agent")?.messages.at(-1);
|
||
assert.equal(reply?.senderLabel, "主 Agent · deepseek-v4-pro");
|
||
assert.match(reply?.body ?? "", /DeepSeek 主链路正常/);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("replyToMasterAgentUserMessage falls back to a runnable aliyun qwen backup account when the master node is offline", async () => {
|
||
await updateDevice("mac-studio", {
|
||
capabilities: {
|
||
gui: { connected: false },
|
||
cli: { connected: false },
|
||
codexAppServer: { connected: false },
|
||
},
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "master-codex-primary",
|
||
label: "主 GPT",
|
||
role: "primary",
|
||
provider: "master_codex_node",
|
||
displayName: "Mac 上的 Master Codex Node",
|
||
nodeId: "offline-node",
|
||
nodeLabel: "离线节点",
|
||
model: "gpt-5.4",
|
||
enabled: true,
|
||
setActive: true,
|
||
loginStatusNote: "通过绑定的 Master Codex Node 对话。",
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "aliyun-qwen-backup",
|
||
label: "阿里备用",
|
||
role: "backup",
|
||
provider: "aliyun_qwen_api",
|
||
displayName: "阿里百炼备用账号",
|
||
accountIdentifier: "dashscope-demo",
|
||
model: "qwen3.5-plus",
|
||
apiKey: "sk-aliyun-demo-123456",
|
||
enabled: true,
|
||
setActive: false,
|
||
loginStatusNote: "阿里百炼 Qwen 备用账号。",
|
||
});
|
||
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = (async (input) => {
|
||
if (typeof input === "string" && input === "https://dashscope.aliyuncs.com/compatible-mode/v1/responses") {
|
||
return new Response(JSON.stringify({ output_text: "阿里备用链路正常。" }), {
|
||
status: 200,
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-request-id": "req-master-aliyun-fallback",
|
||
},
|
||
});
|
||
}
|
||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||
}) as typeof fetch;
|
||
|
||
try {
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-master-aliyun-fallback",
|
||
requestText: "请只回复:阿里备用链路正常。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "aliyun-qwen-backup");
|
||
assert.equal(result.requestId, "req-master-aliyun-fallback");
|
||
|
||
const state = await readState();
|
||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||
const reply = masterProject?.messages.at(-1);
|
||
assert.ok(reply, "expected a master-agent reply to be appended");
|
||
assert.equal(reply?.sender, "master");
|
||
assert.equal(reply?.senderLabel, "主 Agent · qwen3.5-plus");
|
||
assert.match(reply?.body ?? "", /阿里备用链路正常/);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("replyToMasterAgentUserMessage retries the next ready API backup when the first API backend call fails", async () => {
|
||
await saveAiAccount({
|
||
accountId: "openai-primary-ready",
|
||
label: "OpenAI 主控",
|
||
role: "primary",
|
||
provider: "openai_api",
|
||
displayName: "OpenAI 主账号",
|
||
model: "gpt-5.4",
|
||
apiKey: "sk-openai-primary",
|
||
enabled: true,
|
||
setActive: true,
|
||
loginStatusNote: "主 OpenAI 账号。",
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "aliyun-qwen-backup",
|
||
label: "阿里备用",
|
||
role: "backup",
|
||
provider: "aliyun_qwen_api",
|
||
displayName: "阿里百炼备用账号",
|
||
accountIdentifier: "dashscope-demo",
|
||
model: "qwen3.5-plus",
|
||
apiKey: "sk-aliyun-demo-123456",
|
||
enabled: true,
|
||
setActive: false,
|
||
loginStatusNote: "阿里百炼 Qwen 备用账号。",
|
||
});
|
||
|
||
const originalFetch = globalThis.fetch;
|
||
globalThis.fetch = (async (input) => {
|
||
if (typeof input === "string" && input === "https://api.openai.com/v1/responses") {
|
||
return new Response(JSON.stringify({ error: { message: "openai temporary failure" } }), {
|
||
status: 500,
|
||
headers: { "content-type": "application/json" },
|
||
});
|
||
}
|
||
if (typeof input === "string" && input === "https://dashscope.aliyuncs.com/compatible-mode/v1/responses") {
|
||
return new Response(JSON.stringify({ output_text: "阿里备用接管成功。" }), {
|
||
status: 200,
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-request-id": "req-master-api-chain",
|
||
},
|
||
});
|
||
}
|
||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||
}) as typeof fetch;
|
||
|
||
try {
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-master-api-chain",
|
||
requestText: "请只回复:阿里备用接管成功。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "aliyun-qwen-backup");
|
||
assert.equal(result.requestId, "req-master-api-chain");
|
||
|
||
const state = await readState();
|
||
const openaiAccount = state.aiAccounts.find((item) => item.accountId === "openai-primary-ready");
|
||
assert.equal(openaiAccount?.status, "degraded");
|
||
const aliyunAccount = state.aiAccounts.find((item) => item.accountId === "aliyun-qwen-backup");
|
||
assert.equal(aliyunAccount?.isActive, true);
|
||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||
const reply = masterProject?.messages.at(-1);
|
||
assert.ok(reply, "expected a fallback reply to be appended");
|
||
assert.match(reply?.body ?? "", /阿里备用接管成功/);
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
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: "krisolo",
|
||
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",
|
||
label: "主 GPT",
|
||
role: "primary",
|
||
provider: "master_codex_node",
|
||
displayName: "离线主节点",
|
||
nodeId: "offline-node",
|
||
nodeLabel: "离线节点",
|
||
model: "gpt-5.4",
|
||
enabled: true,
|
||
setActive: true,
|
||
loginStatusNote: "离线主节点。",
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "master-codex-backup-ready",
|
||
label: "备用主节点",
|
||
role: "backup",
|
||
provider: "master_codex_node",
|
||
displayName: "在线备用 Master Codex Node",
|
||
nodeId: "mac-studio",
|
||
nodeLabel: "Mac Studio",
|
||
model: "gpt-5.4",
|
||
enabled: true,
|
||
setActive: false,
|
||
loginStatusNote: "在线备用主节点。",
|
||
});
|
||
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-master-node-backup-fallback",
|
||
requestText: "请切到备用主节点。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
mode: "enqueue",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "master-codex-backup-ready");
|
||
assert.ok(result.taskId, "expected a queued master-agent task");
|
||
|
||
const state = await readState();
|
||
const task = state.masterAgentTasks.find((item) => item.taskId === result.taskId);
|
||
assert.ok(task, "expected queued task to be written into state");
|
||
assert.equal(task?.accountId, "master-codex-backup-ready");
|
||
assert.equal(task?.deviceId, "mac-studio");
|
||
});
|
||
|
||
test("replyToMasterAgentUserMessage routes to another bound Codex device when the active node has no usable Codex channel", async () => {
|
||
await updateDevice("mac-studio", {
|
||
status: "online",
|
||
capabilities: {
|
||
gui: { connected: false },
|
||
cli: { connected: false },
|
||
codexAppServer: { connected: false },
|
||
},
|
||
});
|
||
await upsertDeviceHeartbeat({
|
||
deviceId: "macbook-air-codex",
|
||
token: "boss-macbook-air-codex-token",
|
||
name: "MacBook Air",
|
||
avatar: "A",
|
||
account: "krisolo",
|
||
status: "online",
|
||
quota5h: 90,
|
||
quota7d: 90,
|
||
projects: [],
|
||
capabilities: {
|
||
gui: { connected: true },
|
||
cli: { connected: true },
|
||
codexAppServer: { connected: true },
|
||
},
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "master-codex-primary-no-channel",
|
||
label: "主 GPT",
|
||
role: "primary",
|
||
provider: "master_codex_node",
|
||
displayName: "在线但没有 Codex 通道的主节点",
|
||
nodeId: "mac-studio",
|
||
nodeLabel: "Mac Studio",
|
||
model: "gpt-5.4",
|
||
enabled: true,
|
||
setActive: true,
|
||
loginStatusNote: "模拟 boss-agent 未检测到可用 Codex 通道。",
|
||
});
|
||
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-master-node-device-pool",
|
||
requestText: "请使用可用的 Codex 设备池。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
mode: "enqueue",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "master-codex-device-macbook-air-codex");
|
||
assert.ok(result.taskId, "expected a queued master-agent task");
|
||
|
||
const state = await readState();
|
||
const task = state.masterAgentTasks.find((item) => item.taskId === result.taskId);
|
||
assert.ok(task, "expected queued task to be written into state");
|
||
assert.equal(task?.accountId, "master-codex-device-macbook-air-codex");
|
||
assert.equal(task?.deviceId, "macbook-air-codex");
|
||
});
|
||
|
||
test("replyToMasterAgentUserMessage falls back to API when no bound Codex model channel is usable", async () => {
|
||
await updateDevice("mac-studio", {
|
||
status: "online",
|
||
capabilities: {
|
||
gui: { connected: false },
|
||
cli: { connected: false },
|
||
codexAppServer: { connected: false },
|
||
},
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "master-codex-primary-no-model-channel",
|
||
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: "模拟没有任何可用 Codex 模型通道。",
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "openai-api-after-codex-pool",
|
||
label: "API 备用",
|
||
role: "api_fallback",
|
||
provider: "openai_api",
|
||
displayName: "OpenAI API 备用账号",
|
||
model: "gpt-5.4-mini",
|
||
apiKey: "sk-openai-after-codex-pool",
|
||
enabled: true,
|
||
setActive: false,
|
||
loginStatusNote: "Codex 设备池不可用时兜底。",
|
||
});
|
||
|
||
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: "已切到 API 备用链。" }), {
|
||
status: 200,
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-request-id": "req-api-after-codex-pool",
|
||
},
|
||
});
|
||
}
|
||
throw new Error(`unexpected fetch: ${String(input)}`);
|
||
}) as typeof fetch;
|
||
|
||
try {
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-api-after-codex-pool",
|
||
requestText: "Codex 设备都不可用时继续回复。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
mode: "enqueue",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "openai-api-after-codex-pool");
|
||
assert.ok(result.taskId, "expected an API fallback task");
|
||
|
||
const state = await readState();
|
||
const task = state.masterAgentTasks.find((item) => item.taskId === result.taskId);
|
||
assert.ok(task, "expected queued task to be written into state");
|
||
assert.equal(task?.accountId, "openai-api-after-codex-pool");
|
||
assert.equal(task?.deviceId, "master-agent-openai");
|
||
|
||
await waitFor(async () => {
|
||
const nextState = await readState();
|
||
const completedTask = nextState.masterAgentTasks.find((item) => item.taskId === result.taskId);
|
||
return completedTask?.status === "completed";
|
||
});
|
||
} finally {
|
||
globalThis.fetch = originalFetch;
|
||
}
|
||
});
|
||
|
||
test("failed Master Codex Node task is requeued to the next usable Codex device before surfacing an error", async () => {
|
||
await updateDevice("mac-studio", {
|
||
status: "online",
|
||
capabilities: {
|
||
gui: { connected: true },
|
||
cli: { connected: true },
|
||
codexAppServer: { connected: true },
|
||
},
|
||
});
|
||
await upsertDeviceHeartbeat({
|
||
deviceId: "macbook-air-codex-failover",
|
||
token: "boss-macbook-air-codex-failover-token",
|
||
name: "MacBook Air",
|
||
avatar: "A",
|
||
account: "krisolo",
|
||
status: "online",
|
||
quota5h: 90,
|
||
quota7d: 90,
|
||
projects: [],
|
||
capabilities: {
|
||
gui: { connected: true },
|
||
cli: { connected: true },
|
||
codexAppServer: { connected: true },
|
||
},
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "master-codex-primary-runtime-failover",
|
||
label: "主 GPT",
|
||
role: "primary",
|
||
provider: "master_codex_node",
|
||
displayName: "Mac Studio Master Codex Node",
|
||
nodeId: "mac-studio",
|
||
nodeLabel: "Mac Studio",
|
||
model: "gpt-5.4",
|
||
enabled: true,
|
||
setActive: true,
|
||
loginStatusNote: "首选 Master Codex Node。",
|
||
});
|
||
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-master-node-runtime-failover",
|
||
requestText: "请走主节点,如果失败自动切下一台。",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
mode: "enqueue",
|
||
});
|
||
|
||
assert.equal(result.ok, true);
|
||
assert.equal(result.accountId, "master-codex-primary-runtime-failover");
|
||
assert.ok(result.taskId, "expected a queued master-agent task");
|
||
|
||
const requeued = await completeMasterAgentTask({
|
||
taskId: result.taskId,
|
||
deviceId: "mac-studio",
|
||
status: "failed",
|
||
errorMessage: "spawn codex ENOENT",
|
||
});
|
||
|
||
assert.equal(requeued.status, "queued");
|
||
assert.equal(requeued.deviceId, "macbook-air-codex-failover");
|
||
assert.equal(requeued.accountId, "master-codex-device-macbook-air-codex-failover");
|
||
assert.deepEqual(requeued.modelChannelAttemptedDeviceIds, ["mac-studio"]);
|
||
|
||
const state = await readState();
|
||
const task = state.masterAgentTasks.find((item) => item.taskId === result.taskId);
|
||
assert.equal(task?.status, "queued");
|
||
assert.equal(task?.deviceId, "macbook-air-codex-failover");
|
||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||
assert.doesNotMatch(masterProject?.messages.at(-1)?.body ?? "", /Master Codex Node 执行失败/);
|
||
});
|
||
|
||
test("replyToMasterAgentUserMessage tells the user when neither Codex devices nor API keys are available", async () => {
|
||
await updateDevice("mac-studio", {
|
||
status: "online",
|
||
capabilities: {
|
||
gui: { connected: false },
|
||
cli: { connected: false },
|
||
codexAppServer: { connected: false },
|
||
},
|
||
});
|
||
|
||
await saveAiAccount({
|
||
accountId: "master-codex-primary-no-channel-no-api",
|
||
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: "没有可用模型通道。",
|
||
});
|
||
|
||
const result = await replyToMasterAgentUserMessage({
|
||
requestMessageId: "msg-no-model-channel",
|
||
requestText: "现在还能回复吗?",
|
||
requestedBy: "Boss 超级管理员",
|
||
requestedByAccount: "krisolo",
|
||
mode: "enqueue",
|
||
});
|
||
|
||
assert.equal(result.ok, false);
|
||
assert.equal(result.reason, "MASTER_NODE_NOT_CONNECTED");
|
||
|
||
const state = await readState();
|
||
const masterProject = state.projects.find((project) => project.id === "master-agent");
|
||
const reply = masterProject?.messages.at(-1);
|
||
assert.match(reply?.body ?? "", /当前没有可用的模型渠道/);
|
||
assert.doesNotMatch(reply?.body ?? "", /master 节点账号/);
|
||
});
|