fix: route master agent through codex device pool
This commit is contained in:
@@ -10,6 +10,9 @@ 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;
|
||||
@@ -28,6 +31,9 @@ async function setup() {
|
||||
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) {
|
||||
@@ -54,6 +60,14 @@ test.beforeEach(async () => {
|
||||
});
|
||||
|
||||
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",
|
||||
@@ -253,6 +267,14 @@ test("replyToMasterAgentUserMessage uses active DeepSeek API accounts directly",
|
||||
});
|
||||
|
||||
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",
|
||||
@@ -508,3 +530,256 @@ test("replyToMasterAgentUserMessage falls back to a ready backup master node acc
|
||||
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 节点账号/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user