feat: add claw backend adapter
This commit is contained in:
@@ -2,7 +2,7 @@ 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";
|
||||
import { mkdir, mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
let runtimeRoot = "";
|
||||
@@ -240,6 +240,99 @@ test("master-agent enqueue 在主节点离线时会自动切到 OpenAI 后台队
|
||||
}
|
||||
});
|
||||
|
||||
test("master-agent enqueue 在显式选择 claw-runtime 时会通过 Claw 异步回写回复", async () => {
|
||||
const clawDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-queue-"));
|
||||
const clawScriptPath = path.join(clawDir, "claw-runtime.mjs");
|
||||
await writeFile(
|
||||
clawScriptPath,
|
||||
`
|
||||
let stdin = "";
|
||||
process.stdin.setEncoding("utf8");
|
||||
for await (const chunk of process.stdin) {
|
||||
stdin += chunk;
|
||||
}
|
||||
const payload = JSON.parse(stdin);
|
||||
process.stdout.write(JSON.stringify({
|
||||
status: "completed",
|
||||
output: "Claw 已接管当前主 Agent 会话:" + payload.body
|
||||
}));
|
||||
`,
|
||||
"utf8",
|
||||
);
|
||||
|
||||
const previousEnv = {
|
||||
BOSS_CLAW_ENABLED: process.env.BOSS_CLAW_ENABLED,
|
||||
BOSS_CLAW_COMMAND: process.env.BOSS_CLAW_COMMAND,
|
||||
BOSS_CLAW_ARGS: process.env.BOSS_CLAW_ARGS,
|
||||
BOSS_CLAW_TIMEOUT_MS: process.env.BOSS_CLAW_TIMEOUT_MS,
|
||||
};
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = clawScriptPath;
|
||||
process.env.BOSS_CLAW_TIMEOUT_MS = "1000";
|
||||
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-claw",
|
||||
label: "主 GPT",
|
||||
role: "primary",
|
||||
provider: "master_codex_node",
|
||||
displayName: "Mac 上的 Master Codex Node",
|
||||
nodeId: "local-codex-node",
|
||||
nodeLabel: "本机 Codex",
|
||||
model: "gpt-5.4",
|
||||
enabled: true,
|
||||
setActive: true,
|
||||
loginStatusNote: "用于 Claw backend 队列测试。",
|
||||
});
|
||||
|
||||
await updateProjectAgentControls("master-agent", {
|
||||
backendOverride: "claw-runtime",
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await POST(
|
||||
await createAuthedRequest("master-agent", {
|
||||
body: "请走 Claw runtime",
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
task?: { taskId: string; status: string } | null;
|
||||
masterReply?: { accountId?: string } | null;
|
||||
masterReplyState?: string | null;
|
||||
};
|
||||
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.masterReply?.accountId, "claw-runtime");
|
||||
assert.equal(payload.masterReplyState, "queued");
|
||||
assert.ok(payload.task?.taskId);
|
||||
|
||||
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?.status, "completed");
|
||||
assert.equal(task?.replyBody, "Claw 已接管当前主 Agent 会话:请走 Claw runtime");
|
||||
|
||||
const masterProject = nextState.projects.find((project) => project.id === "master-agent");
|
||||
const mirroredReply = masterProject?.messages.at(-1);
|
||||
assert.match(mirroredReply?.body ?? "", /Claw 已接管当前主 Agent 会话/);
|
||||
} finally {
|
||||
process.env.BOSS_CLAW_ENABLED = previousEnv.BOSS_CLAW_ENABLED;
|
||||
process.env.BOSS_CLAW_COMMAND = previousEnv.BOSS_CLAW_COMMAND;
|
||||
process.env.BOSS_CLAW_ARGS = previousEnv.BOSS_CLAW_ARGS;
|
||||
process.env.BOSS_CLAW_TIMEOUT_MS = previousEnv.BOSS_CLAW_TIMEOUT_MS;
|
||||
await rm(clawDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("master-agent enqueue 在首选主节点离线时会回退到可用的备用主节点并返回实际账号", async () => {
|
||||
await saveAiAccount({
|
||||
accountId: "master-codex-primary-offline",
|
||||
|
||||
Reference in New Issue
Block a user