2145 lines
103 KiB
TypeScript
2145 lines
103 KiB
TypeScript
import assert from "node:assert/strict";
|
|
import { createServer as createHttpServer } from "node:http";
|
|
import { mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { test } from "bun:test";
|
|
|
|
import { createCloudApiServer } from "./server.ts";
|
|
import { validateCodeAgentChatSchema } from "./code-agent-chat.ts";
|
|
import { createCodexStdioSessionManager } from "./codex-stdio-session.ts";
|
|
import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts";
|
|
import {
|
|
codexStdioChatFixture,
|
|
codexStdioReadyFixture,
|
|
createFakeAppServerClient,
|
|
createFakeCodexCommand,
|
|
createManualAgentSession,
|
|
delay,
|
|
ensureTestAgentAuth,
|
|
pollAgentResult,
|
|
postAgent,
|
|
postAgentRaw,
|
|
prepareFakeCodexHome
|
|
} from "./server-test-helpers.ts";
|
|
|
|
const TEST_AGENT_ACTOR = { id: "usr_agent_owner", username: "agent-owner", displayName: "Agent Owner", role: "user", status: "active" };
|
|
const TEST_AUTH_SESSION = { id: "auth_ses_agent_owner" };
|
|
|
|
function testAgentSessionRecord(input = {}) {
|
|
const sessionId = input.sessionId ?? input.id;
|
|
return {
|
|
id: sessionId,
|
|
sessionId,
|
|
projectId: input.projectId ?? "pikasTech/HWLAB",
|
|
agentId: input.agentId ?? "hwlab-code-agent",
|
|
status: input.status ?? "idle",
|
|
ownerUserId: input.ownerUserId ?? TEST_AGENT_ACTOR.id,
|
|
ownerRole: input.ownerRole ?? TEST_AGENT_ACTOR.role,
|
|
conversationId: input.conversationId ?? null,
|
|
threadId: input.threadId ?? null,
|
|
lastTraceId: input.traceId ?? input.lastTraceId ?? null,
|
|
session: input.session ?? {},
|
|
updatedAt: input.updatedAt ?? "2026-06-03T00:00:00.000Z"
|
|
};
|
|
}
|
|
|
|
test("cloud api /v1/agent/chat parse and params errors use structured blocker envelope", async () => {
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test"
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const parse = await postAgentRaw(port, "{", { "x-trace-id": "trc_parse_error" });
|
|
assert.equal(parse.status, 400);
|
|
assert.equal(parse.body.status, "failed");
|
|
assert.equal(parse.body.error.code, "parse_error");
|
|
assert.equal(parse.body.error.layer, "api");
|
|
assert.equal(parse.body.error.retryable, true);
|
|
assert.equal(parse.body.error.traceId, "trc_parse_error");
|
|
assert.equal(parse.body.error.route, "/v1/agent/chat");
|
|
assert.match(parse.body.error.userMessage, /JSON/u);
|
|
assert.equal(Object.hasOwn(parse.body, "reply"), false);
|
|
assert.equal(JSON.stringify(parse.body).includes("sk-"), false);
|
|
|
|
const invalid = await postAgentRaw(port, JSON.stringify([]), { "x-trace-id": "trc_invalid_params" });
|
|
assert.equal(invalid.status, 400);
|
|
assert.equal(invalid.body.error.code, "invalid_params");
|
|
assert.equal(invalid.body.error.layer, "api");
|
|
assert.equal(invalid.body.error.retryable, true);
|
|
assert.equal(invalid.body.error.blocker.code, "invalid_params");
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat supports short submit and result polling", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-short-"));
|
|
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-short-codex-home-"));
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
CODEX_HOME: codexHome,
|
|
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
|
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
|
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
|
OPENAI_API_KEY: "test-openai-key-material",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
|
},
|
|
codexStdioManager: {
|
|
describe() {
|
|
return {
|
|
...codexStdioReadyFixture({ workspace, codexHome }),
|
|
sandbox: "danger-full-access"
|
|
};
|
|
},
|
|
async probe() {
|
|
return {
|
|
...codexStdioReadyFixture({ workspace, codexHome }),
|
|
sandbox: "danger-full-access"
|
|
};
|
|
},
|
|
async chat(params = {}) {
|
|
return {
|
|
...codexStdioChatFixture({ workspace, codexHome, params }),
|
|
sandbox: "danger-full-access",
|
|
session: {
|
|
...codexStdioChatFixture({ workspace, codexHome, params }).session,
|
|
sandbox: "danger-full-access"
|
|
}
|
|
};
|
|
},
|
|
cancel() {},
|
|
reapIdle() {}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const traceId = "trc_server-test-short-submit";
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: "cnv_server-test-short-submit"
|
|
});
|
|
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: manualSession.cookie,
|
|
"x-trace-id": traceId,
|
|
"prefer": "respond-async",
|
|
"x-hwlab-short-connection": "1"
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: manualSession.conversationId,
|
|
sessionId: manualSession.sessionId,
|
|
message: "用pwd列出你当前的工作目录"
|
|
})
|
|
});
|
|
assert.equal(submit.status, 202);
|
|
const accepted = await submit.json();
|
|
assert.equal(accepted.accepted, true);
|
|
assert.equal(accepted.shortConnection, true);
|
|
assert.equal(accepted.traceId, traceId);
|
|
assert.equal(accepted.resultUrl, `/v1/agent/chat/result/${traceId}`);
|
|
|
|
const payload = await pollAgentResult(port, traceId);
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.traceId, traceId);
|
|
assert.equal(payload.sandbox, "danger-full-access");
|
|
assert.match(payload.reply.content, new RegExp(workspace.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")));
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
await rm(workspace, { recursive: true, force: true });
|
|
await rm(codexHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat delegates v0.2 turns to AgentRun v0.1 over adapter", async () => {
|
|
const calls = [];
|
|
const ownerSessions = new Map();
|
|
const conversationMessages = [];
|
|
const agentRunServer = createHttpServer(async (request, response) => {
|
|
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
const chunks = [];
|
|
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : null;
|
|
calls.push({ method: request.method, path: url.pathname, search: url.search, body });
|
|
const send = (data) => {
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_agentrun" })}\n`);
|
|
};
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter") {
|
|
return send({
|
|
id: "run_hwlab_adapter",
|
|
status: "claimed",
|
|
terminalStatus: null,
|
|
claimedBy: "runner_hwlab_adapter",
|
|
leaseExpiresAt: "2999-01-01T00:00:00.000Z",
|
|
backendProfile: "deepseek",
|
|
sessionRef: {
|
|
sessionId: "ses_agentrun_deepseek_server_test_agentrun",
|
|
conversationId: "cnv_server-test-agentrun",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
|
metadata: {}
|
|
}
|
|
});
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
|
assert.equal(body.tenantId, "hwlab");
|
|
assert.equal(body.projectId, "pikasTech/HWLAB");
|
|
assert.equal(body.backendProfile, "deepseek");
|
|
assert.equal(body.resourceBundleRef.repoUrl, "http://git-mirror-http.devops-infra.svc.cluster.local/pikasTech/HWLAB.git");
|
|
assert.equal(body.resourceBundleRef.commitId, "0123456789abcdef0123456789abcdef01234567");
|
|
assert.deepEqual(body.resourceBundleRef.toolAliases, [
|
|
{ name: "hwpod", path: "tools/device-pod-cli.mjs", kind: "node-script" },
|
|
{ name: "unidesk-ssh", path: "tools/unidesk-ssh.mjs", kind: "bun-script" }
|
|
]);
|
|
assert.deepEqual(body.resourceBundleRef.promptRefs, [
|
|
{ name: "hwlab-v02-runtime", path: "internal/agent/prompts/hwlab-v02-runtime.md", inject: "thread-start", required: true }
|
|
]);
|
|
assert.deepEqual(body.resourceBundleRef.skillRefs, [
|
|
{ name: "device-pod-cli", path: "skills/device-pod-cli/SKILL.md", required: true, aggregateAs: "device-pod-cli" },
|
|
{ name: "hwlab-agent-runtime", path: "skills/hwlab-agent-runtime/SKILL.md", required: true, aggregateAs: "hwlab-agent-runtime" }
|
|
]);
|
|
const toolCredentials = body.executionPolicy.secretScope.toolCredentials;
|
|
assert.equal(toolCredentials.some((item) => item.tool === "github" && item.projection.envName === "GH_TOKEN" && item.secretRef.name === "agentrun-v01-tool-github-pr"), true);
|
|
assert.equal(toolCredentials.some((item) => item.tool === "unidesk-ssh" && item.projection.envName === "UNIDESK_SSH_CLIENT_TOKEN" && item.secretRef.name === "agentrun-v01-tool-unidesk-ssh"), true);
|
|
assert.equal(body.sessionRef.threadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
|
assert.equal(body.sessionRef.sessionId, "ses_agentrun_deepseek_server_test_agentrun");
|
|
assert.equal(body.sessionRef.metadata.hwlabProjectId, "prj_device_pod_workbench");
|
|
assert.equal(body.sessionRef.metadata.hwlabSessionId, "ses_server-test-agentrun");
|
|
assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "deepseek");
|
|
assert.equal(body.sessionRef.metadata.agentRunSessionPolicy, "backend-profile-scoped");
|
|
assert.equal(body.sessionRef.metadata.threadContinuityPolicy, "hwlab-agentrun-v01-reuse-runner-thread");
|
|
assert.equal(body.sessionRef.metadata.sessionPolicy, "hwlab-agentrun-v01-session-runner-reuse");
|
|
return send({ id: "run_hwlab_adapter", status: "pending", backendProfile: "deepseek", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands") {
|
|
if (body.type === "steer") {
|
|
assert.equal(body.payload.targetTraceId, "trc_server-test-agentrun-adapter");
|
|
assert.equal(body.payload.targetCommandId, "cmd_hwlab_adapter");
|
|
assert.equal(body.payload.traceId, "trc_steer_server_test");
|
|
assert.match(body.payload.prompt, /STEER_MARK/u);
|
|
assert.equal(body.payload.sessionId, "ses_agentrun_deepseek_server_test_agentrun");
|
|
assert.equal(body.payload.hwlabSessionId, "ses_server-test-agentrun");
|
|
assert.equal(body.payload.threadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
|
assert.equal(body.idempotencyKey, "trc_steer_server_test");
|
|
return send({ id: "cmd_hwlab_adapter_steer", runId: "run_hwlab_adapter", state: "pending", type: "steer", seq: 7 });
|
|
}
|
|
assert.equal(body.type, "turn");
|
|
assert.match(body.payload.prompt, /AgentRun adapter/u);
|
|
assert.equal(body.payload.projectId, "prj_device_pod_workbench");
|
|
assert.equal(body.payload.sessionId, "ses_agentrun_deepseek_server_test_agentrun");
|
|
assert.equal(body.payload.hwlabSessionId, "ses_server-test-agentrun");
|
|
assert.equal(body.payload.threadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
|
assert.equal(body.payload.threadContinuityPolicy, "hwlab-agentrun-v01-reuse-runner-thread");
|
|
assert.equal(body.payload.sessionPolicy, "hwlab-agentrun-v01-session-runner-reuse");
|
|
const secondTurn = /第二轮/u.test(body.payload.message ?? body.payload.prompt);
|
|
if (secondTurn) {
|
|
assert.equal(body.payload.prompt, "AgentRun adapter smoke 第二轮:总结我们刚才的对话内容");
|
|
assert.equal(body.payload.message, "AgentRun adapter smoke 第二轮:总结我们刚才的对话内容");
|
|
assert.equal(Object.hasOwn(body.payload, "originalPrompt"), false);
|
|
assert.equal(Object.hasOwn(body.payload, "conversationContext"), false);
|
|
assert.equal(Object.hasOwn(body.payload, "messages"), false);
|
|
assert.equal(body.payload.prompt.includes("HWLAB 同一会话的已脱敏历史上下文"), false);
|
|
assert.equal(body.payload.prompt.includes("看看device-pod可用性"), false);
|
|
assert.equal(body.payload.prompt.includes("v0.2 device-pod 可用性快照"), false);
|
|
assert.equal(body.payload.prompt.includes("legacy synthetic context must not reach provider"), false);
|
|
assert.equal(body.payload.prompt.includes("legacy flat messages must not reach provider"), false);
|
|
assert.equal(body.payload.prompt.includes("test-device-pod-api-key"), false);
|
|
assert.equal(body.payload.prompt.includes("sk-test-device-secret"), false);
|
|
} else {
|
|
assert.equal(Object.hasOwn(body.payload, "conversationContext"), false);
|
|
}
|
|
return send({ id: secondTurn ? "cmd_hwlab_adapter_second" : "cmd_hwlab_adapter", runId: "run_hwlab_adapter", state: "pending", type: "turn", seq: secondTurn ? 2 : 1 });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_adapter/runner-jobs") {
|
|
const secondRunnerJob = body.commandId === "cmd_hwlab_adapter_second";
|
|
assert.ok(body.commandId === "cmd_hwlab_adapter" || secondRunnerJob);
|
|
assert.deepEqual(body.transientEnv.map((entry) => entry.name), [
|
|
"HWLAB_RUNTIME_API_URL",
|
|
"HWLAB_RUNTIME_WEB_URL",
|
|
"HWLAB_RUNTIME_NAMESPACE",
|
|
"HWLAB_RUNTIME_LANE",
|
|
"HWLAB_RUNTIME_ENDPOINT_LOCKED",
|
|
"HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME",
|
|
"UNIDESK_MAIN_SERVER_IP"
|
|
]);
|
|
const transientEnv = Object.fromEntries(body.transientEnv.map((entry) => [entry.name, entry.value]));
|
|
assert.equal(transientEnv.HWLAB_RUNTIME_API_URL, "http://hwlab-cloud-api.hwlab-v02.svc.cluster.local:6667");
|
|
assert.equal(transientEnv.HWLAB_RUNTIME_WEB_URL, "http://hwlab-cloud-web.hwlab-v02.svc.cluster.local:8080");
|
|
assert.equal(transientEnv.HWLAB_RUNTIME_NAMESPACE, "hwlab-v02");
|
|
assert.equal(transientEnv.HWLAB_RUNTIME_LANE, "v02");
|
|
assert.equal(transientEnv.HWLAB_RUNTIME_ENDPOINT_LOCKED, "1");
|
|
assert.equal(transientEnv.HWLAB_CODE_AGENT_ASSEMBLED_RUNTIME, "1");
|
|
assert.equal(transientEnv.UNIDESK_MAIN_SERVER_IP, "http://74.48.78.17:18081");
|
|
assert.ok(body.transientEnv.length >= 7);
|
|
assert.equal(Object.hasOwn(transientEnv, "UNIDESK_SSH_CLIENT_TOKEN"), false);
|
|
assert.equal(Object.hasOwn(transientEnv, "GH_TOKEN"), false);
|
|
assert.equal(Object.hasOwn(transientEnv, "HWLAB_DEVICE_POD_API_URL"), false);
|
|
assert.equal(Object.hasOwn(transientEnv, "HWLAB_CODE_AGENT_DEVICE_POD_API_URL"), false);
|
|
return send({
|
|
action: "create-kubernetes-job",
|
|
runId: "run_hwlab_adapter",
|
|
commandId: body.commandId,
|
|
attemptId: secondRunnerJob ? "attempt_hwlab_adapter_second" : "attempt_hwlab_adapter",
|
|
runnerId: secondRunnerJob ? "runner_hwlab_adapter_second" : "runner_hwlab_adapter",
|
|
namespace: "agentrun-v01",
|
|
jobName: secondRunnerJob ? "agentrun-v01-runner-hwlab-adapter-second" : "agentrun-v01-runner-hwlab-adapter",
|
|
jobIdentity: { namespace: "agentrun-v01", name: secondRunnerJob ? "agentrun-v01-runner-hwlab-adapter-second" : "agentrun-v01-runner-hwlab-adapter" },
|
|
runner: { attemptId: secondRunnerJob ? "attempt_hwlab_adapter_second" : "attempt_hwlab_adapter", runnerId: secondRunnerJob ? "runner_hwlab_adapter_second" : "runner_hwlab_adapter" }
|
|
});
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/events") {
|
|
const afterSeq = Number.parseInt(url.searchParams.get("afterSeq") ?? "0", 10);
|
|
const second = afterSeq >= 5 || url.searchParams.get("afterSeq") === "3" || calls.some((call) => call.path === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter_second/result");
|
|
if (second) return send({ items: [
|
|
{ id: "evt_old_tail", runId: "run_hwlab_adapter", seq: 6, type: "assistant_message", payload: { commandId: "cmd_hwlab_adapter", text: "旧 command 尾部不应进入第二轮。" }, createdAt: "2026-06-01T00:00:02.500Z" },
|
|
{ id: "evt_7", runId: "run_hwlab_adapter", seq: 7, type: "backend_status", payload: { phase: "turn-started", commandId: "cmd_hwlab_adapter_second", attemptId: "attempt_hwlab_adapter_second", runnerId: "runner_hwlab_adapter_second", jobName: "agentrun-v01-runner-hwlab-adapter-second", namespace: "agentrun-v01" }, createdAt: "2026-06-01T00:00:03.000Z" },
|
|
{ id: "evt_7_prompt", runId: "run_hwlab_adapter", seq: 8, type: "backend_status", payload: { phase: "initial-prompt-assembly", commandId: "cmd_hwlab_adapter_second", initialPromptInjected: false, reason: "thread-resume", initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillRefCount: 2, valuesPrinted: false } }, createdAt: "2026-06-01T00:00:03.500Z" },
|
|
{ id: "evt_8", runId: "run_hwlab_adapter", seq: 8, type: "assistant_message", payload: { commandId: "cmd_hwlab_adapter_second", text: "AgentRun adapter 复用已有 runner 完成第二轮。" }, createdAt: "2026-06-01T00:00:04.000Z" },
|
|
{ id: "evt_9", runId: "run_hwlab_adapter", seq: 9, type: "terminal_status", payload: { commandId: "cmd_hwlab_adapter_second", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:05.000Z" }
|
|
] });
|
|
return send({ items: [
|
|
{ id: "evt_1", runId: "run_hwlab_adapter", seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: "cmd_hwlab_adapter", attemptId: "attempt_hwlab_adapter", jobName: "agentrun-v01-runner-hwlab-adapter", namespace: "agentrun-v01" }, createdAt: "2026-06-01T00:00:00.000Z" },
|
|
{ id: "evt_bundle", runId: "run_hwlab_adapter", seq: 2, type: "backend_status", payload: { phase: "resource-bundle-materialized", commandId: "cmd_hwlab_adapter", kind: "git", commitId: "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", toolAliases: { count: 2, names: ["hwpod", "unidesk-ssh"], valuesPrinted: false }, promptRefs: { count: 1, names: ["hwlab-v02-runtime"], valuesPrinted: false }, skillRefs: { count: 2, names: ["device-pod-cli", "hwlab-agent-runtime"], valuesPrinted: false }, initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillRefCount: 2, valuesPrinted: false } }, createdAt: "2026-06-01T00:00:00.250Z" },
|
|
{ id: "evt_tool", runId: "run_hwlab_adapter", seq: 2, type: "tool_call", payload: { method: "item/completed", type: "commandExecution", toolName: "commandExecution", itemId: "call_agentrun_tool", command: "/bin/sh -lc 'hwpod profile list'", status: "completed", exitCode: 0, durationMs: 708, outputSummary: '{"ok":true,"action":"profile.list"}', summary: { outputBytes: 42, outputTruncated: false }, commandId: "cmd_hwlab_adapter", runnerId: "runner_hwlab_adapter", attemptId: "attempt_hwlab_adapter" }, createdAt: "2026-06-01T00:00:00.500Z" },
|
|
{ id: "evt_noise", runId: "run_hwlab_adapter", seq: 3, type: "backend_status", payload: { phase: "thread/status/changed", commandId: "cmd_hwlab_adapter" }, createdAt: "2026-06-01T00:00:00.750Z" },
|
|
{ id: "evt_prompt", runId: "run_hwlab_adapter", seq: 4, type: "backend_status", payload: { phase: "initial-prompt-assembly", commandId: "cmd_hwlab_adapter", initialPromptInjected: true, reason: "thread-start", initialPrompt: { available: true, bytes: 512, sha256: "prompt-sha", promptRefCount: 1, skillRefCount: 2, valuesPrinted: false } }, createdAt: "2026-06-01T00:00:00.875Z" },
|
|
{ id: "evt_2", runId: "run_hwlab_adapter", seq: 4, type: "assistant_message", payload: { commandId: "cmd_hwlab_adapter", text: "AgentRun adapter 已接管 HWLAB Code Agent。" }, createdAt: "2026-06-01T00:00:01.000Z" },
|
|
{ id: "evt_3", runId: "run_hwlab_adapter", seq: 5, type: "terminal_status", payload: { commandId: "cmd_hwlab_adapter", terminalStatus: "completed" }, createdAt: "2026-06-01T00:00:02.000Z" }
|
|
] });
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter/result") {
|
|
return send({
|
|
runId: "run_hwlab_adapter",
|
|
commandId: "cmd_hwlab_adapter",
|
|
attemptId: "attempt_hwlab_adapter",
|
|
runnerId: "runner_hwlab_adapter",
|
|
jobName: "agentrun-v01-runner-hwlab-adapter",
|
|
namespace: "agentrun-v01",
|
|
status: "completed",
|
|
runStatus: "completed",
|
|
commandState: "completed",
|
|
terminalStatus: "completed",
|
|
completed: true,
|
|
reply: "AgentRun adapter 已接管 HWLAB Code Agent。",
|
|
lastSeq: 5,
|
|
eventCount: 5,
|
|
sessionRef: { sessionId: "ses_agentrun_deepseek_server_test_agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" }
|
|
});
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_adapter/commands/cmd_hwlab_adapter_second/result") {
|
|
return send({
|
|
runId: "run_hwlab_adapter",
|
|
commandId: "cmd_hwlab_adapter_second",
|
|
attemptId: "attempt_hwlab_adapter_second",
|
|
runnerId: "runner_hwlab_adapter_second",
|
|
jobName: "agentrun-v01-runner-hwlab-adapter-second",
|
|
namespace: "agentrun-v01",
|
|
status: "completed",
|
|
runStatus: "claimed",
|
|
commandState: "completed",
|
|
terminalStatus: "completed",
|
|
completed: true,
|
|
reply: "AgentRun adapter 复用已有 runner 完成第二轮。",
|
|
lastSeq: 9,
|
|
eventCount: 9,
|
|
sessionRef: { sessionId: "ses_agentrun_deepseek_server_test_agentrun", conversationId: "cnv_server-test-agentrun", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" }
|
|
});
|
|
}
|
|
response.writeHead(404, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: false, failureKind: "schema-invalid", message: `unexpected ${request.method} ${url.pathname}`, traceId: "trc_fake_agentrun" })}\n`);
|
|
});
|
|
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
|
|
|
|
const agentRunPort = agentRunServer.address().port;
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const server = createCloudApiServer({
|
|
traceStore,
|
|
env: {
|
|
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
|
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
|
|
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
|
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
|
HWLAB_CODE_AGENT_DEEPSEEK_MODEL: "deepseek-chat",
|
|
HWLAB_ENVIRONMENT: "v02",
|
|
HWLAB_GITOPS_PROFILE: "v02",
|
|
UNIDESK_MAIN_SERVER_IP: "http://74.48.78.17:18081"
|
|
},
|
|
accessController: {
|
|
required: false,
|
|
async authenticate() {
|
|
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
|
|
},
|
|
async createCodeAgentDevicePodApiKey() {
|
|
return { apiKey: "test-device-pod-api-key" };
|
|
},
|
|
async recordAgentSessionOwner(input) {
|
|
const record = testAgentSessionRecord(input);
|
|
ownerSessions.set(record.id, record);
|
|
return record;
|
|
},
|
|
async getAgentSession(sessionId) {
|
|
return ownerSessions.get(sessionId) ?? null;
|
|
},
|
|
async visibleConversationForActor(actor, conversationId, projectId) {
|
|
assert.equal(actor.id, "usr_agent_owner");
|
|
assert.equal(actor.role, "user");
|
|
assert.equal(conversationId, "cnv_server-test-agentrun");
|
|
assert.equal(projectId, "pikasTech/HWLAB");
|
|
return { conversationId, messages: conversationMessages };
|
|
}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const traceId = "trc_server-test-agentrun-adapter";
|
|
ownerSessions.set("ses_server-test-agentrun", testAgentSessionRecord({
|
|
sessionId: "ses_server-test-agentrun",
|
|
conversationId: "cnv_server-test-agentrun",
|
|
projectId: "pikasTech/HWLAB",
|
|
status: "idle",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445"
|
|
}));
|
|
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", "x-trace-id": traceId, cookie: "hwlab_session=test-stub-session" },
|
|
body: JSON.stringify({
|
|
conversationId: "cnv_server-test-agentrun",
|
|
projectId: "prj_device_pod_workbench",
|
|
sessionId: "ses_server-test-agentrun",
|
|
ownerUserId: "usr_agent_owner",
|
|
ownerRole: "user",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
|
retryOf: "trc_previous-agentrun-web",
|
|
message: "AgentRun adapter smoke"
|
|
})
|
|
});
|
|
assert.equal(submit.status, 202);
|
|
const accepted = await submit.json();
|
|
assert.equal(accepted.shortConnection, true);
|
|
|
|
const payload = await pollAgentResult(port, traceId);
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.provider, "codex-stdio");
|
|
assert.equal(payload.infrastructureBackend, "agentrun-v01/deepseek");
|
|
assert.equal(payload.providerTrace.protocol, "codex-app-server-jsonrpc-stdio");
|
|
assert.equal(payload.providerTrace.command, "codex app-server --listen stdio://");
|
|
assert.equal(payload.providerTrace.terminalStatus, "completed");
|
|
assert.equal(payload.longLivedSessionGate.status, "pass");
|
|
assert.equal(payload.agentRun.runId, "run_hwlab_adapter");
|
|
assert.equal(payload.agentRun.commandId, "cmd_hwlab_adapter");
|
|
assert.equal(payload.agentRun.jobName, "agentrun-v01-runner-hwlab-adapter");
|
|
assert.match(payload.reply.content, /AgentRun adapter/u);
|
|
assert.ok(payload.runnerTrace.events.some((event) => event.label === "agentrun:backend:runner-job-created"));
|
|
const bundleEvent = payload.runnerTrace.events.find((event) => event.label === "agentrun:backend:resource-bundle-materialized");
|
|
assert.deepEqual(bundleEvent?.details?.toolAliases?.names, ["hwpod", "unidesk-ssh"]);
|
|
assert.deepEqual(bundleEvent?.details?.promptRefs?.names, ["hwlab-v02-runtime"]);
|
|
assert.deepEqual(bundleEvent?.details?.skillRefs?.names, ["device-pod-cli", "hwlab-agent-runtime"]);
|
|
const promptEvent = payload.runnerTrace.events.find((event) => event.label === "agentrun:backend:initial-prompt-assembly");
|
|
assert.equal(promptEvent?.details?.initialPromptInjected, true);
|
|
assert.equal(promptEvent?.details?.reason, "thread-start");
|
|
assert.equal(promptEvent?.details?.initialPrompt?.promptRefCount, 1);
|
|
assert.equal(promptEvent?.details?.initialPrompt?.skillRefCount, 2);
|
|
assert.ok(calls.some((call) => call.path === "/api/v1/runs/run_hwlab_adapter/runner-jobs"));
|
|
|
|
const inspectByTrace = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/inspect?traceId=${traceId}`);
|
|
assert.equal(inspectByTrace.status, 200);
|
|
const inspectByTraceBody = await inspectByTrace.json();
|
|
assert.equal(inspectByTraceBody.ok, true);
|
|
assert.equal(inspectByTraceBody.latestTraceId, traceId);
|
|
assert.equal(inspectByTraceBody.session.sessionId, "ses_server-test-agentrun");
|
|
assert.equal(inspectByTraceBody.session.conversationId, "cnv_server-test-agentrun");
|
|
assert.equal(inspectByTraceBody.session.threadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
|
assert.equal(inspectByTraceBody.conversationFacts.conversationId, "cnv_server-test-agentrun");
|
|
assert.equal(inspectByTraceBody.conversationFacts.threadId, "019e8078-db67-7750-a5d9-1a99f3abd445");
|
|
assert.equal(inspectByTraceBody.runnerTrace.traceId, traceId);
|
|
assert.equal(inspectByTraceBody.valuesRedacted, true);
|
|
assert.equal(JSON.stringify(inspectByTraceBody).includes("test-device-pod-api-key"), false);
|
|
|
|
const trace = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/trace/${traceId}`);
|
|
assert.equal(trace.status, 200);
|
|
const traceBody = await trace.json();
|
|
assert.ok(traceBody.events.some((event) => event.runId === "run_hwlab_adapter"));
|
|
assert.ok(traceBody.events.some((event) => event.label === "agentrun:assistant:message"));
|
|
assert.equal(traceBody.events.some((event) => event.details?.initialPromptInjected === true), true);
|
|
const commandTraceEvent = traceBody.events.find((event) => event.label === "item/commandExecution:completed");
|
|
assert.equal(commandTraceEvent.toolName, "commandExecution");
|
|
assert.equal(commandTraceEvent.createdAt, "2026-06-01T00:00:00.500Z");
|
|
assert.match(commandTraceEvent.command, /hwpod profile list/u);
|
|
assert.match(commandTraceEvent.stdoutSummary, /profile\.list/u);
|
|
conversationMessages.push(
|
|
{
|
|
id: "msg_693_user",
|
|
role: "user",
|
|
text: "看看device-pod可用性?",
|
|
status: "sent",
|
|
traceId,
|
|
conversationId: "cnv_server-test-agentrun",
|
|
sessionId: "ses_server-test-agentrun",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
|
createdAt: "2026-06-02T00:00:00.000Z"
|
|
},
|
|
{
|
|
id: "msg_693_agent",
|
|
role: "agent",
|
|
text: "v0.2 device-pod 可用性快照:可用;apiKey=test-device-pod-api-key sk-test-device-secret-000000",
|
|
status: "completed",
|
|
traceId,
|
|
conversationId: "cnv_server-test-agentrun",
|
|
sessionId: "ses_server-test-agentrun",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
|
createdAt: "2026-06-02T00:00:01.000Z"
|
|
}
|
|
);
|
|
|
|
const steer = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/steer`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", "x-trace-id": traceId, cookie: "hwlab_session=test-stub-session" },
|
|
body: JSON.stringify({
|
|
traceId,
|
|
steerTraceId: "trc_steer_server_test",
|
|
conversationId: "cnv_server-test-agentrun",
|
|
projectId: "prj_device_pod_workbench",
|
|
sessionId: "ses_server-test-agentrun",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
|
message: "请按 STEER_MARK 调整最终回复"
|
|
})
|
|
});
|
|
assert.equal(steer.status, 202);
|
|
const steerBody = await steer.json();
|
|
assert.equal(steerBody.accepted, true);
|
|
assert.equal(steerBody.route, "/v1/agent/chat/steer");
|
|
assert.equal(steerBody.traceId, traceId);
|
|
assert.equal(steerBody.steerTraceId, "trc_steer_server_test");
|
|
assert.equal(steerBody.agentRun.runId, "run_hwlab_adapter");
|
|
assert.equal(steerBody.agentRun.targetCommandId, "cmd_hwlab_adapter");
|
|
assert.equal(steerBody.agentRun.steerCommandId, "cmd_hwlab_adapter_steer");
|
|
assert.ok(steerBody.runnerTrace.events.some((event) => event.label === "agentrun:steer:command-created"));
|
|
|
|
const secondTraceId = "trc_server-test-agentrun-adapter-second";
|
|
const second = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", "x-trace-id": secondTraceId, cookie: "hwlab_session=test-stub-session" },
|
|
body: JSON.stringify({
|
|
conversationId: "cnv_server-test-agentrun",
|
|
projectId: "prj_device_pod_workbench",
|
|
sessionId: "ses_server-test-agentrun",
|
|
ownerUserId: "usr_agent_owner",
|
|
ownerRole: "user",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
|
conversationContext: {
|
|
source: "legacy-client",
|
|
messages: [
|
|
{ role: "user", text: "legacy synthetic context must not reach provider" }
|
|
]
|
|
},
|
|
messages: [
|
|
{ role: "assistant", text: "legacy flat messages must not reach provider" }
|
|
],
|
|
message: "AgentRun adapter smoke 第二轮:总结我们刚才的对话内容"
|
|
})
|
|
});
|
|
assert.equal(second.status, 202);
|
|
const secondPayload = await pollAgentResult(port, secondTraceId);
|
|
validateCodeAgentChatSchema(secondPayload);
|
|
assert.equal(secondPayload.status, "completed");
|
|
assert.equal(secondPayload.agentRun.runId, "run_hwlab_adapter");
|
|
assert.equal(secondPayload.agentRun.commandId, "cmd_hwlab_adapter_second");
|
|
assert.equal(secondPayload.providerTrace.commandId, "cmd_hwlab_adapter_second");
|
|
assert.equal(secondPayload.providerTrace.traceId, secondTraceId);
|
|
assert.equal(secondPayload.agentRun.providerTrace.commandId, "cmd_hwlab_adapter_second");
|
|
assert.equal(secondPayload.agentRun.providerTrace.traceId, secondTraceId);
|
|
assert.equal(secondPayload.agentRun.jobName, "agentrun-v01-runner-hwlab-adapter-second");
|
|
assert.equal(secondPayload.sessionReuse.reused, true);
|
|
assert.equal(secondPayload.agentRun.runnerJobCount, 1);
|
|
assert.match(secondPayload.reply.content, /复用已有 runner/u);
|
|
assert.ok(secondPayload.runnerTrace.events.some((event) => event.label === "agentrun:run:reused"));
|
|
assert.ok(secondPayload.runnerTrace.events.some((event) => event.label === "agentrun:runner-job:ensured"));
|
|
assert.equal(secondPayload.runnerTrace.events.some((event) => String(event.text ?? event.message ?? "").includes("旧 command 尾部")), false);
|
|
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs").length, 1);
|
|
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs/run_hwlab_adapter/runner-jobs").length, 2);
|
|
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs/run_hwlab_adapter/commands").length, 3);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
|
}
|
|
});
|
|
|
|
test("cloud api AgentRun adapter reports persistent thread resume when a completed run needs a new runner", async () => {
|
|
const calls = [];
|
|
const hwlabSessionId = "ses_server-test-thread-resume";
|
|
const agentRunSessionId = "ses_agentrun_deepseek_server_test_thread_resume";
|
|
const conversationId = "cnv_server-test-thread-resume";
|
|
const threadId = "019e90e0-9535-7130-a894-47ef4e127206";
|
|
const ownerSessions = new Map([[hwlabSessionId, testAgentSessionRecord({
|
|
sessionId: hwlabSessionId,
|
|
conversationId,
|
|
threadId,
|
|
traceId: "trc_previous_thread_resume",
|
|
status: "idle",
|
|
session: {
|
|
agentRun: {
|
|
adapter: "agentrun-v01",
|
|
backendProfile: "deepseek",
|
|
managerUrl: "http://127.0.0.1:1",
|
|
runId: "run_issue812_previous",
|
|
commandId: "cmd_issue812_previous",
|
|
jobName: "agentrun-v01-runner-issue812-previous",
|
|
namespace: "agentrun-v01",
|
|
sessionId: agentRunSessionId,
|
|
conversationId,
|
|
threadId,
|
|
terminalStatus: "completed",
|
|
runStatus: "completed",
|
|
reuseEligible: true,
|
|
reused: false
|
|
}
|
|
}
|
|
})]]);
|
|
|
|
const agentRunServer = createHttpServer(async (request, response) => {
|
|
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
const chunks = [];
|
|
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : null;
|
|
calls.push({ method: request.method, path: url.pathname, body });
|
|
const send = (data) => {
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_issue812_resume" })}\n`);
|
|
};
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue812_previous") {
|
|
return send({
|
|
id: "run_issue812_previous",
|
|
status: "completed",
|
|
terminalStatus: "completed",
|
|
backendProfile: "deepseek",
|
|
sessionRef: { sessionId: agentRunSessionId, conversationId, threadId, metadata: {} }
|
|
});
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/sessions") {
|
|
assert.equal(body.sessionId, agentRunSessionId);
|
|
assert.equal(body.backendProfile, "deepseek");
|
|
return send({ sessionId: body.sessionId, backendProfile: body.backendProfile });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
|
assert.equal(body.sessionRef.sessionId, agentRunSessionId);
|
|
assert.equal(body.sessionRef.conversationId, conversationId);
|
|
assert.equal(body.sessionRef.threadId, threadId);
|
|
assert.equal(body.sessionRef.metadata.hwlabSessionId, hwlabSessionId);
|
|
assert.equal(body.sessionRef.metadata.threadContinuityPolicy, "hwlab-agentrun-v01-reuse-runner-thread");
|
|
return send({ id: "run_issue812_resume", status: "pending", backendProfile: "deepseek", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_issue812_resume/commands") {
|
|
assert.equal(body.type, "turn");
|
|
assert.equal(body.payload.sessionId, agentRunSessionId);
|
|
assert.equal(body.payload.hwlabSessionId, hwlabSessionId);
|
|
assert.equal(body.payload.threadId, threadId);
|
|
assert.match(body.payload.prompt, /ISSUE812_RESUME/u);
|
|
return send({ id: "cmd_issue812_resume", runId: "run_issue812_resume", state: "pending", type: "turn", seq: 1 });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_issue812_resume/runner-jobs") {
|
|
assert.equal(body.commandId, "cmd_issue812_resume");
|
|
return send({
|
|
action: "create-kubernetes-job",
|
|
runId: "run_issue812_resume",
|
|
commandId: "cmd_issue812_resume",
|
|
attemptId: "attempt_issue812_resume",
|
|
runnerId: "runner_issue812_resume",
|
|
namespace: "agentrun-v01",
|
|
jobName: "agentrun-v01-runner-issue812-resume",
|
|
jobIdentity: { namespace: "agentrun-v01", name: "agentrun-v01-runner-issue812-resume" },
|
|
runner: { attemptId: "attempt_issue812_resume", runnerId: "runner_issue812_resume" }
|
|
});
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue812_resume/events") {
|
|
return send({ items: [
|
|
{ id: "evt_issue812_1", runId: "run_issue812_resume", seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: "cmd_issue812_resume", attemptId: "attempt_issue812_resume", jobName: "agentrun-v01-runner-issue812-resume", namespace: "agentrun-v01" }, createdAt: "2026-06-04T00:00:00.000Z" },
|
|
{ id: "evt_issue812_2", runId: "run_issue812_resume", seq: 2, type: "backend_status", payload: { phase: "initial-prompt-assembly", commandId: "cmd_issue812_resume", initialPromptInjected: false, reason: "thread-resume" }, createdAt: "2026-06-04T00:00:00.500Z" },
|
|
{ id: "evt_issue812_3", runId: "run_issue812_resume", seq: 3, type: "assistant_message", payload: { commandId: "cmd_issue812_resume", text: "ISSUE812_RESUME_OK" }, createdAt: "2026-06-04T00:00:01.000Z" },
|
|
{ id: "evt_issue812_4", runId: "run_issue812_resume", seq: 4, type: "terminal_status", payload: { commandId: "cmd_issue812_resume", terminalStatus: "completed" }, createdAt: "2026-06-04T00:00:02.000Z" }
|
|
] });
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_issue812_resume/commands/cmd_issue812_resume/result") {
|
|
return send({
|
|
runId: "run_issue812_resume",
|
|
commandId: "cmd_issue812_resume",
|
|
attemptId: "attempt_issue812_resume",
|
|
runnerId: "runner_issue812_resume",
|
|
jobName: "agentrun-v01-runner-issue812-resume",
|
|
namespace: "agentrun-v01",
|
|
status: "completed",
|
|
runStatus: "completed",
|
|
commandState: "completed",
|
|
terminalStatus: "completed",
|
|
completed: true,
|
|
reply: "ISSUE812_RESUME_OK",
|
|
lastSeq: 4,
|
|
eventCount: 4,
|
|
sessionRef: { sessionId: agentRunSessionId, conversationId, threadId }
|
|
});
|
|
}
|
|
response.writeHead(404, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: false, failureKind: "schema-invalid", message: `unexpected ${request.method} ${url.pathname}`, traceId: "trc_fake_issue812_resume" })}\n`);
|
|
});
|
|
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
|
|
|
|
const agentRunPort = agentRunServer.address().port;
|
|
for (const record of ownerSessions.values()) {
|
|
record.session.agentRun.managerUrl = `http://127.0.0.1:${agentRunPort}`;
|
|
}
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const server = createCloudApiServer({
|
|
traceStore,
|
|
env: {
|
|
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
|
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
|
|
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
|
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
|
HWLAB_ENVIRONMENT: "v02",
|
|
HWLAB_GITOPS_PROFILE: "v02"
|
|
},
|
|
accessController: {
|
|
required: false,
|
|
async authenticate() {
|
|
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
|
|
},
|
|
async createCodeAgentDevicePodApiKey() {
|
|
return { apiKey: "test-device-pod-api-key" };
|
|
},
|
|
async recordAgentSessionOwner(input) {
|
|
const record = testAgentSessionRecord(input);
|
|
ownerSessions.set(record.id, record);
|
|
return record;
|
|
},
|
|
async getAgentSession(sessionId) {
|
|
return ownerSessions.get(sessionId) ?? null;
|
|
}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const traceId = "trc_server-test-issue812-resume";
|
|
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", "x-trace-id": traceId, cookie: "hwlab_session=test-stub-session" },
|
|
body: JSON.stringify({
|
|
conversationId,
|
|
sessionId: hwlabSessionId,
|
|
ownerUserId: "usr_agent_owner",
|
|
ownerRole: "user",
|
|
threadId,
|
|
message: "ISSUE812_RESUME after completed AgentRun run"
|
|
})
|
|
});
|
|
assert.equal(submit.status, 202);
|
|
|
|
const payload = await pollAgentResult(port, traceId);
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.sessionId, hwlabSessionId);
|
|
assert.equal(payload.threadId, threadId);
|
|
assert.equal(payload.agentRun.runId, "run_issue812_resume");
|
|
assert.equal(payload.agentRun.reused, false);
|
|
assert.equal(payload.agentRun.runnerReused, false);
|
|
assert.equal(payload.agentRun.threadReused, true);
|
|
assert.equal(payload.agentRun.persistentResume, true);
|
|
assert.equal(payload.sessionReuse.threadId, threadId);
|
|
assert.equal(payload.sessionReuse.reused, true);
|
|
assert.equal(payload.sessionReuse.status, "thread-resumed");
|
|
assert.equal(payload.sessionReuse.runnerReused, false);
|
|
assert.equal(payload.sessionReuse.threadReused, true);
|
|
assert.equal(payload.sessionReuse.persistentResume, true);
|
|
assert.equal(payload.reply.content, "ISSUE812_RESUME_OK");
|
|
assert.equal(calls.some((call) => call.method === "GET" && call.path === "/api/v1/runs/run_issue812_previous"), true);
|
|
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs").length, 1);
|
|
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs/run_issue812_resume/runner-jobs").length, 1);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
|
}
|
|
});
|
|
|
|
test("cloud api AgentRun adapter exposes invalid tool-call attribution in result payload", async () => {
|
|
const agentRunServer = createHttpServer(async (request, response) => {
|
|
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
const chunks = [];
|
|
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : null;
|
|
const send = (data) => {
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_agentrun_invalid_tool" })}\n`);
|
|
};
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
|
return send({ id: "run_invalid_tool", status: "pending", backendProfile: "deepseek", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_invalid_tool/commands") {
|
|
return send({ id: "cmd_invalid_tool", runId: "run_invalid_tool", state: "pending", type: "turn", seq: 1 });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_invalid_tool/runner-jobs") {
|
|
return send({
|
|
action: "create-kubernetes-job",
|
|
runId: "run_invalid_tool",
|
|
commandId: "cmd_invalid_tool",
|
|
attemptId: "attempt_invalid_tool",
|
|
runnerId: "runner_invalid_tool",
|
|
namespace: "agentrun-v01",
|
|
jobName: "agentrun-v01-runner-invalid-tool",
|
|
runner: { attemptId: "attempt_invalid_tool", runnerId: "runner_invalid_tool" }
|
|
});
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_invalid_tool/events") {
|
|
return send({ items: [
|
|
{ id: "evt_invalid_tool", runId: "run_invalid_tool", seq: 1, type: "error", payload: { commandId: "cmd_invalid_tool", failureKind: "provider-invalid-tool-call", message: "invalid params, invalid function arguments json string, tool_call_id: call_function_selftest_2 (2013)" }, createdAt: "2026-06-02T00:00:00.000Z" },
|
|
{ id: "evt_invalid_tool_terminal", runId: "run_invalid_tool", seq: 2, type: "terminal_status", payload: { commandId: "cmd_invalid_tool", terminalStatus: "failed", failureKind: "provider-invalid-tool-call" }, createdAt: "2026-06-02T00:00:01.000Z" }
|
|
] });
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_invalid_tool/commands/cmd_invalid_tool/result") {
|
|
return send({
|
|
runId: "run_invalid_tool",
|
|
commandId: "cmd_invalid_tool",
|
|
attemptId: "attempt_invalid_tool",
|
|
runnerId: "runner_invalid_tool",
|
|
jobName: "agentrun-v01-runner-invalid-tool",
|
|
namespace: "agentrun-v01",
|
|
status: "failed",
|
|
runStatus: "failed",
|
|
commandState: "failed",
|
|
terminalStatus: "failed",
|
|
failureKind: "provider-invalid-tool-call",
|
|
failureMessage: "invalid params, invalid function arguments json string, tool_call_id: call_function_selftest_2 (2013)",
|
|
completed: false,
|
|
lastSeq: 2,
|
|
eventCount: 2,
|
|
sessionRef: { sessionId: "ses_agentrun_invalid_tool", conversationId: "cnv_invalid_tool", threadId: "thread_invalid_tool" }
|
|
});
|
|
}
|
|
response.writeHead(404, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: false, failureKind: "schema-invalid", message: `unexpected ${request.method} ${url.pathname}`, traceId: "trc_fake_agentrun_invalid_tool" })}\n`);
|
|
});
|
|
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
|
|
const agentRunPort = agentRunServer.address().port;
|
|
const ownerSessions = new Map([["ses_server-test-invalid-tool", testAgentSessionRecord({
|
|
sessionId: "ses_server-test-invalid-tool",
|
|
conversationId: "cnv_invalid_tool",
|
|
status: "idle"
|
|
})]]);
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
|
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
|
|
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
|
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
|
HWLAB_ENVIRONMENT: "v02",
|
|
HWLAB_GITOPS_PROFILE: "v02"
|
|
},
|
|
accessController: {
|
|
required: false,
|
|
async authenticate() {
|
|
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
|
|
},
|
|
async createCodeAgentDevicePodApiKey() {
|
|
return { apiKey: "test-device-pod-api-key" };
|
|
},
|
|
async recordAgentSessionOwner(input) {
|
|
const record = testAgentSessionRecord(input);
|
|
ownerSessions.set(record.id, record);
|
|
return record;
|
|
},
|
|
async getAgentSession(sessionId) {
|
|
return ownerSessions.get(sessionId) ?? null;
|
|
}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const traceId = "trc_server-test-agentrun-invalid-tool";
|
|
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", "x-trace-id": traceId, cookie: "hwlab_session=test-stub-session" },
|
|
body: JSON.stringify({ conversationId: "cnv_invalid_tool", sessionId: "ses_server-test-invalid-tool", message: "invalid tool-call attribution" })
|
|
});
|
|
assert.equal(submit.status, 202);
|
|
const payload = await pollAgentResult(port, traceId);
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "failed");
|
|
assert.equal(payload.providerTrace.failureKind, "provider-invalid-tool-call");
|
|
assert.equal(payload.error.code, "provider-invalid-tool-call");
|
|
assert.equal(payload.error.category, "provider_invalid_tool_call");
|
|
assert.match(payload.error.userMessage, /无效 tool-call arguments JSON/u);
|
|
assert.match(payload.error.userMessage, /不是 HWLAB device-pod/u);
|
|
assert.equal(payload.blocker.category, "provider_invalid_tool_call");
|
|
assert.match(payload.blocker.summary, /invalid function arguments json string/u);
|
|
assert.equal(payload.session.status, "failed");
|
|
assert.equal(payload.session.lifecycle.requiresNewSession, true);
|
|
assert.equal(payload.sessionReuse.threadId, "thread_invalid_tool");
|
|
assert.equal(payload.agentRun.reuseEligible, false);
|
|
assert.equal(payload.reuseEligible, false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
|
}
|
|
});
|
|
|
|
test("cloud api AgentRun adapter maps minimax-m3 provider profile to AgentRun backend", async () => {
|
|
const calls = [];
|
|
const agentRunServer = createHttpServer(async (request, response) => {
|
|
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
const chunks = [];
|
|
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : null;
|
|
calls.push({ method: request.method, path: url.pathname, body });
|
|
const send = (data) => {
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_agentrun_minimax_m3" })}\n`);
|
|
};
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
|
assert.equal(body.backendProfile, "minimax-m3");
|
|
assert.equal(body.sessionRef.sessionId, "ses_agentrun_minimax_m3_server_test_minimax_m3");
|
|
assert.equal(body.sessionRef.metadata.hwlabSessionId, "ses_server-test-minimax-m3");
|
|
assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "minimax-m3");
|
|
assert.equal(body.sessionRef.metadata.agentRunSessionPolicy, "backend-profile-scoped");
|
|
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].profile, "minimax-m3");
|
|
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.name, "agentrun-v01-provider-minimax-m3");
|
|
assert.equal(body.executionPolicy.secretScope.providerCredentials[0].secretRef.namespace, "agentrun-v01");
|
|
return send({ id: "run_hwlab_minimax_m3", status: "pending", backendProfile: "minimax-m3", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/commands") {
|
|
assert.equal(body.payload.providerProfile, "minimax-m3");
|
|
assert.equal(body.payload.sessionId, "ses_agentrun_minimax_m3_server_test_minimax_m3");
|
|
assert.equal(body.payload.hwlabSessionId, "ses_server-test-minimax-m3");
|
|
assert.match(body.payload.prompt, /MiniMax-M3/u);
|
|
return send({ id: "cmd_hwlab_minimax_m3", runId: "run_hwlab_minimax_m3", state: "pending", type: "turn", seq: 1 });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/runner-jobs") {
|
|
assert.equal(body.commandId, "cmd_hwlab_minimax_m3");
|
|
return send({
|
|
action: "create-kubernetes-job",
|
|
runId: "run_hwlab_minimax_m3",
|
|
commandId: "cmd_hwlab_minimax_m3",
|
|
attemptId: "attempt_hwlab_minimax_m3",
|
|
runnerId: "runner_hwlab_minimax_m3",
|
|
namespace: "agentrun-v01",
|
|
jobName: "agentrun-v01-runner-hwlab-minimax-m3",
|
|
jobIdentity: { namespace: "agentrun-v01", name: "agentrun-v01-runner-hwlab-minimax-m3" },
|
|
runner: { attemptId: "attempt_hwlab_minimax_m3", runnerId: "runner_hwlab_minimax_m3" }
|
|
});
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/events") {
|
|
return send({ items: [
|
|
{ id: "evt_minimax_1", runId: "run_hwlab_minimax_m3", seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: "cmd_hwlab_minimax_m3", attemptId: "attempt_hwlab_minimax_m3", jobName: "agentrun-v01-runner-hwlab-minimax-m3", namespace: "agentrun-v01" }, createdAt: "2026-06-02T00:00:00.000Z" },
|
|
{ id: "evt_minimax_2", runId: "run_hwlab_minimax_m3", seq: 2, type: "assistant_message", payload: { commandId: "cmd_hwlab_minimax_m3", text: "AGENTRUN_MINIMAX_M3_OK" }, createdAt: "2026-06-02T00:00:01.000Z" },
|
|
{ id: "evt_minimax_3", runId: "run_hwlab_minimax_m3", seq: 3, type: "terminal_status", payload: { commandId: "cmd_hwlab_minimax_m3", terminalStatus: "completed" }, createdAt: "2026-06-02T00:00:02.000Z" }
|
|
] });
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_minimax_m3/commands/cmd_hwlab_minimax_m3/result") {
|
|
return send({
|
|
runId: "run_hwlab_minimax_m3",
|
|
commandId: "cmd_hwlab_minimax_m3",
|
|
attemptId: "attempt_hwlab_minimax_m3",
|
|
runnerId: "runner_hwlab_minimax_m3",
|
|
jobName: "agentrun-v01-runner-hwlab-minimax-m3",
|
|
namespace: "agentrun-v01",
|
|
status: "completed",
|
|
runStatus: "completed",
|
|
commandState: "completed",
|
|
terminalStatus: "completed",
|
|
completed: true,
|
|
reply: "AGENTRUN_MINIMAX_M3_OK",
|
|
lastSeq: 3,
|
|
eventCount: 3,
|
|
sessionRef: { sessionId: "ses_agentrun_minimax_m3_server_test_minimax_m3", conversationId: "cnv_server-test-minimax-m3", threadId: null }
|
|
});
|
|
}
|
|
response.writeHead(404, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: false, failureKind: "schema-invalid", message: `unexpected ${request.method} ${url.pathname}`, traceId: "trc_fake_agentrun_minimax_m3" })}\n`);
|
|
});
|
|
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
|
|
|
|
const agentRunPort = agentRunServer.address().port;
|
|
const minimaxOwnerSessions = new Map([["ses_server-test-minimax-m3", testAgentSessionRecord({
|
|
sessionId: "ses_server-test-minimax-m3",
|
|
conversationId: "cnv_server-test-minimax-m3",
|
|
status: "idle"
|
|
})]]);
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
|
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
|
|
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
|
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
|
HWLAB_ENVIRONMENT: "v02",
|
|
HWLAB_GITOPS_PROFILE: "v02"
|
|
},
|
|
accessController: {
|
|
required: false,
|
|
async authenticate() {
|
|
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
|
|
},
|
|
async recordAgentSessionOwner(input) {
|
|
const record = testAgentSessionRecord(input);
|
|
minimaxOwnerSessions.set(record.id, record);
|
|
return record;
|
|
},
|
|
async getAgentSession(sessionId) {
|
|
return minimaxOwnerSessions.get(sessionId) ?? null;
|
|
}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const traceId = "trc_server-test-agentrun-minimax-m3";
|
|
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", "x-trace-id": traceId, cookie: "hwlab_session=test-stub-session" },
|
|
body: JSON.stringify({
|
|
conversationId: "cnv_server-test-minimax-m3",
|
|
sessionId: "ses_server-test-minimax-m3",
|
|
providerProfile: "minimax-m3",
|
|
message: "MiniMax-M3 AgentRun adapter smoke"
|
|
})
|
|
});
|
|
assert.equal(submit.status, 202);
|
|
const accepted = await submit.json();
|
|
assert.equal(accepted.shortConnection, true);
|
|
|
|
const payload = await pollAgentResult(port, traceId);
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.infrastructureBackend, "agentrun-v01/minimax-m3");
|
|
assert.equal(payload.agentRun.backendProfile, "minimax-m3");
|
|
assert.equal(payload.sessionId, "ses_server-test-minimax-m3");
|
|
assert.equal(payload.agentRun.sessionId, "ses_agentrun_minimax_m3_server_test_minimax_m3");
|
|
assert.equal(payload.providerTrace.backendProfile, "minimax-m3");
|
|
assert.equal(payload.agentRun.jobName, "agentrun-v01-runner-hwlab-minimax-m3");
|
|
assert.equal(payload.reply.content, "AGENTRUN_MINIMAX_M3_OK");
|
|
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs").length, 1);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
|
}
|
|
});
|
|
|
|
test("cloud api AgentRun adapter scopes AgentRun sessions by backend profile", async () => {
|
|
const calls = [];
|
|
const hwlabSessionId = "ses_server-test-profile-switch";
|
|
const minimaxSessionId = "ses_agentrun_minimax_m3_server_test_profile_switch";
|
|
const ownerSessions = new Map([[hwlabSessionId, testAgentSessionRecord({
|
|
sessionId: hwlabSessionId,
|
|
conversationId: "cnv_server-test-profile-switch",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
|
traceId: "trc_existing_deepseek",
|
|
status: "active",
|
|
session: {
|
|
agentRun: {
|
|
adapter: "agentrun-v01",
|
|
backendProfile: "deepseek",
|
|
runId: "run_existing_deepseek",
|
|
commandId: "cmd_existing_deepseek",
|
|
jobName: "agentrun-v01-runner-existing-deepseek",
|
|
namespace: "agentrun-v01",
|
|
sessionId: "ses_agentrun_deepseek_server_test_profile_switch",
|
|
reuseEligible: true,
|
|
managerUrl: "http://127.0.0.1:1",
|
|
status: "runner-job-created"
|
|
}
|
|
}
|
|
})]]);
|
|
|
|
const agentRunServer = createHttpServer(async (request, response) => {
|
|
const url = new URL(request.url || "/", "http://127.0.0.1");
|
|
const chunks = [];
|
|
for await (const chunk of request) chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
const body = chunks.length ? JSON.parse(Buffer.concat(chunks).toString("utf8")) : null;
|
|
calls.push({ method: request.method, path: url.pathname, body });
|
|
const send = (data) => {
|
|
response.writeHead(200, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_profile_switch" })}\n`);
|
|
};
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_existing_deepseek") {
|
|
response.writeHead(500, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: false, failureKind: "unexpected-reuse", message: "deepseek run must not be reused for minimax-m3" })}\n`);
|
|
return;
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs") {
|
|
assert.equal(body.backendProfile, "minimax-m3");
|
|
assert.equal(body.sessionRef.sessionId, minimaxSessionId);
|
|
assert.equal(body.sessionRef.metadata.hwlabSessionId, hwlabSessionId);
|
|
assert.equal(body.sessionRef.metadata.agentRunSessionProfile, "minimax-m3");
|
|
return send({ id: "run_hwlab_profile_switch_m3", status: "pending", backendProfile: "minimax-m3", sessionRef: body.sessionRef, resourceBundleRef: body.resourceBundleRef });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/commands") {
|
|
assert.equal(body.payload.providerProfile, "minimax-m3");
|
|
assert.equal(body.payload.sessionId, minimaxSessionId);
|
|
assert.equal(body.payload.hwlabSessionId, hwlabSessionId);
|
|
return send({ id: "cmd_hwlab_profile_switch_m3", runId: "run_hwlab_profile_switch_m3", state: "pending", type: "turn", seq: 1 });
|
|
}
|
|
if (request.method === "POST" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/runner-jobs") {
|
|
assert.equal(body.commandId, "cmd_hwlab_profile_switch_m3");
|
|
return send({
|
|
action: "create-kubernetes-job",
|
|
runId: "run_hwlab_profile_switch_m3",
|
|
commandId: "cmd_hwlab_profile_switch_m3",
|
|
attemptId: "attempt_hwlab_profile_switch_m3",
|
|
runnerId: "runner_hwlab_profile_switch_m3",
|
|
namespace: "agentrun-v01",
|
|
jobName: "agentrun-v01-runner-profile-switch-m3",
|
|
jobIdentity: { namespace: "agentrun-v01", name: "agentrun-v01-runner-profile-switch-m3" },
|
|
runner: { attemptId: "attempt_hwlab_profile_switch_m3", runnerId: "runner_hwlab_profile_switch_m3" }
|
|
});
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/events") {
|
|
return send({ items: [
|
|
{ id: "evt_profile_switch_1", runId: "run_hwlab_profile_switch_m3", seq: 1, type: "backend_status", payload: { phase: "runner-job-created", commandId: "cmd_hwlab_profile_switch_m3", attemptId: "attempt_hwlab_profile_switch_m3", jobName: "agentrun-v01-runner-profile-switch-m3", namespace: "agentrun-v01" }, createdAt: "2026-06-02T00:00:00.000Z" },
|
|
{ id: "evt_profile_switch_2", runId: "run_hwlab_profile_switch_m3", seq: 2, type: "assistant_message", payload: { commandId: "cmd_hwlab_profile_switch_m3", text: "AGENTRUN_HWLAB_MINIMAX_M3_OK" }, createdAt: "2026-06-02T00:00:01.000Z" },
|
|
{ id: "evt_profile_switch_3", runId: "run_hwlab_profile_switch_m3", seq: 3, type: "terminal_status", payload: { commandId: "cmd_hwlab_profile_switch_m3", terminalStatus: "completed" }, createdAt: "2026-06-02T00:00:02.000Z" }
|
|
] });
|
|
}
|
|
if (request.method === "GET" && url.pathname === "/api/v1/runs/run_hwlab_profile_switch_m3/commands/cmd_hwlab_profile_switch_m3/result") {
|
|
return send({
|
|
runId: "run_hwlab_profile_switch_m3",
|
|
commandId: "cmd_hwlab_profile_switch_m3",
|
|
attemptId: "attempt_hwlab_profile_switch_m3",
|
|
runnerId: "runner_hwlab_profile_switch_m3",
|
|
jobName: "agentrun-v01-runner-profile-switch-m3",
|
|
namespace: "agentrun-v01",
|
|
status: "completed",
|
|
runStatus: "completed",
|
|
commandState: "completed",
|
|
terminalStatus: "completed",
|
|
completed: true,
|
|
reply: "AGENTRUN_HWLAB_MINIMAX_M3_OK",
|
|
lastSeq: 3,
|
|
eventCount: 3,
|
|
sessionRef: { sessionId: minimaxSessionId, conversationId: "cnv_server-test-profile-switch", threadId: "019e8078-db67-7750-a5d9-1a99f3abd445" }
|
|
});
|
|
}
|
|
response.writeHead(404, { "content-type": "application/json" });
|
|
response.end(`${JSON.stringify({ ok: false, failureKind: "schema-invalid", message: `unexpected ${request.method} ${url.pathname}`, traceId: "trc_fake_profile_switch" })}\n`);
|
|
});
|
|
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
|
|
|
|
const agentRunPort = agentRunServer.address().port;
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
|
AGENTRUN_MGR_URL: `http://127.0.0.1:${agentRunPort}`,
|
|
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
|
|
HWLAB_CODE_AGENT_AGENTRUN_SOURCE_COMMIT: "0123456789abcdef0123456789abcdef01234567",
|
|
HWLAB_CODE_AGENT_DEFAULT_PROVIDER_PROFILE: "deepseek",
|
|
HWLAB_ENVIRONMENT: "v02",
|
|
HWLAB_GITOPS_PROFILE: "v02"
|
|
},
|
|
accessController: {
|
|
required: false,
|
|
async authenticate() {
|
|
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
|
|
},
|
|
async recordAgentSessionOwner(input) {
|
|
const record = testAgentSessionRecord(input);
|
|
ownerSessions.set(record.id, record);
|
|
return record;
|
|
},
|
|
async getAgentSession(sessionId) {
|
|
return ownerSessions.get(sessionId) ?? null;
|
|
}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const traceId = "trc_server-test-agentrun-profile-switch";
|
|
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", "x-trace-id": traceId, cookie: "hwlab_session=test-stub-session" },
|
|
body: JSON.stringify({
|
|
conversationId: "cnv_server-test-profile-switch",
|
|
sessionId: hwlabSessionId,
|
|
ownerUserId: "usr_agent_owner",
|
|
ownerRole: "user",
|
|
threadId: "019e8078-db67-7750-a5d9-1a99f3abd445",
|
|
providerProfile: "minimax-m3",
|
|
message: "Switch this HWLAB session to MiniMax-M3 through AgentRun"
|
|
})
|
|
});
|
|
assert.equal(submit.status, 202);
|
|
|
|
const payload = await pollAgentResult(port, traceId);
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.sessionId, hwlabSessionId);
|
|
assert.equal(payload.infrastructureBackend, "agentrun-v01/minimax-m3");
|
|
assert.equal(payload.agentRun.backendProfile, "minimax-m3");
|
|
assert.equal(payload.agentRun.runId, "run_hwlab_profile_switch_m3");
|
|
assert.equal(payload.agentRun.sessionId, minimaxSessionId);
|
|
assert.equal(payload.reply.content, "AGENTRUN_HWLAB_MINIMAX_M3_OK");
|
|
assert.equal(calls.some((call) => call.method === "GET" && call.path === "/api/v1/runs/run_existing_deepseek"), false);
|
|
assert.equal(calls.filter((call) => call.method === "POST" && call.path === "/api/v1/runs").length, 1);
|
|
const stored = ownerSessions.get(hwlabSessionId);
|
|
assert.equal(stored.session.agentRun.backendProfile, "minimax-m3");
|
|
assert.equal(stored.session.agentRun.sessionId, minimaxSessionId);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
await new Promise((resolve, reject) => agentRunServer.close((error) => (error ? reject(error) : resolve())));
|
|
}
|
|
});
|
|
|
|
test("cloud api AgentRun adapter rejects non-internal manager URLs by default", async () => {
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
|
AGENTRUN_MGR_URL: "http://74.48.78.17:8080"
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: "cnv_server-test-agentrun-public-url"
|
|
});
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: { "content-type": "application/json", "x-trace-id": "trc_server-test-agentrun-public-url", cookie: manualSession.cookie },
|
|
body: JSON.stringify({ conversationId: manualSession.conversationId, sessionId: manualSession.sessionId, message: "must reject public manager url" })
|
|
});
|
|
assert.equal(response.status, 500);
|
|
const body = await response.json();
|
|
assert.equal(body.error.code, "internal_error");
|
|
assert.match(body.error.reason, /internal k3s Service DNS/u);
|
|
} finally {
|
|
await new Promise((resolve, reject) => server.close((error) => (error ? reject(error) : resolve())));
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat records authenticated owner on agent session", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-owner-"));
|
|
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-owner-codex-home-"));
|
|
const ownerRecords = [];
|
|
const ownerSessions = new Map([["ses_server-test-owner", testAgentSessionRecord({
|
|
sessionId: "ses_server-test-owner",
|
|
conversationId: "cnv_server-test-owner",
|
|
status: "idle"
|
|
})]]);
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
CODEX_HOME: codexHome,
|
|
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
|
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
|
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
|
OPENAI_API_KEY: "test-openai-key-material",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
|
},
|
|
accessController: {
|
|
required: true,
|
|
async authenticate() {
|
|
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
|
|
},
|
|
async recordAgentSessionOwner(input) {
|
|
const record = testAgentSessionRecord(input);
|
|
ownerRecords.push(input);
|
|
ownerSessions.set(record.id, record);
|
|
return record;
|
|
},
|
|
async getAgentSession(sessionId) {
|
|
return ownerSessions.get(sessionId) ?? null;
|
|
}
|
|
},
|
|
codexStdioManager: {
|
|
describe() { return { ...codexStdioReadyFixture({ workspace, codexHome }), sandbox: "danger-full-access" }; },
|
|
async probe() { return { ...codexStdioReadyFixture({ workspace, codexHome }), sandbox: "danger-full-access" }; },
|
|
async chat(params = {}) {
|
|
return {
|
|
...codexStdioChatFixture({ workspace, codexHome, params }),
|
|
sandbox: "danger-full-access"
|
|
};
|
|
},
|
|
cancel() {},
|
|
reapIdle() {}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const payload = await postAgent(port, {
|
|
conversationId: "cnv_server-test-owner",
|
|
sessionId: "ses_server-test-owner",
|
|
traceId: "trc_server-test-owner",
|
|
message: "owner binding smoke"
|
|
});
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.ownerUserId, "usr_agent_owner");
|
|
assert.equal(ownerRecords.length, 1);
|
|
assert.equal(ownerRecords[0].ownerUserId, "usr_agent_owner");
|
|
assert.equal(ownerRecords[0].sessionId, "ses_server_stdio_pwd");
|
|
assert.equal(ownerRecords[0].conversationId, "cnv_server-test-owner");
|
|
assert.equal(ownerRecords[0].traceId, "trc_server-test-owner");
|
|
assert.equal(ownerRecords[0].session.messageCount, 2);
|
|
assert.equal(ownerRecords[0].session.messages.length, 2);
|
|
assert.equal(ownerRecords[0].session.messages[0].role, "user");
|
|
assert.equal(ownerRecords[0].session.messages[0].text, "owner binding smoke");
|
|
assert.equal(ownerRecords[0].session.messages[0].status, "sent");
|
|
assert.equal(ownerRecords[0].session.messages[1].role, "agent");
|
|
assert.equal(ownerRecords[0].session.messages[1].status, "completed");
|
|
assert.equal(ownerRecords[0].session.messages[1].traceId, "trc_server-test-owner");
|
|
assert.equal(ownerRecords[0].session.firstUserMessagePreview, "owner binding smoke");
|
|
assert.equal(ownerRecords[0].session.valuesRedacted, true);
|
|
assert.equal(JSON.stringify(ownerRecords).includes("test-openai-key-material"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
await rm(workspace, { recursive: true, force: true });
|
|
await rm(codexHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat/inspect maps conversation session and thread details to trace evidence", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-inspect-"));
|
|
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-inspect-codex-home-"));
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
CODEX_HOME: codexHome,
|
|
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
|
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
|
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
|
OPENAI_API_KEY: "test-openai-key-material",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
|
},
|
|
codexStdioManager: {
|
|
describe() {
|
|
return {
|
|
...codexStdioReadyFixture({ workspace, codexHome }),
|
|
sandbox: "danger-full-access",
|
|
recentSessions: [{
|
|
sessionId: "ses_server_stdio_pwd",
|
|
conversationId: "cnv_server-test-inspect",
|
|
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
|
|
status: "idle",
|
|
currentTraceId: null,
|
|
lastTraceId: "trc_server-test-inspect",
|
|
workspace,
|
|
sandbox: "danger-full-access",
|
|
secretMaterialStored: false,
|
|
valuesRedacted: true
|
|
}]
|
|
};
|
|
},
|
|
async probe() {
|
|
return {
|
|
...codexStdioReadyFixture({ workspace, codexHome }),
|
|
sandbox: "danger-full-access"
|
|
};
|
|
},
|
|
async chat(params = {}) {
|
|
const base = codexStdioChatFixture({ workspace, codexHome, params });
|
|
return {
|
|
...base,
|
|
sandbox: "danger-full-access",
|
|
session: {
|
|
...base.session,
|
|
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
|
|
sandbox: "danger-full-access"
|
|
},
|
|
sessionReuse: {
|
|
...base.sessionReuse,
|
|
threadId: "019e6c72-2373-75e3-9856-eff286db7163"
|
|
}
|
|
};
|
|
},
|
|
get(sessionId) {
|
|
if (sessionId !== "ses_server_stdio_pwd") return null;
|
|
return {
|
|
sessionId,
|
|
conversationId: "cnv_server-test-inspect",
|
|
threadId: "019e6c72-2373-75e3-9856-eff286db7163",
|
|
status: "idle",
|
|
lastTraceId: "trc_server-test-inspect",
|
|
workspace,
|
|
sandbox: "danger-full-access",
|
|
secretMaterialStored: false,
|
|
valuesRedacted: true
|
|
};
|
|
},
|
|
cancel() {},
|
|
reapIdle() {}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const traceId = "trc_server-test-inspect";
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: "cnv_server-test-inspect"
|
|
});
|
|
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: manualSession.cookie,
|
|
"x-trace-id": traceId,
|
|
"prefer": "respond-async",
|
|
"x-hwlab-short-connection": "1"
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: manualSession.conversationId,
|
|
sessionId: manualSession.sessionId,
|
|
message: "inspect mapping smoke"
|
|
})
|
|
});
|
|
assert.equal(submit.status, 202);
|
|
const payload = await pollAgentResult(port, traceId);
|
|
assert.equal(payload.status, "completed");
|
|
|
|
const inspect = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/inspect?conversationId=cnv_server-test-inspect&sessionId=${manualSession.sessionId}&threadId=019e6c72-2373-75e3-9856-eff286db7163`);
|
|
assert.equal(inspect.status, 200);
|
|
const body = await inspect.json();
|
|
assert.equal(body.ok, true);
|
|
assert.equal(body.status, "found");
|
|
assert.equal(body.latestTraceId, traceId);
|
|
assert.equal(body.traceUrl, `/v1/agent/chat/trace/${traceId}`);
|
|
assert.equal(body.resultUrl, `/v1/agent/chat/result/${traceId}`);
|
|
assert.equal(body.query.sessionId, manualSession.sessionId);
|
|
assert.equal(body.query.threadId, "019e6c72-2373-75e3-9856-eff286db7163");
|
|
assert.equal(body.session.sessionId, "ses_server_stdio_pwd");
|
|
assert.equal(body.session.threadId, "019e6c72-2373-75e3-9856-eff286db7163");
|
|
assert.equal(body.conversationFacts.conversationId, "cnv_server-test-inspect");
|
|
assert.equal(body.runnerTrace.traceId, traceId);
|
|
assert.equal(body.valuesRedacted, true);
|
|
assert.equal(body.secretMaterialStored, false);
|
|
assert.equal(JSON.stringify(body).includes("test-openai-key-material"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
await rm(workspace, { recursive: true, force: true });
|
|
await rm(codexHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat/inspect returns not_found for empty conversation facts", async () => {
|
|
const server = createCloudApiServer();
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/inspect?conversationId=cnv_server-test-inspect-missing&sessionId=ses_server_test_missing&threadId=019e6c72-2373-75e3-9856-eff286db7163&traceId=trc_server_test_missing`);
|
|
assert.equal(response.status, 404);
|
|
const body = await response.json();
|
|
assert.equal(body.ok, false);
|
|
assert.equal(body.status, "not_found");
|
|
assert.equal(body.latestTraceId, null);
|
|
assert.deepEqual(body.traceIds, []);
|
|
assert.equal(body.traceUrl, null);
|
|
assert.equal(body.resultUrl, null);
|
|
assert.equal(body.conversationFacts.turnCount, 0);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api trace returns persisted summary when live trace store has expired", async () => {
|
|
const ownerSessions = new Map();
|
|
ownerSessions.set("ses_issue842_expired", testAgentSessionRecord({
|
|
sessionId: "ses_issue842_expired",
|
|
projectId: "prj_v02_code_agent",
|
|
conversationId: "cnv_issue842_expired",
|
|
threadId: "thread-issue-842-expired",
|
|
traceId: "trc_issue842_expired",
|
|
status: "active",
|
|
session: {
|
|
finalResponse: {
|
|
text: "历史 trace 已过期,但 final response 仍可审计。",
|
|
textChars: 31,
|
|
role: "assistant",
|
|
status: "completed",
|
|
traceId: "trc_issue842_expired",
|
|
source: "code-agent-result",
|
|
valuesPrinted: false
|
|
},
|
|
traceSummary: {
|
|
traceId: "trc_issue842_expired",
|
|
source: "agent-session-snapshot",
|
|
sourceEventCount: 26,
|
|
terminalStatus: "completed",
|
|
noiseEventCount: 4,
|
|
omittedNoiseCount: 4,
|
|
valuesPrinted: false
|
|
},
|
|
agentRun: {
|
|
adapter: "agentrun-v01",
|
|
runId: "run_issue842_expired",
|
|
commandId: "cmd_issue842_expired",
|
|
runnerId: "runner_issue842_expired",
|
|
jobName: "agentrun-v01-runner-issue842-expired",
|
|
namespace: "agentrun-v01",
|
|
terminalStatus: "completed",
|
|
valuesPrinted: false
|
|
},
|
|
valuesRedacted: true
|
|
}
|
|
}));
|
|
const traceStore = createCodeAgentTraceStore();
|
|
const server = createCloudApiServer({
|
|
traceStore,
|
|
env: {
|
|
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
|
|
AGENTRUN_MGR_URL: "http://127.0.0.1:9",
|
|
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1",
|
|
HWLAB_ENVIRONMENT: "v02",
|
|
HWLAB_GITOPS_PROFILE: "v02"
|
|
},
|
|
accessController: {
|
|
required: false,
|
|
async authenticate() {
|
|
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
|
|
},
|
|
async getAgentSessionByTraceId(traceId) {
|
|
return [...ownerSessions.values()].find((session) => session.lastTraceId === traceId) ?? null;
|
|
}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/trace/trc_issue842_expired`, {
|
|
headers: { cookie: "hwlab_session=test-stub-session" }
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const body = await response.json();
|
|
assert.equal(body.ok, true);
|
|
assert.equal(body.status, "expired");
|
|
assert.equal(body.traceStatus, "expired");
|
|
assert.equal(body.eventCount, 0);
|
|
assert.equal(body.fallback.available, true);
|
|
assert.equal(body.finalResponse.text, "历史 trace 已过期,但 final response 仍可审计。");
|
|
assert.equal(body.traceSummary.sourceEventCount, 26);
|
|
assert.equal(body.agentRun.runId, "run_issue842_expired");
|
|
assert.equal(body.agentRun.commandId, "cmd_issue842_expired");
|
|
assert.equal(body.retention.liveTraceStore, "expired-or-evicted");
|
|
assert.equal(JSON.stringify(body).includes("password"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api result polling compacts large runnerTrace while preserving providerTrace", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-"));
|
|
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-result-compact-codex-home-"));
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
CODEX_HOME: codexHome,
|
|
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
|
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
|
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
|
HWLAB_CODE_AGENT_RESULT_TRACE_EVENT_LIMIT: "32",
|
|
OPENAI_API_KEY: "test-openai-key-material",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
|
},
|
|
codexStdioManager: {
|
|
describe() {
|
|
return {
|
|
...codexStdioReadyFixture({ workspace, codexHome }),
|
|
sandbox: "danger-full-access"
|
|
};
|
|
},
|
|
async probe() {
|
|
return {
|
|
...codexStdioReadyFixture({ workspace, codexHome }),
|
|
sandbox: "danger-full-access"
|
|
};
|
|
},
|
|
async chat(params = {}) {
|
|
const base = codexStdioChatFixture({ workspace, codexHome, params });
|
|
const events = Array.from({ length: 160 }, (_, index) => ({
|
|
seq: index + 1,
|
|
traceId: params.traceId,
|
|
type: "assistant_message",
|
|
status: "chunk",
|
|
label: index === 159 ? "assistant:completed" : "assistant:chunk",
|
|
createdAt: "2026-05-23T00:00:00.000Z",
|
|
waitingFor: index === 159 ? null : "turn/completed",
|
|
valuesPrinted: false
|
|
}));
|
|
return {
|
|
...base,
|
|
sandbox: "danger-full-access",
|
|
session: {
|
|
...base.session,
|
|
sandbox: "danger-full-access"
|
|
},
|
|
runnerTrace: {
|
|
...base.runnerTrace,
|
|
events,
|
|
eventLabels: events.map((event) => event.label),
|
|
eventCount: events.length,
|
|
lastEvent: events.at(-1)
|
|
}
|
|
};
|
|
},
|
|
cancel() {},
|
|
reapIdle() {}
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const traceId = "trc_server-test-result-compact";
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: "cnv_server-test-result-compact"
|
|
});
|
|
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: manualSession.cookie,
|
|
"x-trace-id": traceId,
|
|
"prefer": "respond-async",
|
|
"x-hwlab-short-connection": "1"
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: manualSession.conversationId,
|
|
sessionId: manualSession.sessionId,
|
|
message: "生成大量 trace 后返回"
|
|
})
|
|
});
|
|
assert.equal(submit.status, 202);
|
|
|
|
const payload = await pollAgentResult(port, traceId);
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.traceId, traceId);
|
|
assert.equal(payload.providerTrace.protocol, "codex-app-server-jsonrpc-stdio");
|
|
assert.equal(payload.providerTrace.command, "codex app-server --listen stdio://");
|
|
assert.equal(payload.runnerTrace.eventCount, 160);
|
|
assert.equal(payload.runnerTrace.eventsCompacted, true);
|
|
assert.equal(payload.runnerTrace.events.length, 32);
|
|
assert.equal(payload.runnerTrace.events.some((event) => event.label === "trace:compacted"), true);
|
|
assert.equal(payload.runnerTrace.lastEvent.label, "assistant:completed");
|
|
assert.equal(payload.runnerTrace.eventLabels.includes("trace:compacted"), true);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
await rm(workspace, { recursive: true, force: true });
|
|
await rm(codexHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat answers skills prompt through real Codex stdio and retains trace", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-skills-"));
|
|
const codexHome = path.join(workspace, "codex-home");
|
|
const skillsDir = path.join(workspace, "skills");
|
|
const fakeCodex = await createFakeCodexCommand();
|
|
await mkdir(path.join(skillsDir, "hwlab-test-skill"), { recursive: true });
|
|
await prepareFakeCodexHome(codexHome);
|
|
await writeFile(path.join(skillsDir, "hwlab-test-skill", "SKILL.md"), [
|
|
"---",
|
|
"name: hwlab-test-skill",
|
|
"description: Test skill mounted for Codex stdio discovery.",
|
|
"version: v-test",
|
|
"---",
|
|
"",
|
|
"# HWLAB Test Skill"
|
|
].join("\n"));
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
OPENAI_API_KEY: "test-openai-key-material",
|
|
CODEX_HOME: codexHome,
|
|
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
|
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
|
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
|
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
|
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
|
HWLAB_CODE_AGENT_SKILLS_DIRS: skillsDir,
|
|
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
|
},
|
|
codexStdioManager: createCodexStdioSessionManager({
|
|
idFactory: () => "ses_server_stdio_skills",
|
|
createRpcClient: async () => createFakeAppServerClient({
|
|
text: "模型泛化回答不应成为 skills 最终回复。"
|
|
})
|
|
})
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const payload = await postAgent(port, {
|
|
conversationId: "cnv_server_stdio_skills",
|
|
traceId: "trc_server_stdio_skills",
|
|
message: "列出你可用的skills"
|
|
});
|
|
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.provider, "codex-stdio");
|
|
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
|
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
|
|
assert.equal(payload.providerTrace.sidecarOnly, undefined);
|
|
assert.equal(payload.providerTrace.toolName, "codex-app-server.thread/start+turn/start");
|
|
assert.equal(payload.skills.status, "ready");
|
|
const discovered = payload.skills.items.find((skill) => skill.name === "hwlab-test-skill");
|
|
assert.ok(discovered);
|
|
assert.equal(discovered.source, path.join(skillsDir, "hwlab-test-skill", "SKILL.md"));
|
|
assert.equal(discovered.manifest.path, path.join(skillsDir, "hwlab-test-skill", "SKILL.md"));
|
|
assert.equal(discovered.version, "v-test");
|
|
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "completed"));
|
|
assert.match(payload.reply.content, /hwlab-test-skill/u);
|
|
assert.match(payload.reply.content, /真实 skill manifest 清单/u);
|
|
assert.doesNotMatch(payload.reply.content, /模型泛化回答/u);
|
|
assert.ok(payload.runnerTrace.events.some((event) => event.label === "prompt:sent"));
|
|
assert.ok(payload.runnerTrace.events.some((event) => event.label === "tool:codex-app-server.thread/start+turn/start:started"));
|
|
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
await rm(workspace, { recursive: true, force: true });
|
|
await rm(fakeCodex.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat returns structured skills blocker without local skills manifest", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-skills-missing-"));
|
|
const codexHome = path.join(workspace, "codex-home");
|
|
const fakeCodex = await createFakeCodexCommand();
|
|
await prepareFakeCodexHome(codexHome);
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
OPENAI_API_KEY: "test-openai-key-material",
|
|
CODEX_HOME: codexHome,
|
|
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
|
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
|
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
|
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
|
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
|
HWLAB_CODE_AGENT_SKILLS_DIRS: path.join(workspace, "missing-skills"),
|
|
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
|
},
|
|
codexStdioManager: createCodexStdioSessionManager({
|
|
idFactory: () => "ses_server_stdio_skills_missing",
|
|
createRpcClient: async () => createFakeAppServerClient({
|
|
text: "模型泛化回答:alpha-skill"
|
|
})
|
|
})
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const payload = await postAgent(port, {
|
|
conversationId: "cnv_server_stdio_skills_missing",
|
|
traceId: "trc_server_stdio_skills_missing",
|
|
message: "列出你可用的skills"
|
|
});
|
|
|
|
validateCodeAgentChatSchema(payload);
|
|
assert.equal(payload.status, "failed");
|
|
assert.equal(payload.provider, "codex-stdio");
|
|
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
|
assert.equal(payload.capabilityLevel, "blocked");
|
|
assert.equal(payload.error.code, "skills_unavailable");
|
|
assert.equal(payload.skills.status, "blocked");
|
|
assert.equal(payload.skills.blockers[0].code, "skills_unavailable");
|
|
assert.deepEqual(payload.skills.sourceIssues, ["pikasTech/HWLAB#136", "pikasTech/HWLAB#237"]);
|
|
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "blocked"));
|
|
assert.equal(payload.toolCalls.some((toolCall) => toolCall.name === "codex-app-server.thread/start+turn/start"), false);
|
|
assert.equal(payload.session.status, "idle");
|
|
assert.equal(Object.hasOwn(payload, "reply"), false);
|
|
assert.equal(JSON.stringify(payload).includes("alpha-skill"), false);
|
|
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
await rm(workspace, { recursive: true, force: true });
|
|
await rm(fakeCodex.root, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat blocks forbidden file paths without leaking values", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-file-block-"));
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HWLAB_CODE_AGENT_WORKSPACE: workspace
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const payload = await postAgent(port, {
|
|
conversationId: "cnv_server-test-security-block",
|
|
traceId: "trc_server-test-security-block",
|
|
message: "cat ../outside.txt"
|
|
});
|
|
assert.equal(payload.status, "failed");
|
|
assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u);
|
|
assert.deepEqual(payload.toolCalls, []);
|
|
assert.equal(payload.capabilityLevel, "blocked");
|
|
assert.ok(payload.runnerTrace.events.some((event) => event.label === "codex-stdio:blocked"));
|
|
assert.equal(Object.hasOwn(payload, "reply"), false);
|
|
assert.equal(JSON.stringify(payload).includes("sk-"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat/cancel reports unsupported lifecycle degradation", async () => {
|
|
const traceStore = createCodeAgentTraceStore();
|
|
traceStore.append("trc_cancel_unsupported", {
|
|
type: "request",
|
|
status: "running",
|
|
label: "request:running",
|
|
sessionId: "ses_cancel_unsupported",
|
|
sessionStatus: "busy",
|
|
sessionLifecycleStatus: "busy"
|
|
});
|
|
const server = createCloudApiServer({
|
|
traceStore,
|
|
codexStdioManager: {
|
|
get() {
|
|
return null;
|
|
}
|
|
},
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio"
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/cancel`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
"x-trace-id": "trc_cancel_unsupported"
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: "cnv_cancel_unsupported",
|
|
sessionId: "ses_cancel_unsupported",
|
|
traceId: "trc_cancel_unsupported"
|
|
})
|
|
});
|
|
assert.equal(response.status, 501);
|
|
const payload = await response.json();
|
|
assert.equal(payload.status, "degraded");
|
|
assert.equal(payload.unsupported, true);
|
|
assert.equal(payload.degraded, true);
|
|
assert.equal(payload.error.code, "cancel_unsupported");
|
|
assert.equal(payload.sessionLifecycleStatus, "failed");
|
|
assert.equal(payload.sessionSummary.unsupported, true);
|
|
assert.match(payload.sessionSummary.userMessage, /unsupported\/degraded/u);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat reports Codex stdio blocker instead of structured skills fallback", async () => {
|
|
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-no-skills-"));
|
|
const skillsDir = path.join(root, "missing-skills");
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HWLAB_CODE_AGENT_WORKSPACE: root
|
|
},
|
|
skillsDirs: [skillsDir],
|
|
skillsDirsExact: true
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: "cnv_server-test-skills-missing"
|
|
});
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: manualSession.cookie
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: manualSession.conversationId,
|
|
sessionId: manualSession.sessionId,
|
|
message: "列出你可用的skills"
|
|
})
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.status, "failed");
|
|
assert.equal(payload.provider, "codex-stdio");
|
|
assert.notEqual(payload.error.code, "skills_unavailable");
|
|
assert.equal(payload.capabilityLevel, "blocked");
|
|
assert.equal(payload.skills.status, "not_requested");
|
|
assert.deepEqual(payload.toolCalls, []);
|
|
assert.ok(payload.runnerLimitations.includes("no-controlled-readonly-fallback"));
|
|
assert.equal(Object.hasOwn(payload, "reply"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat does not run unsupported local grep fallback", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-tool-blocked-"));
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HWLAB_CODE_AGENT_WORKSPACE: workspace
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: "cnv_server-test-tool-unavailable"
|
|
});
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: manualSession.cookie
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: manualSession.conversationId,
|
|
sessionId: manualSession.sessionId,
|
|
message: "请用grep搜索 package.json"
|
|
})
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.status, "failed");
|
|
assert.notEqual(payload.error.code, "tool_unavailable");
|
|
assert.equal(payload.provider, "codex-stdio");
|
|
assert.equal(payload.capabilityLevel, "blocked");
|
|
assert.deepEqual(payload.toolCalls, []);
|
|
assert.ok(payload.runnerLimitations.includes("no-controlled-readonly-fallback"));
|
|
assert.equal(Object.hasOwn(payload, "reply"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat does not call delayed provider fallback beyond legacy 4500ms UI timeout", async () => {
|
|
let providerCalled = false;
|
|
const server = createCloudApiServer({
|
|
callCodeAgentProvider: async ({ providerPlan, message, traceId }) => {
|
|
providerCalled = true;
|
|
await delay(4700);
|
|
return {
|
|
provider: providerPlan.provider,
|
|
model: providerPlan.model,
|
|
backend: providerPlan.backend,
|
|
content: `延迟真实 provider stub: ${message} / ${traceId}`,
|
|
usage: null,
|
|
providerTrace: {
|
|
source: "delayed-test-provider"
|
|
}
|
|
};
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: "cnv_server-test-agent-chat-delayed"
|
|
});
|
|
const startedAt = Date.now();
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: manualSession.cookie,
|
|
"x-trace-id": "trc_server-test-agent-chat-delayed"
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: manualSession.conversationId,
|
|
sessionId: manualSession.sessionId,
|
|
message: "请用一句话说明当前 HWLAB 工作台可以做什么,稍后回答"
|
|
})
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(Date.now() - startedAt < 4500, true);
|
|
assert.equal(payload.status, "failed");
|
|
assert.equal(payload.conversationId, "cnv_server-test-agent-chat-delayed");
|
|
assert.equal(payload.traceId, "trc_server-test-agent-chat-delayed");
|
|
assert.equal(payload.provider, "codex-stdio");
|
|
assert.equal(payload.capabilityLevel, "blocked");
|
|
assert.equal(providerCalled, false);
|
|
assert.equal(Object.hasOwn(payload, "reply"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat does not call OpenAI provider 502/503 fallback", async () => {
|
|
for (const status of [502, 503]) {
|
|
const providerServer = createHttpServer((request, response) => {
|
|
request.resume();
|
|
response.writeHead(status, { "content-type": "application/json" });
|
|
response.end(JSON.stringify({
|
|
error: {
|
|
message: `upstream ${status}`
|
|
}
|
|
}));
|
|
});
|
|
await new Promise((resolve) => providerServer.listen(0, "127.0.0.1", resolve));
|
|
const providerPort = providerServer.address().port;
|
|
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
OPENAI_API_KEY: "test-openai-key-material",
|
|
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
|
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "true",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: `http://127.0.0.1:${providerPort}/v1/responses`
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: `cnv_server-test-agent-chat-provider-${status}`
|
|
});
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: manualSession.cookie,
|
|
"x-trace-id": `trc_server-test-agent-chat-provider-${status}`
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: manualSession.conversationId,
|
|
sessionId: manualSession.sessionId,
|
|
message: `provider ${status}`
|
|
})
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.status, "failed");
|
|
assert.equal(payload.traceId, `trc_server-test-agent-chat-provider-${status}`);
|
|
assert.match(payload.error.code, /^(codex_cli_binary_missing|codex_cli_native_dependency_missing|stdio_protocol_not_wired)$/u);
|
|
assert.match(payload.error.layer, /^(runner|api)$/u);
|
|
assert.equal(typeof payload.error.retryable, "boolean");
|
|
assert.equal(payload.error.blocker.traceId, `trc_server-test-agent-chat-provider-${status}`);
|
|
assert.equal(typeof payload.error.blocker.retryable, "boolean");
|
|
assert.equal(payload.error.blocker.capabilityLevel, "blocked");
|
|
assert.equal(payload.provider, "codex-stdio");
|
|
assert.equal(Object.hasOwn(payload, "reply"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
await new Promise((resolve, reject) => {
|
|
providerServer.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat does not call empty provider text fallback", async () => {
|
|
let providerCalled = false;
|
|
const server = createCloudApiServer({
|
|
callCodeAgentProvider: async () => {
|
|
providerCalled = true;
|
|
throw new Error("empty provider fallback must not be used");
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const manualSession = await createManualAgentSession(port, {
|
|
conversationId: "cnv_empty-provider-text"
|
|
});
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
|
method: "POST",
|
|
headers: {
|
|
"content-type": "application/json",
|
|
cookie: manualSession.cookie
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: manualSession.conversationId,
|
|
sessionId: manualSession.sessionId,
|
|
message: "你好"
|
|
})
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.conversationId, "cnv_empty-provider-text");
|
|
assert.equal(payload.status, "failed");
|
|
assert.match(payload.error.code, /^(codex_cli_binary_missing|stdio_protocol_not_wired)$/u);
|
|
assert.equal(payload.provider, "codex-stdio");
|
|
assert.equal(Object.hasOwn(payload, "reply"), false);
|
|
assert.equal(providerCalled, false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|