test: cover Code Agent slow timeout paths

This commit is contained in:
Code Queue Review
2026-05-23 15:24:06 +00:00
parent 395e2d8656
commit 903a733b2e
5 changed files with 269 additions and 4 deletions
+13 -4
View File
@@ -154,6 +154,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
workspace: options.workspace,
skillsDirs: options.skillsDirs,
skillsDirsExact: options.skillsDirsExact,
skillsDiscoveryDelayMs: options.skillsDiscoveryDelayMs,
sessionRegistry: options.sessionRegistry,
codexStdioManager: options.codexStdioManager
});
@@ -188,6 +189,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
workspace: options.workspace,
skillsDirs: options.skillsDirs,
skillsDirsExact: options.skillsDirsExact,
skillsDiscoveryDelayMs: options.skillsDiscoveryDelayMs,
sessionRegistry: options.sessionRegistry,
codexStdioManager: options.codexStdioManager
});
@@ -318,7 +320,7 @@ export async function handleCodeAgentChat(params = {}, options = {}) {
if (error.longLivedSessionGate !== undefined) payload.longLivedSessionGate = error.longLivedSessionGate;
if (error.availability !== undefined) {
payload.availability = error.availability;
} else if (error.code === "provider_unavailable") {
} else if (["provider_unavailable", "provider_timeout"].includes(error.code)) {
payload.availability = describeCodeAgentAvailability(options.env ?? process.env, options);
} else if (["runner_unavailable", "tool_unavailable", "skills_unavailable", "security_blocked", "codex_stdio_blocked", "codex_stdio_failed", "codex_stdio_protocol_blocked", "codex_stdio_empty_response"].includes(error.code)) {
payload.availability = describeCodeAgentAvailability(options.env ?? process.env, options);
@@ -779,7 +781,7 @@ async function callCodexStdioRunner({ message, conversationId, sessionId, traceI
}
}
async function callReadOnlyRunner({ intent, conversationId, sessionId, traceId, env, now, workspace, skillsDirs, skillsDirsExact, sessionRegistry, codexStdioManager }) {
async function callReadOnlyRunner({ intent, conversationId, sessionId, traceId, env, now, workspace, skillsDirs, skillsDirsExact, skillsDiscoveryDelayMs, sessionRegistry, codexStdioManager }) {
const resolvedWorkspace = resolveRunnerWorkspace(env, { workspace });
const registry = resolveCodeAgentSessionRegistry({ sessionRegistry });
const sessionAcquire = registry.acquire({
@@ -1028,7 +1030,7 @@ async function callReadOnlyRunner({ intent, conversationId, sessionId, traceId,
}
if (intent.kind === "skills") {
const skills = await discoverSkills({ env, skillsDirs, skillsDirsExact, traceId });
const skills = await discoverSkills({ env, skillsDirs, skillsDirsExact, traceId, delayMs: skillsDiscoveryDelayMs });
if (skills.status === "blocked") {
session = registry.fail(session.sessionId, {
now,
@@ -1830,7 +1832,8 @@ function assertReadOnlyToolCompleted(toolCall, { workspace, skills, runner, runn
});
}
async function discoverSkills({ env, skillsDirs, skillsDirsExact, traceId }) {
async function discoverSkills({ env, skillsDirs, skillsDirsExact, traceId, delayMs = 0 }) {
await maybeDelayForTest(delayMs);
const checkedDirs = resolveSkillDirs(env, { skillsDirs, skillsDirsExact });
const sourceSummaries = [];
const items = [];
@@ -2559,6 +2562,12 @@ function runnerCommandEnv(env = process.env) {
};
}
async function maybeDelayForTest(value) {
const delayMs = Number.parseInt(value ?? "", 10);
if (!Number.isInteger(delayMs) || delayMs <= 0) return;
await new Promise((resolve) => setTimeout(resolve, Math.min(delayMs, 10000)));
}
function boundToolOutput(value, maxLength = READONLY_TOOL_OUTPUT_LIMIT) {
const text = String(value ?? "");
if (text.length <= maxLength) return { text, truncated: false };
@@ -563,6 +563,99 @@ test("Code Agent M3 Skill CLI missing API base returns structured config blocker
assert.equal(JSON.stringify(payload).includes("://"), false);
});
test("Code Agent M3 Skill CLI preserves slow structured blocker beyond legacy 4500ms window", async () => {
const startedAt = Date.now();
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_m3_skill_slow_blocker",
traceId: "trc_m3_skill_slow_blocker",
message: "通过 HWLAB API 读取 M3 DI1 并返回结构化 blocker"
},
{
now: () => "2026-05-23T00:06:30.000Z",
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
OPENAI_API_KEY: "must-not-be-used"
},
callProvider: async () => {
throw new Error("OpenAI fallback must not be used for M3 IO blockers");
},
m3IoSkillRequestJson: async (url, request) => {
assert.equal(new URL(url).pathname, HWLAB_M3_IO_API_ROUTE);
assert.equal(request.timeoutMs, 30000);
await delay(4600);
return {
ok: true,
status: 200,
body: {
serviceId: "hwlab-cloud-api",
contractVersion: "m3-io-control-v1",
status: "blocked",
accepted: false,
action: "di.read",
traceId: "trc_m3_skill_slow_blocker",
operationId: "op_m3_di_read_slow_blocker",
auditId: "aud_m3_di_read_slow_blocker_failed",
evidenceId: "evd_m3_di_read_slow_blocker_failed",
blocker: {
code: "runtime_durable_not_green",
message: "runtime durable evidence is not green",
zh: "runtime durable evidence 尚未为 green"
},
blockerClassification: {
category: "runtime_durability",
reason: "durable evidence is blocked"
},
durableStatus: {
status: "degraded",
durable: false,
blocker: "runtime_durable_not_green"
},
evidenceState: {
status: "blocked",
sourceKind: "BLOCKED",
blocker: "runtime_durable_not_green",
writeStatus: "not_written"
},
controlPath: {
cloudApi: true,
gatewaySimu: false,
boxSimu: false,
patchPanel: false,
frontendBypass: false
}
}
};
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(Date.now() - startedAt >= 4500, true);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "hwlab-skill-cli");
assert.equal(payload.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli");
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(payload.sessionMode, "controlled-m3-io-skill-cli");
assert.equal(payload.runner.kind, "hwlab-m3-io-skill-cli");
assert.equal(payload.blocker.code, "m3_readiness_blocked");
assert.equal(payload.blocker.layer, "m3-readiness");
assert.equal(payload.blocker.retryable, false);
assert.equal(payload.toolCalls[0].status, "blocked");
assert.equal(payload.toolCalls[0].blocker.code, "runtime_durable_not_green");
assert.equal(payload.toolCalls[0].traceId, "trc_m3_skill_slow_blocker");
assert.equal(payload.skills.status, "used");
assert.equal(payload.skills.blockers[0].code, "runtime_durable_not_green");
assert.equal(payload.runnerTrace.traceId, "trc_m3_skill_slow_blocker");
assert.equal(payload.runnerTrace.blocker.code, "runtime_durable_not_green");
assert.equal(payload.providerTrace.fallbackUsed, false);
assert.equal(payload.providerTrace.traceId, "trc_m3_skill_slow_blocker");
assert.equal(Object.hasOwn(payload, "error"), false);
assert.match(payload.reply.content, /Blocker: runtime_durable_not_green/u);
});
test("Code Agent M3 Skill CLI HWLAB API unavailable returns retryable structured blocker", async () => {
const payload = await handleCodeAgentChat(
{
@@ -728,6 +821,10 @@ test("Code Agent blocks direct gateway or patch-panel requests instead of using
assert.match(direct.error.message, /Skill CLI -> HWLAB API \/v1\/m3\/io/u);
});
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
test("Code Agent M3 Skill CLI blocks missing service-local HWLAB API base URL before loopback fallback", async () => {
const payload = await handleCodeAgentChat(
{
+1
View File
@@ -336,6 +336,7 @@ async function handleCodeAgentChatHttp(request, response, options) {
workspace: options.workspace,
skillsDirs: options.skillsDirs,
skillsDirsExact: options.skillsDirsExact,
skillsDiscoveryDelayMs: options.skillsDiscoveryDelayMs,
sessionRegistry: options.sessionRegistry,
m3IoSkillRequestJson: options.m3IoSkillRequestJson,
codexStdioManager: options.codexStdioManager
+73
View File
@@ -1351,6 +1351,73 @@ test("cloud api /v1/agent/chat discovers skills manifest with source and version
}
});
test("cloud api /v1/agent/chat keeps slow skills discovery running past legacy 4500ms timeout", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-slow-skills-"));
const workspace = path.join(root, "workspace");
const skillsDir = path.join(root, "skills");
await mkdir(workspace, { recursive: true });
await mkdir(path.join(skillsDir, "slow-skill"), { recursive: true });
await writeFile(path.join(skillsDir, "slow-skill", "SKILL.md"), [
"---",
"name: slow-skill",
"description: Slow skill discovery summary.",
"version: 2026.05.23",
"commit: slow12345678",
"---",
"",
"# Slow"
].join("\n"));
const server = createCloudApiServer({
env: {
PATH: process.env.PATH,
HWLAB_CODE_AGENT_WORKSPACE: workspace
},
skillsDirs: [skillsDir],
skillsDirsExact: true,
skillsDiscoveryDelayMs: 4600
});
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
try {
const { port } = server.address();
const startedAt = Date.now();
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-slow-skills"
},
body: JSON.stringify({
conversationId: "cnv_server-test-slow-skills",
message: "请列出你可用的所有 skills"
})
});
assert.equal(response.status, 200);
const payload = await response.json();
assert.equal(Date.now() - startedAt >= 4500, true);
assert.equal(payload.status, "completed");
assert.equal(payload.provider, "codex-readonly-runner");
assert.equal(payload.backend, "hwlab-cloud-api/codex-readonly-runner");
assert.equal(payload.traceId, "trc_server-test-slow-skills");
assert.equal(payload.capabilityLevel, "read-only-session-tools");
assert.equal(payload.sessionMode, "controlled-readonly-session-registry");
assert.equal(payload.toolCalls[0].name, "skills.discover");
assert.equal(payload.toolCalls[0].status, "completed");
assert.equal(payload.skills.status, "ready");
assert.equal(payload.skills.items[0].name, "slow-skill");
assert.equal(payload.skills.items[0].traceId, "trc_server-test-slow-skills");
assert.equal(payload.runner.kind, "hwlab-readonly-runner");
assert.equal(payload.runnerTrace.traceId, "trc_server-test-slow-skills");
assert.ok(payload.runnerTrace.events.includes("tool:skills.discover:completed"));
assert.equal(Object.hasOwn(payload, "error"), false);
} finally {
await new Promise((resolve, reject) => {
server.close((error) => (error ? reject(error) : resolve()));
});
}
});
test("cloud api /v1/agent/chat reports structured skills_unavailable blocker", async () => {
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-no-skills-"));
const skillsDir = path.join(root, "missing-skills");
@@ -1629,6 +1696,8 @@ test("cloud api /v1/agent/chat keeps delayed provider failure structured beyond
assert.equal(payload.error.code, "provider_unavailable");
assert.equal(payload.error.providerStatus, 503);
assert.match(payload.error.message, /HTTP 503/u);
assert.equal(payload.backend, "hwlab-cloud-api/openai-responses");
assert.equal(payload.availability.fallback.backend, "hwlab-cloud-api/openai-responses");
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
@@ -1681,6 +1750,10 @@ test("cloud api /v1/agent/chat reports provider timeout as failed without a repl
assert.equal(payload.error.retryable, true);
assert.match(payload.error.userMessage, /超时/u);
assert.match(payload.error.message, /timed out after 50ms/u);
assert.equal(payload.provider, "openai-responses");
assert.equal(payload.backend, "hwlab-cloud-api/openai-responses");
assert.equal(payload.availability.endpoint, "POST /v1/agent/chat");
assert.equal(payload.availability.runner.kind, "hwlab-readonly-runner");
assert.equal(Object.hasOwn(payload, "reply"), false);
} finally {
await new Promise((resolve, reject) => {
@@ -226,3 +226,88 @@ test("M3 Skill CLI reports HWLAB API unavailable as retryable structured blocker
assert.equal(result.error.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(result.error.toolName, "hwlab-agent-runtime.m3-io");
});
test("M3 Skill CLI preserves slow structured blocker and timeout budget", async () => {
const startedAt = Date.now();
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"di.read",
"--api-base-url",
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"--trace-id",
"trc_skill_cli_slow_blocker",
"--request-id",
"req_skill_cli_slow_blocker",
"--timeout-ms",
"30000"
],
{
now: () => "2026-05-23T00:09:00.000Z",
requestJson: async (url, request) => {
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`);
assert.equal(request.timeoutMs, 30000);
await delay(4600);
return {
ok: true,
status: 200,
body: {
status: "blocked",
accepted: false,
traceId: "trc_skill_cli_slow_blocker",
operationId: "op_skill_cli_slow_blocker",
auditId: "aud_skill_cli_slow_blocker_failed",
evidenceId: "evd_skill_cli_slow_blocker_failed",
blocker: {
code: "runtime_durable_not_green",
message: "runtime durable evidence is not green",
zh: "runtime durable evidence 尚未为 green"
},
blockerClassification: {
category: "runtime_durability"
},
evidenceState: {
status: "blocked",
sourceKind: "BLOCKED",
blocker: "runtime_durable_not_green",
writeStatus: "not_written"
},
durableStatus: {
status: "degraded",
durable: false,
blocker: "runtime_durable_not_green"
},
controlPath: {
cloudApi: true,
frontendBypass: false
}
}
};
}
}
);
assert.equal(Date.now() - startedAt >= 4500, true);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(result.traceId, "trc_skill_cli_slow_blocker");
assert.equal(result.requestId, "req_skill_cli_slow_blocker");
assert.equal(result.operationId, "op_skill_cli_slow_blocker");
assert.equal(result.blocker.code, "runtime_durable_not_green");
assert.equal(result.blocker.category, "runtime_durability");
assert.equal(result.capabilityBlocker.code, "runtime_durable_not_green");
assert.equal(result.trustBlocker.code, "runtime_durable_not_green");
assert.equal(result.blockers.some((blocker) => blocker.code === "runtime_durable_not_green"), true);
assert.equal(result.audit.auditId, "aud_skill_cli_slow_blocker_failed");
assert.equal(result.evidence.evidenceId, "evd_skill_cli_slow_blocker_failed");
assert.equal(result.durable.blocker, "runtime_durable_not_green");
assert.equal(result.safety.fallbackUsed, false);
assert.equal(result.safety.directGatewayCalls, false);
});
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}