Files
pikasTech-HWLAB/internal/cloud/server-agent-chat-lifecycle.test.ts
T

1647 lines
70 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// SPEC: PJ2026-0104010803 Workbench唯一投影 draft-2026-06-20-p2-terminal-outbox-recovery; PJ2026-01060505 Workbench Performance draft-2026-06-17-p0.
// Responsibility: Cloud API AgentRun read lifecycle, Codex fallback, inspect, and cancel regression tests.
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 { createCloudRuntimeStore } from "../db/runtime-store.ts";
import { validateCodeAgentChatSchema } from "./code-agent-chat.ts";
import { createCodexStdioSessionManager } from "./codex-stdio-session.ts";
import { createCodeAgentTraceStore } from "./code-agent-trace-store.ts";
import { submitAgentRunChatTurn } from "./code-agent-agentrun-adapter.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 trace skips AgentRun refresh when terminal snapshot is already complete (#1422)", async () => {
const calls = [];
const traceId = "trc_issue1422_trace_terminal_snapshot";
const runId = "run_issue1422_trace_terminal_snapshot";
const commandId = "cmd_issue1422_trace_terminal_snapshot";
const finalText = "trace endpoint should reuse the local terminal snapshot.";
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, search: url.search });
response.writeHead(500, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected refresh ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const traceStore = createCodeAgentTraceStore();
traceStore.append(traceId, {
type: "backend",
status: "running",
label: "agentrun:backend:runner-job-created",
source: "agentrun",
sourceSeq: 1,
runId,
commandId,
valuesPrinted: false
});
traceStore.append(traceId, {
type: "result",
status: "completed",
label: "agentrun:terminal:completed",
source: "agentrun",
sourceSeq: 3,
runId,
commandId,
terminal: true,
valuesPrinted: false
});
const codeAgentChatResults = new Map([[traceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId,
conversationId: "cnv_issue1422_trace_terminal_snapshot",
sessionId: "ses_issue1422_trace_terminal_snapshot",
threadId: "thread-issue1422-trace-terminal-snapshot",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", traceId, valuesPrinted: false },
traceSummary: {
traceId,
source: "agentrun-command-result",
sourceEventCount: 3,
terminalStatus: "completed",
agentRun: { runId, commandId, lastSeq: 3, valuesPrinted: false },
valuesPrinted: false
},
agentRun: {
adapter: "agentrun-v01",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
runId,
commandId,
traceId,
lastSeq: 3,
status: "completed",
commandState: "completed",
terminalStatus: "completed",
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
},
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
}]]);
const server = createCloudApiServer({
traceStore,
codeAgentChatResults,
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_PROVIDER_ID: "D601",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
}
}
});
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/traces/${traceId}?limit=1`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.status, "completed");
assert.equal(body.eventCount, 2);
assert.equal(body.agentRun.commandId, commandId);
assert.equal(body.finalResponse.text, finalText);
assert.deepEqual(calls, []);
} 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 trace does not schedule terminal side effects from GET reads (#1519)", async () => {
const ownerCalls = [];
const factCalls = [];
let releaseOwnerWrite;
let ownerWriteReleased = false;
const ownerWrite = new Promise((resolve) => {
releaseOwnerWrite = () => {
if (ownerWriteReleased) return;
ownerWriteReleased = true;
resolve();
};
});
const traceId = "trc_issue1422_trace_effects_async";
const runId = "run_issue1422_trace_effects_async";
const commandId = "cmd_issue1422_trace_effects_async";
const finalText = "trace reads should not wait for terminal side effects.";
const traceStore = createCodeAgentTraceStore();
traceStore.append(traceId, {
type: "backend",
status: "running",
label: "agentrun:backend:run-created",
source: "agentrun",
sourceSeq: 1,
runId,
commandId,
valuesPrinted: false
});
traceStore.append(traceId, {
type: "result",
status: "completed",
label: "agentrun:terminal:completed",
source: "agentrun",
sourceSeq: 3,
runId,
commandId,
terminal: true,
valuesPrinted: false
});
const codeAgentChatResults = new Map([[traceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId,
conversationId: "cnv_issue1422_trace_effects_async",
sessionId: "ses_issue1422_trace_effects_async",
threadId: "thread-issue1422-trace-effects-async",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", traceId, valuesPrinted: false },
traceSummary: {
traceId,
source: "agentrun-command-result",
sourceEventCount: 3,
terminalStatus: "completed",
finalAssistantRow: { role: "assistant", status: "completed", textChars: finalText.length, textPreview: finalText, messageId: `msg_${traceId.slice(4)}`, valuesPrinted: false },
agentRun: { runId, commandId, lastSeq: 3, valuesPrinted: false },
valuesPrinted: false
},
agentRun: {
adapter: "agentrun-v01",
managerUrl: "http://127.0.0.1:1",
runId,
commandId,
traceId,
lastSeq: 3,
status: "completed",
commandState: "completed",
terminalStatus: "completed",
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
},
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
}]]);
const server = createCloudApiServer({
traceStore,
codeAgentChatResults,
env: {
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
sessionRegistry: {
recordFact(conversationId, fact) {
factCalls.push({ conversationId, fact });
return { ok: true };
}
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
},
async recordAgentSessionOwner(input) {
ownerCalls.push(input);
await ownerWrite;
return { ok: true, sessionId: input.sessionId };
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const fetchTrace = async () => {
const response = await Promise.race([
fetch(`http://127.0.0.1:${port}/v1/agent/traces/${traceId}?limit=1`, {
headers: { cookie: "hwlab_session=test-stub-session" }
}),
delay(250).then(() => null)
]);
assert.ok(response, "trace read should not wait for terminal side effects to finish");
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.status, "completed");
assert.equal(body.eventCount, 2);
assert.equal(body.finalResponse.text, finalText);
};
for (let i = 0; i < 2; i += 1) {
await fetchTrace();
}
for (let i = 0; i < 20 && ownerCalls.length < 1; i += 1) await delay(10);
assert.equal(ownerCalls.length, 0);
assert.equal(factCalls.length, 0);
assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects, undefined);
releaseOwnerWrite();
assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects, undefined);
} finally {
releaseOwnerWrite?.();
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api turn status skips AgentRun refresh for complete terminal evidence (#1422)", async () => {
const calls = [];
const traceId = "trc_issue1422_terminal_fast_path";
const runId = "run_issue1422_terminal_fast_path";
const commandId = "cmd_issue1422_terminal_fast_path";
const finalText = "终态结果已经由 AgentRun command result 投影完成。";
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, search: url.search });
response.writeHead(500, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected refresh ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const codeAgentChatResults = new Map([[traceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId,
conversationId: "cnv_issue1422_terminal_fast_path",
sessionId: "ses_issue1422_terminal_fast_path",
threadId: "thread-issue1422-terminal-fast-path",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", traceId, valuesPrinted: false },
traceSummary: {
traceId,
source: "agentrun-command-result",
sourceEventCount: 3,
terminalStatus: "completed",
finalAssistantRow: { role: "assistant", status: "completed", textChars: finalText.length, textPreview: finalText, messageId: `msg_${traceId.slice(4)}`, valuesPrinted: false },
agentRun: { runId, commandId, lastSeq: 3, valuesPrinted: false },
valuesPrinted: false
},
agentRun: {
adapter: "agentrun-v01",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
runId,
commandId,
traceId,
lastSeq: 3,
status: "completed",
commandState: "completed",
terminalStatus: "completed",
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
},
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
}]]);
const server = createCloudApiServer({
traceStore: createCodeAgentTraceStore(),
codeAgentChatResults,
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_PROVIDER_ID: "D601",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
}
}
});
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/turns/${traceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.status, "completed");
assert.equal(body.terminal, true);
assert.equal(body.finalResponse.text, finalText);
assert.equal(body.traceSummary.agentRun.commandId, commandId);
assert.deepEqual(calls, []);
} 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 turn status does not record terminal side effects from GET reads (#1519)", async () => {
const ownerCalls = [];
const factCalls = [];
let releaseOwnerWrite;
let ownerWriteReleased = false;
const ownerWrite = new Promise((resolve) => {
releaseOwnerWrite = () => {
if (ownerWriteReleased) return;
ownerWriteReleased = true;
resolve();
};
});
const traceId = "trc_issue1422_terminal_effects_once";
const runId = "run_issue1422_terminal_effects_once";
const commandId = "cmd_issue1422_terminal_effects_once";
const finalText = "终态副作用只需要收敛一次。";
const codeAgentChatResults = new Map([[traceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId,
conversationId: "cnv_issue1422_terminal_effects_once",
sessionId: "ses_issue1422_terminal_effects_once",
threadId: "thread-issue1422-terminal-effects-once",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
finalResponse: { text: finalText, textChars: finalText.length, role: "assistant", status: "completed", traceId, valuesPrinted: false },
traceSummary: {
traceId,
source: "agentrun-command-result",
sourceEventCount: 3,
terminalStatus: "completed",
finalAssistantRow: { role: "assistant", status: "completed", textChars: finalText.length, textPreview: finalText, messageId: `msg_${traceId.slice(4)}`, valuesPrinted: false },
agentRun: { runId, commandId, lastSeq: 3, valuesPrinted: false },
valuesPrinted: false
},
agentRun: {
adapter: "agentrun-v01",
managerUrl: "http://127.0.0.1:1",
runId,
commandId,
traceId,
lastSeq: 3,
status: "completed",
commandState: "completed",
terminalStatus: "completed",
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
},
providerTrace: { traceId, runId, commandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
}]]);
const server = createCloudApiServer({
traceStore: createCodeAgentTraceStore(),
codeAgentChatResults,
env: {
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
HWLAB_CODE_AGENT_AGENTRUN_PROVIDER_ID: "D601",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
sessionRegistry: {
recordFact(conversationId, fact) {
factCalls.push({ conversationId, fact });
return { ok: true };
}
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
},
async recordAgentSessionOwner(input) {
ownerCalls.push(input);
await ownerWrite;
return { ok: true, sessionId: input.sessionId };
}
}
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const fetchTurnStatus = async () => {
const response = await Promise.race([
fetch(`http://127.0.0.1:${port}/v1/agent/turns/${traceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
}),
delay(250).then(() => null)
]);
assert.ok(response, "turn status should not wait for terminal side effects to finish");
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.status, "completed");
assert.equal(body.finalResponse.text, finalText);
};
for (let i = 0; i < 2; i += 1) {
await fetchTurnStatus();
}
for (let i = 0; i < 20 && ownerCalls.length < 1; i += 1) await delay(10);
assert.equal(ownerCalls.length, 0);
assert.equal(factCalls.length, 0);
assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects, undefined);
releaseOwnerWrite();
assert.equal(codeAgentChatResults.get(traceId).turnStatusTerminalEffects, undefined);
} finally {
releaseOwnerWrite?.();
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api legacy trace read does not repair historical AgentRun trace after lastTraceId advances (#1519)", async () => {
const calls = [];
const ownerSessions = new Map();
const runId = "run_issue955_historical";
const firstTraceId = "trc_issue955_historical_first";
const secondTraceId = "trc_issue955_historical_second";
const firstCommandId = "cmd_issue955_historical_first";
const secondCommandId = "cmd_issue955_historical_second";
const firstFinalText = "目前只有一个 HWPOD 可用:\n\n- d601-f103-v2 (board: D601-F103-V2 / STM32F103)\n\nAPI 返回 count=1, availableCount=1。";
const secondFinalText = "编译成功!hwpod build completed。";
const events = [
{ id: "evt_issue955_hist_first_created", runId, seq: 1, type: "command_created", payload: { commandId: firstCommandId }, createdAt: "2026-06-06T01:43:23.000Z" },
{ id: "evt_issue955_hist_first_tool", runId, seq: 25, type: "tool_call", payload: { method: "item/completed", type: "commandExecution", toolName: "commandExecution", itemId: "call_issue955_hwpod_list", command: "/bin/sh -lc 'hwpod list --available'", status: "completed", exitCode: 0, outputSummary: '{\"count\":1,\"availableCount\":1}', commandId: firstCommandId }, createdAt: "2026-06-06T01:43:46.000Z" },
{ id: "evt_issue955_hist_first_message", runId, seq: 34, type: "assistant_message", payload: { commandId: firstCommandId, itemId: "msg_issue955_first", text: firstFinalText }, createdAt: "2026-06-06T01:46:19.000Z" },
{ id: "evt_issue955_hist_first_done", runId, seq: 35, type: "terminal_status", payload: { commandId: firstCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:46:19.000Z" },
{ id: "evt_issue955_hist_second_created", runId, seq: 36, type: "command_created", payload: { commandId: secondCommandId }, createdAt: "2026-06-06T01:46:11.000Z" },
{ id: "evt_issue955_hist_second_message", runId, seq: 58, type: "assistant_message", payload: { commandId: secondCommandId, itemId: "msg_issue955_second", text: secondFinalText }, createdAt: "2026-06-06T01:46:28.000Z" },
{ id: "evt_issue955_hist_second_done", runId, seq: 67, type: "terminal_status", payload: { commandId: secondCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:46:28.000Z" }
];
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_issue955_historical" })}\n`);
};
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands`) {
return send({ items: [
{ id: firstCommandId, runId, state: "completed", idempotencyKey: firstTraceId, createdAt: "2026-06-06T01:43:30.040Z", updatedAt: "2026-06-06T01:43:46.360Z", payload: { traceId: firstTraceId, conversationId: "cnv_issue955_historical", hwlabSessionId: "ses_issue955_historical", threadId: "thread-issue955-historical", providerProfile: "deepseek" } },
{ id: secondCommandId, runId, state: "completed", idempotencyKey: secondTraceId, createdAt: "2026-06-06T01:46:11.000Z", updatedAt: "2026-06-06T01:46:28.000Z", payload: { traceId: secondTraceId, conversationId: "cnv_issue955_historical", hwlabSessionId: "ses_issue955_historical", threadId: "thread-issue955-historical", providerProfile: "deepseek" } }
] });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${firstCommandId}/result`) {
return send({ runId, commandId: firstCommandId, status: "completed", runStatus: "completed", commandState: "completed", terminalStatus: "completed", completed: true, reply: firstFinalText, lastSeq: 35, eventCount: 35, sessionRef: { sessionId: "ses_agentrun_deepseek_issue955", conversationId: "cnv_issue955_historical", threadId: "thread-issue955-historical" } });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${secondCommandId}/result`) {
return send({ runId, commandId: secondCommandId, status: "completed", runStatus: "completed", commandState: "completed", terminalStatus: "completed", completed: true, reply: secondFinalText, lastSeq: 67, eventCount: 67, sessionRef: { sessionId: "ses_agentrun_deepseek_issue955", conversationId: "cnv_issue955_historical", threadId: "thread-issue955-historical" } });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) {
return send({ items: events });
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
ownerSessions.set("ses_issue955_historical", testAgentSessionRecord({
sessionId: "ses_issue955_historical",
projectId: "prj_v02_code_agent",
conversationId: "cnv_issue955_historical",
threadId: "thread-issue955-historical",
lastTraceId: secondTraceId,
status: "active",
session: {
messages: [
{ id: "msg_issue955_user_first", role: "user", text: "看看有几个hwpod可用", traceId: firstTraceId, status: "submitted" },
{ id: "msg_issue955_user_second", role: "user", text: "试一下编译 d601-f103-v2", traceId: secondTraceId, status: "submitted" },
{ id: "msg_issue955_second", role: "agent", text: firstFinalText, traceId: secondTraceId, status: "idle" }
],
agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: firstCommandId, backendProfile: "deepseek", terminalStatus: "completed", lastSeq: 35, valuesPrinted: false },
finalResponse: { text: firstFinalText, textChars: firstFinalText.length, role: "assistant", status: "completed", traceId: secondTraceId, valuesPrinted: false },
traceSummary: { traceId: secondTraceId, source: "code-agent-derived-session-evidence", sourceEventCount: 36, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 35, valuesPrinted: false }, valuesPrinted: false },
traceResults: {
[secondTraceId]: {
traceId: secondTraceId,
status: "completed",
conversationId: "cnv_issue955_historical",
sessionId: "ses_issue955_historical",
threadId: "thread-issue955-historical",
finalResponse: { text: firstFinalText, textChars: firstFinalText.length, role: "assistant", status: "completed", traceId: secondTraceId, valuesPrinted: false },
traceSummary: { traceId: secondTraceId, source: "agent-session-trace-repair", sourceEventCount: 4, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 35, valuesPrinted: false }, valuesPrinted: false },
agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: firstCommandId, backendProfile: "deepseek", terminalStatus: "completed", lastSeq: 35, valuesPrinted: false },
valuesRedacted: true
}
},
valuesRedacted: true
}
}));
const server = createCloudApiServer({
traceStore: createCodeAgentTraceStore(),
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_PROVIDER_ID: "D601",
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 || session.session?.messages?.some((message) => message.traceId === traceId)) ?? null;
},
async recordAgentSessionOwner(input) {
const existing = ownerSessions.get(input.sessionId) ?? testAgentSessionRecord(input);
const nextSession = { ...(existing.session ?? {}), ...(input.session ?? {}) };
nextSession.traceResults = { ...(existing.session?.traceResults ?? {}), ...(input.session?.traceResults ?? {}) };
const record = testAgentSessionRecord({ ...existing, ...input, session: nextSession });
ownerSessions.set(record.id, record);
return record;
}
}
});
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/traces/${firstTraceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 200);
const body = await response.json();
assert.equal(body.traceId, firstTraceId);
assert.equal(body.status, "missing");
assert.equal(body.projectionStatus, "unknown");
assert.equal(body.eventCount, 0);
assert.deepEqual(calls, []);
assert.equal(ownerSessions.get("ses_issue955_historical").session.traceResults[firstTraceId], undefined);
const secondResultResponse = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/result/${secondTraceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(secondResultResponse.status, 200);
const secondResult = await secondResultResponse.json();
assert.equal(secondResult.traceId, secondTraceId);
assert.equal(secondResult.projectionStatus, "caught-up");
assert.equal(secondResult.agentRun.commandId, firstCommandId);
assert.deepEqual(calls, []);
assert.equal(ownerSessions.get("ses_issue955_historical").session.traceResults[secondTraceId].agentRun.commandId, firstCommandId);
} 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 result polling does not repair polluted completed AgentRun memory cache (#1519)", async () => {
const calls = [];
const runId = "run_issue955_live_cache";
const firstTraceId = "trc_issue955_live_cache_first";
const secondTraceId = "trc_issue955_live_cache_second";
const firstCommandId = "cmd_issue955_live_cache_first";
const secondCommandId = "cmd_issue955_live_cache_second";
const firstFinalText = "目前只有一个 HWPOD 可用:d601-f103-v2。";
const secondFinalText = "编译成功!hwpod build completedKeil MDK USART atk_f103.hex queued。";
const events = [
{ id: "evt_issue955_live_first_message", runId, seq: 10, type: "assistant_message", payload: { commandId: firstCommandId, itemId: "msg_issue955_live_first", text: firstFinalText }, createdAt: "2026-06-06T01:46:19.000Z" },
{ id: "evt_issue955_live_first_done", runId, seq: 11, type: "terminal_status", payload: { commandId: firstCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:46:19.000Z" },
{ id: "evt_issue955_live_second_message", runId, seq: 20, type: "assistant_message", payload: { commandId: secondCommandId, itemId: "msg_issue955_live_second", text: secondFinalText }, createdAt: "2026-06-06T01:46:28.000Z" },
{ id: "evt_issue955_live_second_done", runId, seq: 21, type: "terminal_status", payload: { commandId: secondCommandId, terminalStatus: "completed" }, createdAt: "2026-06-06T01:46:28.000Z" }
];
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname, search: url.search });
const send = (data) => {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data, traceId: "trc_fake_issue955_live_cache" })}\n`);
};
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands`) {
return send({ items: [
{ id: firstCommandId, runId, state: "completed", idempotencyKey: firstTraceId, payload: { traceId: firstTraceId, conversationId: "cnv_issue955_live_cache", hwlabSessionId: "ses_issue955_live_cache", threadId: "thread-issue955-live-cache" } },
{ id: secondCommandId, runId, state: "completed", idempotencyKey: secondTraceId, payload: { traceId: secondTraceId, conversationId: "cnv_issue955_live_cache", hwlabSessionId: "ses_issue955_live_cache", threadId: "thread-issue955-live-cache" } }
] });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${firstCommandId}/result`) {
return send({ runId, commandId: firstCommandId, status: "completed", runStatus: "completed", commandState: "completed", terminalStatus: "completed", completed: true, reply: firstFinalText, lastSeq: 11, eventCount: 11 });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${secondCommandId}/result`) {
return send({ runId, commandId: secondCommandId, status: "completed", runStatus: "completed", commandState: "completed", terminalStatus: "completed", completed: true, reply: secondFinalText, lastSeq: 21, eventCount: 21 });
}
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) {
return send({ items: events });
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const codeAgentChatResults = new Map();
codeAgentChatResults.set(secondTraceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId: secondTraceId,
conversationId: "cnv_issue955_live_cache",
sessionId: "ses_issue955_live_cache",
threadId: "thread-issue955-live-cache",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
reply: { role: "assistant", content: firstFinalText },
finalResponse: { text: firstFinalText, textChars: firstFinalText.length, role: "assistant", status: "completed", traceId: secondTraceId, valuesPrinted: false },
traceSummary: { traceId: secondTraceId, source: "code-agent-derived-session-evidence", sourceEventCount: 11, terminalStatus: "completed", agentRun: { runId, commandId: firstCommandId, lastSeq: 11, valuesPrinted: false }, valuesPrinted: false },
agentRun: {
adapter: "agentrun-v01",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
runId,
commandId: firstCommandId,
traceId: secondTraceId,
lastSeq: 11,
terminalStatus: "completed",
providerTrace: { traceId: secondTraceId, runId, commandId: firstCommandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
},
providerTrace: { traceId: secondTraceId, runId, commandId: firstCommandId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
});
const server = createCloudApiServer({
traceStore: createCodeAgentTraceStore(),
codeAgentChatResults,
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_PROVIDER_ID: "D601",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
}
}
});
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/result/${secondTraceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 200);
const body = await response.json();
const text = JSON.stringify(body);
assert.equal(body.traceId, secondTraceId);
assert.equal(body.status, "completed");
assert.equal(body.projectionStatus, "caught-up");
assert.equal(body.agentRun.commandId, firstCommandId);
assert.equal(body.agentRun.traceId, secondTraceId);
assert.equal(body.terminalEvidence.available, true);
assert.equal(body.terminalEvidence.agentRun.commandId, firstCommandId);
assert.equal(body.terminalEvidence.finalResponse.traceId, secondTraceId);
assert.equal(body.reply.content, firstFinalText);
assert.match(text, /目前只有一个 HWPOD 可用/u);
assert.doesNotMatch(text, /编译成功/u);
assert.equal(text.includes('"fallback"'), false);
assert.deepEqual(calls, []);
assert.equal(codeAgentChatResults.get(secondTraceId).agentRun.commandId, firstCommandId);
} 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 result polling does not query AgentRun registry when projection is stale (#1519)", async () => {
const calls = [];
const runId = "run_issue955_registry_missing";
const traceId = "trc_issue955_registry_missing";
const staleCommandId = "cmd_issue955_registry_stale";
const staleFinalText = "目前只有一个 HWPOD 可用:d601-f103-v2。";
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname });
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands`) {
response.writeHead(200, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: true, data: { items: [] }, traceId: "trc_fake_issue955_missing" })}\n`);
return;
}
response.writeHead(404, { "content-type": "application/json" });
response.end(`${JSON.stringify({ ok: false, message: `unexpected ${request.method} ${url.pathname}` })}\n`);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const codeAgentChatResults = new Map();
codeAgentChatResults.set(traceId, {
accepted: true,
status: "completed",
shortConnection: true,
traceId,
conversationId: "cnv_issue955_registry_missing",
sessionId: "ses_issue955_registry_missing",
threadId: "thread-issue955-registry-missing",
ownerUserId: TEST_AGENT_ACTOR.id,
ownerRole: TEST_AGENT_ACTOR.role,
reply: { role: "assistant", content: staleFinalText },
finalResponse: { text: staleFinalText, textChars: staleFinalText.length, role: "assistant", status: "completed", traceId, valuesPrinted: false },
agentRun: { adapter: "agentrun-v01", managerUrl: `http://127.0.0.1:${agentRunPort}`, runId, commandId: staleCommandId, traceId, terminalStatus: "completed", valuesPrinted: false },
valuesPrinted: false
});
const server = createCloudApiServer({
traceStore: createCodeAgentTraceStore(),
codeAgentChatResults,
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_PROVIDER_ID: "D601",
HWLAB_ENVIRONMENT: "v02",
HWLAB_GITOPS_PROFILE: "v02"
},
accessController: {
required: false,
async authenticate() {
return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION };
}
}
});
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/result/${traceId}`, {
headers: { cookie: "hwlab_session=test-stub-session" }
});
assert.equal(response.status, 200);
const body = await response.json();
const text = JSON.stringify(body);
assert.equal(body.traceId, traceId);
assert.equal(body.status, "completed");
assert.equal(body.projectionStatus, "caught-up");
assert.equal(body.agentRun.commandId, staleCommandId);
assert.match(text, /目前只有一个 HWPOD 可用/u);
assert.deepEqual(calls, []);
assert.equal(calls.some((call) => call.path.includes("/result")), 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 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/cancel rejects AgentRun trace scope mismatch", async () => {
const traceId = "trc_cancel_scope_mismatch";
const codeAgentChatResults = new Map();
const traceStore = createCodeAgentTraceStore();
codeAgentChatResults.set(traceId, {
traceId,
status: "running",
conversationId: "cnv_cancel_scope",
sessionId: "ses_cancel_scope",
threadId: "thread-cancel-scope",
agentRun: {
runId: "run_cancel_scope",
commandId: "cmd_cancel_scope",
sessionId: "ses_agentrun_cancel_scope",
conversationId: "cnv_cancel_scope",
threadId: "thread-cancel-scope",
managerUrl: "http://127.0.0.1:65535",
status: "running",
commandState: "running"
}
});
const server = createCloudApiServer({
traceStore,
codeAgentChatResults,
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
HWLAB_CODE_AGENT_PROVIDER: "agentrun-v01",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
}
});
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": traceId
},
body: JSON.stringify({
conversationId: "cnv_cancel_scope_other",
sessionId: "ses_cancel_scope_other",
threadId: "thread-cancel-scope-other",
traceId
})
});
assert.equal(response.status, 409);
const payload = await response.json();
assert.equal(payload.status, "blocked");
assert.equal(payload.canceled, false);
assert.equal(payload.error.code, "cancel_scope_mismatch");
assert.equal(codeAgentChatResults.get(traceId)?.status, "running");
assert.ok(traceStore.snapshot(traceId)?.events?.some((event) => event.label === "agentrun:cancel:scope_mismatch"));
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat/cancel keeps already terminal AgentRun result instead of overwriting", async () => {
const traceId = "trc_cancel_already_terminal";
const codeAgentChatResults = new Map();
const traceStore = createCodeAgentTraceStore();
codeAgentChatResults.set(traceId, {
traceId,
status: "completed",
finalResponse: "OK",
conversationId: "cnv_cancel_already_terminal",
sessionId: "ses_cancel_already_terminal",
threadId: "thread-cancel-already-terminal",
agentRun: {
runId: "run_cancel_already_terminal",
commandId: "cmd_cancel_already_terminal",
sessionId: "ses_agentrun_cancel_already_terminal",
conversationId: "cnv_cancel_already_terminal",
threadId: "thread-cancel-already-terminal",
status: "completed",
commandState: "completed",
terminalStatus: "completed"
}
});
const accessController = {
required: false,
async authenticate() { return { ok: true, actor: TEST_AGENT_ACTOR, session: TEST_AUTH_SESSION }; }
};
const server = createCloudApiServer({
traceStore,
codeAgentChatResults,
accessController,
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
HWLAB_CODE_AGENT_PROVIDER: "agentrun-v01"
}
});
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": traceId
},
body: JSON.stringify({
conversationId: "cnv_cancel_already_terminal",
sessionId: "ses_cancel_already_terminal",
threadId: "thread-cancel-already-terminal",
traceId
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "completed");
assert.equal(payload.finalResponse, "OK");
assert.equal(payload.cancelDisposition.status, "already_terminal");
assert.equal(payload.cancelDisposition.forwarded, false);
assert.equal(codeAgentChatResults.get(traceId).status, "completed");
assert.ok(traceStore.snapshot(traceId)?.events?.some((event) => event.label === "agentrun:cancel:already-terminal"));
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat/cancel returns already-terminal when AgentRun result completed", async () => {
const calls = [];
const traceId = "trc_cancel_terminal_result_authority";
const runId = "run_cancel_terminal_result_authority";
const commandId = "cmd_cancel_terminal_result_authority";
const finalText = "Cancel did not overwrite the completed AgentRun result.";
const agentRunServer = createHttpServer(async (request, response) => {
const url = new URL(request.url || "/", "http://127.0.0.1");
calls.push({ method: request.method, path: url.pathname });
const send = (body, status = 200) => {
response.writeHead(status, { "content-type": "application/json" });
response.end(`${JSON.stringify(body)}\n`);
};
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/events`) return send({ items: [] });
if (request.method === "GET" && url.pathname === `/api/v1/runs/${runId}/commands/${commandId}/result`) {
return send({ runId, commandId, status: "completed", runStatus: "completed", commandState: "completed", terminalStatus: "completed", completed: true, reply: finalText, lastSeq: 7, eventCount: 7 });
}
if (request.method === "POST" && url.pathname === `/api/v1/commands/${commandId}/cancel`) return send({ ok: true, canceled: true });
return send({ ok: false, message: `unexpected ${request.method} ${url.pathname}` }, 404);
});
await new Promise((resolve) => agentRunServer.listen(0, "127.0.0.1", resolve));
const agentRunPort = agentRunServer.address().port;
const traceStore = createCodeAgentTraceStore();
const codeAgentChatResults = new Map();
codeAgentChatResults.set(traceId, {
traceId,
status: "running",
conversationId: "cnv_cancel_terminal_result_authority",
sessionId: "ses_cancel_terminal_result_authority",
threadId: "thread-cancel-terminal-result-authority",
agentRun: {
adapter: "agentrun-v01",
managerUrl: `http://127.0.0.1:${agentRunPort}`,
runId,
commandId,
traceId,
providerTrace: { traceId, runId, commandId, valuesPrinted: false },
lastSeq: 6,
status: "running",
commandState: "running",
valuesPrinted: false
},
valuesRedacted: true
});
const server = createCloudApiServer({
traceStore,
codeAgentChatResults,
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_ADAPTER: "agentrun-v01",
HWLAB_CODE_AGENT_PROVIDER: "agentrun-v01",
HWLAB_CODE_AGENT_AGENTRUN_ALLOW_NON_K3S_URL: "1"
}
});
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": traceId },
body: JSON.stringify({ traceId, conversationId: "cnv_cancel_terminal_result_authority", sessionId: "ses_cancel_terminal_result_authority", threadId: "thread-cancel-terminal-result-authority" })
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(payload.status, "completed");
assert.equal(payload.cancelDisposition.status, "already_terminal");
assert.equal(payload.cancelDisposition.forwarded, false);
assert.equal(payload.finalResponse.text, finalText);
assert.equal(calls.some((call) => call.method === "POST" && call.path === `/api/v1/commands/${commandId}/cancel`), false);
assert.equal(codeAgentChatResults.get(traceId)?.status, "completed");
} 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 /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|codex_stdio_supervisor_disabled|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()));
});
}
});