feat: add claw backend adapter
This commit is contained in:
65
tests/claw-backend-config.test.ts
Normal file
65
tests/claw-backend-config.test.ts
Normal file
@@ -0,0 +1,65 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import {
|
||||
getClawBackendConfigForTesting,
|
||||
isClawBackendConfiguredForTesting,
|
||||
} from "../src/lib/execution/backends/claw-config.ts";
|
||||
|
||||
function snapshotEnv() {
|
||||
return {
|
||||
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_WORKDIR: process.env.BOSS_CLAW_WORKDIR,
|
||||
BOSS_CLAW_TIMEOUT_MS: process.env.BOSS_CLAW_TIMEOUT_MS,
|
||||
BOSS_CLAW_DEFAULT_MODEL: process.env.BOSS_CLAW_DEFAULT_MODEL,
|
||||
};
|
||||
}
|
||||
|
||||
function restoreEnv(snapshot: ReturnType<typeof snapshotEnv>) {
|
||||
process.env.BOSS_CLAW_ENABLED = snapshot.BOSS_CLAW_ENABLED;
|
||||
process.env.BOSS_CLAW_COMMAND = snapshot.BOSS_CLAW_COMMAND;
|
||||
process.env.BOSS_CLAW_ARGS = snapshot.BOSS_CLAW_ARGS;
|
||||
process.env.BOSS_CLAW_WORKDIR = snapshot.BOSS_CLAW_WORKDIR;
|
||||
process.env.BOSS_CLAW_TIMEOUT_MS = snapshot.BOSS_CLAW_TIMEOUT_MS;
|
||||
process.env.BOSS_CLAW_DEFAULT_MODEL = snapshot.BOSS_CLAW_DEFAULT_MODEL;
|
||||
}
|
||||
|
||||
test("Claw backend 在未配置时默认关闭", () => {
|
||||
const previous = snapshotEnv();
|
||||
|
||||
delete process.env.BOSS_CLAW_ENABLED;
|
||||
delete process.env.BOSS_CLAW_COMMAND;
|
||||
delete process.env.BOSS_CLAW_ARGS;
|
||||
delete process.env.BOSS_CLAW_WORKDIR;
|
||||
delete process.env.BOSS_CLAW_TIMEOUT_MS;
|
||||
delete process.env.BOSS_CLAW_DEFAULT_MODEL;
|
||||
|
||||
const config = getClawBackendConfigForTesting();
|
||||
|
||||
assert.equal(config.enabled, false);
|
||||
assert.equal(isClawBackendConfiguredForTesting(config), false);
|
||||
|
||||
restoreEnv(previous);
|
||||
});
|
||||
|
||||
test("Claw backend 在配置完整时返回 command、args 和 timeout", () => {
|
||||
const previous = snapshotEnv();
|
||||
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = "claw";
|
||||
process.env.BOSS_CLAW_ARGS = "run --json";
|
||||
process.env.BOSS_CLAW_WORKDIR = "/tmp/claw";
|
||||
process.env.BOSS_CLAW_TIMEOUT_MS = "45000";
|
||||
|
||||
const config = getClawBackendConfigForTesting();
|
||||
|
||||
assert.equal(config.enabled, true);
|
||||
assert.equal(config.command, "claw");
|
||||
assert.deepEqual(config.args, ["run", "--json"]);
|
||||
assert.equal(config.cwd, "/tmp/claw");
|
||||
assert.equal(config.timeoutMs, 45000);
|
||||
assert.equal(isClawBackendConfiguredForTesting(config), true);
|
||||
|
||||
restoreEnv(previous);
|
||||
});
|
||||
121
tests/claw-backend.test.ts
Normal file
121
tests/claw-backend.test.ts
Normal file
@@ -0,0 +1,121 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
|
||||
import { createClawBackendForTesting } from "../src/lib/execution/backends/claw-backend.ts";
|
||||
|
||||
test("Claw backend 只在启用且请求类型受支持时 canHandle", async () => {
|
||||
const backend = createClawBackendForTesting({
|
||||
config: {
|
||||
enabled: true,
|
||||
command: "claw",
|
||||
args: ["run"],
|
||||
timeoutMs: 45_000,
|
||||
},
|
||||
runner: async () => ({
|
||||
status: "completed",
|
||||
backendId: "claw-runtime",
|
||||
output: "ok",
|
||||
}),
|
||||
});
|
||||
|
||||
assert.equal(
|
||||
await backend.canHandle({
|
||||
kind: "master_agent_reply",
|
||||
projectId: "master-agent",
|
||||
requestMessageId: "msg-1",
|
||||
body: "继续",
|
||||
}),
|
||||
true,
|
||||
);
|
||||
assert.equal(
|
||||
await backend.canHandle({
|
||||
kind: "dispatch_execution",
|
||||
projectId: "project-1",
|
||||
requestMessageId: "msg-2",
|
||||
body: "继续",
|
||||
}),
|
||||
false,
|
||||
);
|
||||
});
|
||||
|
||||
test("Claw backend 执行时会把 executionPrompt、模型和推理强度交给 runner", async () => {
|
||||
const calls: unknown[] = [];
|
||||
const backend = createClawBackendForTesting({
|
||||
config: {
|
||||
enabled: true,
|
||||
command: "claw",
|
||||
args: ["run"],
|
||||
timeoutMs: 45_000,
|
||||
defaultModel: "claude-sonnet",
|
||||
},
|
||||
runner: async (input) => {
|
||||
calls.push(input);
|
||||
return {
|
||||
status: "completed",
|
||||
backendId: "claw-runtime",
|
||||
output: "链路正常",
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
const result = await backend.execute({
|
||||
kind: "master_agent_reply",
|
||||
projectId: "master-agent",
|
||||
requestMessageId: "msg-1",
|
||||
body: "继续推进",
|
||||
executionPrompt: "系统提示词 + 用户提示词 + 当前消息",
|
||||
modelOverride: "gpt-5.4",
|
||||
reasoningEffortOverride: "high",
|
||||
});
|
||||
|
||||
assert.equal(result.status, "completed");
|
||||
assert.deepEqual(calls, [
|
||||
{
|
||||
config: {
|
||||
enabled: true,
|
||||
command: "claw",
|
||||
args: ["run"],
|
||||
timeoutMs: 45_000,
|
||||
defaultModel: "claude-sonnet",
|
||||
},
|
||||
payload: {
|
||||
kind: "master_agent_reply",
|
||||
projectId: "master-agent",
|
||||
requestMessageId: "msg-1",
|
||||
body: "继续推进",
|
||||
executionPrompt: "系统提示词 + 用户提示词 + 当前消息",
|
||||
model: "gpt-5.4",
|
||||
reasoningEffort: "high",
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
|
||||
test("Claw backend describe 返回稳定描述", async () => {
|
||||
const backend = createClawBackendForTesting({
|
||||
config: {
|
||||
enabled: true,
|
||||
command: "claw",
|
||||
args: ["run"],
|
||||
timeoutMs: 45_000,
|
||||
},
|
||||
runner: async () => ({
|
||||
status: "completed",
|
||||
backendId: "claw-runtime",
|
||||
output: "ok",
|
||||
}),
|
||||
});
|
||||
|
||||
const description = await backend.describe({
|
||||
kind: "thread_reply",
|
||||
projectId: "project-1",
|
||||
requestMessageId: "msg-1",
|
||||
body: "继续",
|
||||
});
|
||||
|
||||
assert.deepEqual(description, {
|
||||
backendId: "claw-runtime",
|
||||
label: "Claw Runtime",
|
||||
mode: "local",
|
||||
});
|
||||
});
|
||||
180
tests/claw-runner.test.ts
Normal file
180
tests/claw-runner.test.ts
Normal file
@@ -0,0 +1,180 @@
|
||||
import assert from "node:assert/strict";
|
||||
import { mkdtemp, realpath, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import test from "node:test";
|
||||
|
||||
import { runClawCommandForTesting } from "../src/lib/execution/backends/claw-runner.ts";
|
||||
|
||||
async function createTempScript(source: string) {
|
||||
const dir = await mkdtemp(join(tmpdir(), "claw-runner-"));
|
||||
const scriptPath = join(dir, "claw-script.mjs");
|
||||
await writeFile(scriptPath, source, "utf8");
|
||||
return { dir, scriptPath };
|
||||
}
|
||||
|
||||
test("Claw runner 把 completed JSON 响应映射成 completed,并把 payload 写入 stdin", async () => {
|
||||
const workspace = await mkdtemp(join(tmpdir(), "claw-runner-cwd-"));
|
||||
const expectedWorkspace = await realpath(workspace);
|
||||
const { scriptPath } = await createTempScript(`
|
||||
import { readFile } from "node:fs/promises";
|
||||
|
||||
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: JSON.stringify({
|
||||
body: payload.body,
|
||||
cwd: process.cwd(),
|
||||
command: payload.command,
|
||||
}),
|
||||
}));
|
||||
`);
|
||||
|
||||
const result = await runClawCommandForTesting({
|
||||
config: {
|
||||
enabled: true,
|
||||
command: process.execPath,
|
||||
args: [scriptPath],
|
||||
cwd: workspace,
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
payload: {
|
||||
body: "继续执行",
|
||||
command: "claw",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, "completed");
|
||||
if (result.status !== "completed") {
|
||||
assert.fail("expected completed");
|
||||
}
|
||||
|
||||
const output = JSON.parse(result.output) as {
|
||||
body: string;
|
||||
cwd: string;
|
||||
command: string;
|
||||
};
|
||||
|
||||
assert.equal(output.body, "继续执行");
|
||||
assert.equal(output.cwd, expectedWorkspace);
|
||||
assert.equal(output.command, "claw");
|
||||
});
|
||||
|
||||
test("Claw runner 把 failed JSON 响应映射成 failed", async () => {
|
||||
const { scriptPath } = await createTempScript(`
|
||||
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: "failed",
|
||||
error: "bad-request:" + payload.body,
|
||||
}));
|
||||
`);
|
||||
|
||||
const result = await runClawCommandForTesting({
|
||||
config: {
|
||||
enabled: true,
|
||||
command: process.execPath,
|
||||
args: [scriptPath],
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
payload: {
|
||||
body: "格式不对",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, "failed");
|
||||
if (result.status !== "failed") {
|
||||
assert.fail("expected failed");
|
||||
}
|
||||
|
||||
assert.equal(result.error, "bad-request:格式不对");
|
||||
});
|
||||
|
||||
test("Claw runner 把无效 JSON 响应映射成 INVALID_CLAW_RESPONSE", async () => {
|
||||
const { scriptPath } = await createTempScript(`
|
||||
process.stdout.write("not-json");
|
||||
`);
|
||||
|
||||
const result = await runClawCommandForTesting({
|
||||
config: {
|
||||
enabled: true,
|
||||
command: process.execPath,
|
||||
args: [scriptPath],
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
payload: {
|
||||
body: "anything",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, "failed");
|
||||
if (result.status !== "failed") {
|
||||
assert.fail("expected failed");
|
||||
}
|
||||
|
||||
assert.match(result.error, /INVALID_CLAW_RESPONSE/);
|
||||
});
|
||||
|
||||
test("Claw runner 把非零退出码映射成 stderr 或退出码错误", async () => {
|
||||
const { scriptPath } = await createTempScript(`
|
||||
process.stderr.write("claw crashed");
|
||||
process.exit(2);
|
||||
`);
|
||||
|
||||
const result = await runClawCommandForTesting({
|
||||
config: {
|
||||
enabled: true,
|
||||
command: process.execPath,
|
||||
args: [scriptPath],
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
payload: {
|
||||
body: "anything",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, "failed");
|
||||
if (result.status !== "failed") {
|
||||
assert.fail("expected failed");
|
||||
}
|
||||
|
||||
assert.match(result.error, /claw crashed/);
|
||||
});
|
||||
|
||||
test("Claw runner 超时后返回 CLAW_TIMEOUT", async () => {
|
||||
const { scriptPath } = await createTempScript(`
|
||||
setTimeout(() => {
|
||||
process.stdout.write(JSON.stringify({ status: "completed", output: "late" }));
|
||||
}, 500);
|
||||
`);
|
||||
|
||||
const result = await runClawCommandForTesting({
|
||||
config: {
|
||||
enabled: true,
|
||||
command: process.execPath,
|
||||
args: [scriptPath],
|
||||
timeoutMs: 50,
|
||||
},
|
||||
payload: {
|
||||
body: "slow",
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(result.status, "failed");
|
||||
if (result.status !== "failed") {
|
||||
assert.fail("expected failed");
|
||||
}
|
||||
|
||||
assert.match(result.error, /CLAW_TIMEOUT/);
|
||||
});
|
||||
@@ -1,6 +1,9 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import { selectExecutionBackendForTesting } from "@/lib/execution/backend-selector";
|
||||
import {
|
||||
listExecutionBackendChoices,
|
||||
selectExecutionBackendForTesting,
|
||||
} from "@/lib/execution/backend-selector";
|
||||
|
||||
test("selectExecutionBackendForTesting prefers the ready primary master codex node", async () => {
|
||||
const backend = await selectExecutionBackendForTesting({
|
||||
@@ -73,3 +76,46 @@ test("selectExecutionBackendForTesting falls back to master node last when highe
|
||||
|
||||
assert.equal(backend.backendId, "master-codex-node");
|
||||
});
|
||||
|
||||
test("listExecutionBackendChoices keeps claw disabled by default", () => {
|
||||
const backends = listExecutionBackendChoices({
|
||||
primary: { provider: "master_codex_node", status: "ready" },
|
||||
backups: [{ provider: "openai_api", status: "ready" }],
|
||||
requestKind: "master_agent_reply",
|
||||
});
|
||||
|
||||
assert.deepEqual(
|
||||
backends.map((backend) => backend.backendId),
|
||||
["master-codex-node", "openai-api"],
|
||||
);
|
||||
});
|
||||
|
||||
test("selectExecutionBackendForTesting honors an explicit claw request when claw is enabled", async () => {
|
||||
const backend = await selectExecutionBackendForTesting({
|
||||
primary: { provider: "master_codex_node", status: "ready" },
|
||||
backups: [{ provider: "openai_api", status: "ready" }],
|
||||
requestKind: "master_agent_reply",
|
||||
requestedBackendId: "claw-runtime",
|
||||
claw: {
|
||||
enabled: true,
|
||||
supportsKinds: ["master_agent_reply", "thread_reply"],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(backend.backendId, "claw-runtime");
|
||||
});
|
||||
|
||||
test("selectExecutionBackendForTesting falls back when claw is requested but unavailable", async () => {
|
||||
const backend = await selectExecutionBackendForTesting({
|
||||
primary: { provider: "master_codex_node", status: "ready" },
|
||||
backups: [{ provider: "openai_api", status: "ready" }],
|
||||
requestKind: "master_agent_reply",
|
||||
requestedBackendId: "claw-runtime",
|
||||
claw: {
|
||||
enabled: false,
|
||||
supportsKinds: ["master_agent_reply"],
|
||||
},
|
||||
});
|
||||
|
||||
assert.equal(backend.backendId, "master-codex-node");
|
||||
});
|
||||
|
||||
@@ -19,10 +19,13 @@ test("ExecutionRequest 工厂会生成稳定默认字段", () => {
|
||||
assert.equal(request.projectId, "master-agent");
|
||||
assert.equal(request.requestMessageId, "msg-1");
|
||||
assert.equal(request.body, "你好");
|
||||
assert.equal(request.executionPrompt, undefined);
|
||||
assert.equal(request.targetProjectId, undefined);
|
||||
assert.equal(request.targetThreadId, undefined);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(request, "requestedByAccount"), true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(request, "requestedByLabel"), true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(request, "executionPrompt"), true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(request, "requestedBackendId"), true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(request, "taskId"), true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(request, "targetProjectId"), true);
|
||||
assert.equal(Object.prototype.hasOwnProperty.call(request, "targetThreadId"), true);
|
||||
|
||||
@@ -53,6 +53,9 @@ async function resetMasterAgentControls() {
|
||||
const project = state.projects.find((item) => item.id === "master-agent");
|
||||
assert.ok(project, "expected seeded master-agent project");
|
||||
delete project.agentControls;
|
||||
state.userProjectAgentControls = state.userProjectAgentControls.filter(
|
||||
(item) => item.projectId !== "master-agent",
|
||||
);
|
||||
await writeState(state);
|
||||
}
|
||||
|
||||
@@ -110,6 +113,7 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
body: JSON.stringify({
|
||||
modelOverride: "gpt-5.4",
|
||||
reasoningEffortOverride: "medium",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
@@ -121,12 +125,14 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
controls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(postPayload.ok, true);
|
||||
assert.equal(postPayload.controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(postPayload.controls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(postPayload.controls?.backendOverride, "claw-runtime");
|
||||
|
||||
const getResponse = await getAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
@@ -142,12 +148,14 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
controls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(getPayload.ok, true);
|
||||
assert.equal(getPayload.controls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(getPayload.controls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(getPayload.controls?.backendOverride, "claw-runtime");
|
||||
|
||||
const projectResponse = await getProjectRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent", {
|
||||
@@ -163,12 +171,14 @@ test("master-agent 对话控制路由可读写并回显到项目详情", async (
|
||||
agentControls: {
|
||||
modelOverride?: string;
|
||||
reasoningEffortOverride?: string;
|
||||
backendOverride?: string;
|
||||
updatedAt: string;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(projectPayload.ok, true);
|
||||
assert.equal(projectPayload.agentControls?.modelOverride, "gpt-5.4");
|
||||
assert.equal(projectPayload.agentControls?.reasoningEffortOverride, "medium");
|
||||
assert.equal(projectPayload.agentControls?.backendOverride, "claw-runtime");
|
||||
});
|
||||
|
||||
test("master-agent 对话控制按当前账号隔离,不会串到其他用户", async () => {
|
||||
@@ -873,6 +883,36 @@ test("POST /agent-controls rejects unknown-key payload and preserves controls",
|
||||
assert.equal(afterProject?.updatedAt, beforeUpdatedAt);
|
||||
});
|
||||
|
||||
test("master-agent 对话控制 POST 会稳定拒绝非法 backendOverride", async () => {
|
||||
await setup();
|
||||
|
||||
const session = await createAuthSession({
|
||||
account: "17600003315",
|
||||
role: "highest_admin",
|
||||
displayName: "Boss 超级管理员",
|
||||
loginMethod: "password",
|
||||
});
|
||||
|
||||
const response = await postAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"content-type": "application/json",
|
||||
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
backendOverride: "bad-backend",
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 400);
|
||||
const payload = (await response.json()) as { ok: boolean; message?: string };
|
||||
assert.equal(payload.ok, false);
|
||||
assert.equal(payload.message, "INVALID_BACKEND_OVERRIDE");
|
||||
});
|
||||
|
||||
test("master-agent controls helper 不会写入普通项目", async () => {
|
||||
await setup();
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -198,6 +198,7 @@ test("prompt-profile 写入当前对话提示词时按当前账号隔离", async
|
||||
headers: memberRequest.headers,
|
||||
body: JSON.stringify({
|
||||
promptOverride: "成员自己的当前对话提示词",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
@@ -208,8 +209,12 @@ test("prompt-profile 写入当前对话提示词时按当前账号隔离", async
|
||||
ok: boolean;
|
||||
projectPromptOverride: string | null;
|
||||
account: string;
|
||||
projectControls: {
|
||||
backendOverride?: string | null;
|
||||
} | null;
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.account, "18800001111");
|
||||
assert.equal(payload.projectPromptOverride, "成员自己的当前对话提示词");
|
||||
assert.equal(payload.projectControls?.backendOverride, "claw-runtime");
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user