feat: gate claw runtime selection by availability
This commit is contained in:
@@ -1,7 +1,11 @@
|
||||
import assert from "node:assert/strict";
|
||||
import test from "node:test";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import {
|
||||
getClawBackendConfigForTesting,
|
||||
getClawBackendAvailabilityForTesting,
|
||||
isClawBackendConfiguredForTesting,
|
||||
} from "../src/lib/execution/backends/claw-config.ts";
|
||||
|
||||
@@ -63,3 +67,48 @@ test("Claw backend 在配置完整时返回 command、args 和 timeout", () => {
|
||||
|
||||
restoreEnv(previous);
|
||||
});
|
||||
|
||||
test("Claw backend availability 会在可执行命令和脚本都存在时返回 ready", async () => {
|
||||
const previous = snapshotEnv();
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-config-"));
|
||||
const scriptPath = path.join(tempDir, "claw-smoke.mjs");
|
||||
await writeFile(scriptPath, "console.log('ok');\n", "utf8");
|
||||
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = scriptPath;
|
||||
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
||||
|
||||
try {
|
||||
const availability = await getClawBackendAvailabilityForTesting();
|
||||
|
||||
assert.equal(availability.status, "ready");
|
||||
assert.equal(availability.selectable, true);
|
||||
assert.equal(availability.reason, "ready");
|
||||
} finally {
|
||||
restoreEnv(previous);
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("Claw backend availability 会在脚本参数不存在时返回不可选", async () => {
|
||||
const previous = snapshotEnv();
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-config-"));
|
||||
const missingScript = path.join(tempDir, "missing-claw-script.mjs");
|
||||
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = missingScript;
|
||||
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
||||
|
||||
try {
|
||||
const availability = await getClawBackendAvailabilityForTesting();
|
||||
|
||||
assert.equal(availability.status, "misconfigured");
|
||||
assert.equal(availability.selectable, false);
|
||||
assert.equal(availability.reason, "script_not_found");
|
||||
} finally {
|
||||
restoreEnv(previous);
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
@@ -98,6 +98,14 @@ test("selectExecutionBackendForTesting honors an explicit claw request when claw
|
||||
requestedBackendId: "claw-runtime",
|
||||
claw: {
|
||||
enabled: true,
|
||||
selectable: true,
|
||||
availability: {
|
||||
status: "ready",
|
||||
selectable: true,
|
||||
configured: true,
|
||||
reason: "ready",
|
||||
reasonLabel: "Claw Runtime 可用。",
|
||||
},
|
||||
supportsKinds: ["master_agent_reply", "thread_reply"],
|
||||
},
|
||||
});
|
||||
@@ -113,6 +121,14 @@ test("selectExecutionBackendForTesting falls back when claw is requested but una
|
||||
requestedBackendId: "claw-runtime",
|
||||
claw: {
|
||||
enabled: false,
|
||||
selectable: false,
|
||||
availability: {
|
||||
status: "disabled",
|
||||
selectable: false,
|
||||
configured: false,
|
||||
reason: "disabled",
|
||||
reasonLabel: "Claw Runtime 当前未启用。",
|
||||
},
|
||||
supportsKinds: ["master_agent_reply"],
|
||||
},
|
||||
});
|
||||
|
||||
@@ -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 { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { execFile as execFileCallback } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { NextRequest } from "next/server";
|
||||
@@ -93,92 +93,117 @@ test("master-agent 会话可保存并读取模型与推理强度覆盖", async (
|
||||
|
||||
test("master-agent 对话控制路由可读写并回显到项目详情", async () => {
|
||||
await setup();
|
||||
|
||||
const session = await createAuthSession({
|
||||
account: "17600003315",
|
||||
role: "highest_admin",
|
||||
displayName: "Boss 超级管理员",
|
||||
loginMethod: "password",
|
||||
});
|
||||
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-agent-controls-"));
|
||||
const scriptPath = path.join(tempDir, "claw-runtime.mjs");
|
||||
await writeFile(scriptPath, "console.log('ok');\n", "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_WORKDIR: process.env.BOSS_CLAW_WORKDIR,
|
||||
};
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = scriptPath;
|
||||
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
||||
|
||||
const postResponse = await postAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
modelOverride: "gpt-5.4",
|
||||
reasoningEffortOverride: "medium",
|
||||
backendOverride: "claw-runtime",
|
||||
try {
|
||||
const session = await createAuthSession({
|
||||
account: "17600003315",
|
||||
role: "highest_admin",
|
||||
displayName: "Boss 超级管理员",
|
||||
loginMethod: "password",
|
||||
});
|
||||
|
||||
const headers = {
|
||||
"content-type": "application/json",
|
||||
cookie: `${AUTH_SESSION_COOKIE}=${session.sessionToken}`,
|
||||
};
|
||||
|
||||
const postResponse = await postAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
method: "POST",
|
||||
headers,
|
||||
body: JSON.stringify({
|
||||
modelOverride: "gpt-5.4",
|
||||
reasoningEffortOverride: "medium",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(postResponse.status, 200);
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(postResponse.status, 200);
|
||||
|
||||
const postPayload = (await postResponse.json()) as {
|
||||
ok: boolean;
|
||||
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 postPayload = (await postResponse.json()) as {
|
||||
ok: boolean;
|
||||
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", {
|
||||
method: "GET",
|
||||
headers,
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(getResponse.status, 200);
|
||||
const getResponse = await getAgentControlsRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/agent-controls", {
|
||||
method: "GET",
|
||||
headers,
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(getResponse.status, 200);
|
||||
|
||||
const getPayload = (await getResponse.json()) as {
|
||||
ok: boolean;
|
||||
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 getPayload = (await getResponse.json()) as {
|
||||
ok: boolean;
|
||||
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", {
|
||||
method: "GET",
|
||||
headers,
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(projectResponse.status, 200);
|
||||
const projectResponse = await getProjectRoute(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent", {
|
||||
method: "GET",
|
||||
headers,
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
assert.equal(projectResponse.status, 200);
|
||||
|
||||
const projectPayload = (await projectResponse.json()) as {
|
||||
ok: boolean;
|
||||
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");
|
||||
const projectPayload = (await projectResponse.json()) as {
|
||||
ok: boolean;
|
||||
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");
|
||||
} finally {
|
||||
if (previousEnv.BOSS_CLAW_ENABLED === undefined) delete process.env.BOSS_CLAW_ENABLED;
|
||||
else process.env.BOSS_CLAW_ENABLED = previousEnv.BOSS_CLAW_ENABLED;
|
||||
if (previousEnv.BOSS_CLAW_COMMAND === undefined) delete process.env.BOSS_CLAW_COMMAND;
|
||||
else process.env.BOSS_CLAW_COMMAND = previousEnv.BOSS_CLAW_COMMAND;
|
||||
if (previousEnv.BOSS_CLAW_ARGS === undefined) delete process.env.BOSS_CLAW_ARGS;
|
||||
else process.env.BOSS_CLAW_ARGS = previousEnv.BOSS_CLAW_ARGS;
|
||||
if (previousEnv.BOSS_CLAW_WORKDIR === undefined) delete process.env.BOSS_CLAW_WORKDIR;
|
||||
else process.env.BOSS_CLAW_WORKDIR = previousEnv.BOSS_CLAW_WORKDIR;
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("master-agent 对话控制按当前账号隔离,不会串到其他用户", async () => {
|
||||
|
||||
@@ -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 { mkdtemp, rm } from "node:fs/promises";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { NextRequest } from "next/server";
|
||||
|
||||
let runtimeRoot = "";
|
||||
@@ -13,7 +13,7 @@ let getUserMasterPromptRoute: typeof import("../src/app/api/v1/master-agent/prom
|
||||
let getUserMasterMemoriesRoute: typeof import("../src/app/api/v1/master-agent/memories/route");
|
||||
let patchUserMasterMemoryRoute: typeof import("../src/app/api/v1/master-agent/memories/[memoryId]/route");
|
||||
let getProjectMemoriesRoute: typeof import("../src/app/api/v1/projects/[projectId]/memories/route");
|
||||
let getPromptProfileRoute: typeof import("../src/app/api/v1/projects/[projectId]/prompt-profile/route");
|
||||
let promptProfileRoute: typeof import("../src/app/api/v1/projects/[projectId]/prompt-profile/route");
|
||||
|
||||
async function setup() {
|
||||
if (runtimeRoot) return;
|
||||
@@ -22,7 +22,7 @@ async function setup() {
|
||||
process.env.BOSS_RUNTIME_ROOT = runtimeRoot;
|
||||
process.env.BOSS_STATE_FILE = path.join(runtimeRoot, "boss-state.json");
|
||||
|
||||
const [data, auth, promptPolicyRoute, userPromptRoute, memoriesRoute, memoryRoute, projectMemoriesRoute, promptProfileRoute] = await Promise.all([
|
||||
const [data, auth, promptPolicyRoute, userPromptRoute, memoriesRoute, memoryRoute, projectMemoriesRoute, loadedPromptProfileRoute] = await Promise.all([
|
||||
import("../src/lib/boss-data.ts"),
|
||||
import("../src/lib/boss-auth.ts"),
|
||||
import("../src/app/api/v1/master-agent/prompt-policy/route.ts"),
|
||||
@@ -40,7 +40,7 @@ async function setup() {
|
||||
getUserMasterMemoriesRoute = memoriesRoute;
|
||||
patchUserMasterMemoryRoute = memoryRoute;
|
||||
getProjectMemoriesRoute = projectMemoriesRoute.GET;
|
||||
getPromptProfileRoute = promptProfileRoute.POST;
|
||||
promptProfileRoute = loadedPromptProfileRoute;
|
||||
}
|
||||
|
||||
async function createAuthedRequest(account = "17600003315", role: "member" | "admin" | "highest_admin" = "highest_admin") {
|
||||
@@ -189,17 +189,69 @@ test("master-agent 记忆页会返回当前用户所有项目记忆", async () =
|
||||
|
||||
test("prompt-profile 写入当前对话提示词时按当前账号隔离", async () => {
|
||||
await setup();
|
||||
const tempDir = await mkdtemp(path.join(os.tmpdir(), "boss-claw-prompt-profile-"));
|
||||
const scriptPath = path.join(tempDir, "claw-runtime.mjs");
|
||||
await writeFile(scriptPath, "console.log('ok');\n", "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_WORKDIR: process.env.BOSS_CLAW_WORKDIR,
|
||||
};
|
||||
process.env.BOSS_CLAW_ENABLED = "true";
|
||||
process.env.BOSS_CLAW_COMMAND = process.execPath;
|
||||
process.env.BOSS_CLAW_ARGS = scriptPath;
|
||||
process.env.BOSS_CLAW_WORKDIR = tempDir;
|
||||
|
||||
try {
|
||||
const memberRequest = await createAuthedRequest("18800001111", "member");
|
||||
|
||||
const response = await promptProfileRoute.POST(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/prompt-profile", {
|
||||
method: "POST",
|
||||
headers: memberRequest.headers,
|
||||
body: JSON.stringify({
|
||||
promptOverride: "成员自己的当前对话提示词",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
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");
|
||||
} finally {
|
||||
if (previousEnv.BOSS_CLAW_ENABLED === undefined) delete process.env.BOSS_CLAW_ENABLED;
|
||||
else process.env.BOSS_CLAW_ENABLED = previousEnv.BOSS_CLAW_ENABLED;
|
||||
if (previousEnv.BOSS_CLAW_COMMAND === undefined) delete process.env.BOSS_CLAW_COMMAND;
|
||||
else process.env.BOSS_CLAW_COMMAND = previousEnv.BOSS_CLAW_COMMAND;
|
||||
if (previousEnv.BOSS_CLAW_ARGS === undefined) delete process.env.BOSS_CLAW_ARGS;
|
||||
else process.env.BOSS_CLAW_ARGS = previousEnv.BOSS_CLAW_ARGS;
|
||||
if (previousEnv.BOSS_CLAW_WORKDIR === undefined) delete process.env.BOSS_CLAW_WORKDIR;
|
||||
else process.env.BOSS_CLAW_WORKDIR = previousEnv.BOSS_CLAW_WORKDIR;
|
||||
await rm(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
test("prompt-profile 会返回当前 Claw Runtime 的可用性状态", async () => {
|
||||
await setup();
|
||||
|
||||
const memberRequest = await createAuthedRequest("18800001111", "member");
|
||||
|
||||
const response = await getPromptProfileRoute(
|
||||
const response = await promptProfileRoute.GET(
|
||||
new NextRequest("http://127.0.0.1:3000/api/v1/projects/master-agent/prompt-profile", {
|
||||
method: "POST",
|
||||
method: "GET",
|
||||
headers: memberRequest.headers,
|
||||
body: JSON.stringify({
|
||||
promptOverride: "成员自己的当前对话提示词",
|
||||
backendOverride: "claw-runtime",
|
||||
}),
|
||||
}),
|
||||
{ params: Promise.resolve({ projectId: "master-agent" }) },
|
||||
);
|
||||
@@ -207,14 +259,20 @@ test("prompt-profile 写入当前对话提示词时按当前账号隔离", async
|
||||
assert.equal(response.status, 200);
|
||||
const payload = (await response.json()) as {
|
||||
ok: boolean;
|
||||
projectPromptOverride: string | null;
|
||||
account: string;
|
||||
projectControls: {
|
||||
backendOverride?: string | null;
|
||||
} | null;
|
||||
clawAvailability?: {
|
||||
configured: boolean;
|
||||
status: string;
|
||||
selectable: boolean;
|
||||
reason: string;
|
||||
reasonLabel: string;
|
||||
};
|
||||
};
|
||||
assert.equal(payload.ok, true);
|
||||
assert.equal(payload.account, "18800001111");
|
||||
assert.equal(payload.projectPromptOverride, "成员自己的当前对话提示词");
|
||||
assert.equal(payload.projectControls?.backendOverride, "claw-runtime");
|
||||
assert.deepEqual(payload.clawAvailability, {
|
||||
configured: false,
|
||||
status: "disabled",
|
||||
selectable: false,
|
||||
reason: "disabled",
|
||||
reasonLabel: "Claw Runtime 当前未启用。",
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user