import assert from "node:assert/strict"; import test from "node:test"; import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.mjs"; import { handleCodeAgentChat, validateCodeAgentChatSchema } from "./code-agent-chat.mjs"; import { createCodexStdioSessionManager } from "./codex-stdio-session.mjs"; import { classifyCodexRunnerCapability, classifyCodeAgentChatReadiness } from "../../scripts/src/code-agent-response-contract.mjs"; import { HWLAB_M3_IO_API_ROUTE } from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs"; test("code agent session registry creates, reuses, and expires sessions without storing secrets", () => { const registry = createCodeAgentSessionRegistry({ idleTimeoutMs: 1000, idFactory: () => "ses_test-registry" }); const first = registry.acquire({ conversationId: "cnv_registry", workspace: "/workspace/hwlab", sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_registry_1", now: "2026-05-23T00:00:00.000Z" }); assert.equal(first.ok, true); assert.equal(first.reused, false); assert.equal(first.session.sessionId, "ses_test-registry"); assert.equal(first.session.status, "busy"); assert.equal(first.session.idleTimeoutMs, 1000); assert.equal(first.session.lastTraceId, "trc_registry_1"); assert.equal(first.session.secretMaterialStored, false); assert.equal(first.session.valuesRedacted, true); registry.release(first.session.sessionId, { conversationId: "cnv_registry", traceId: "trc_registry_1", reused: first.session.reused, now: "2026-05-23T00:00:00.010Z" }); const second = registry.acquire({ conversationId: "cnv_registry", workspace: "/workspace/hwlab", sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_registry_2", now: "2026-05-23T00:00:00.500Z" }); assert.equal(second.ok, true); assert.equal(second.reused, true); assert.equal(second.session.sessionId, "ses_test-registry"); assert.equal(second.session.turn, 2); assert.equal(second.session.lastTraceId, "trc_registry_2"); registry.release(second.session.sessionId, { conversationId: "cnv_registry", traceId: "trc_registry_2", reused: second.session.reused, now: "2026-05-23T00:00:00.510Z" }); const expired = registry.acquire({ conversationId: "cnv_registry", workspace: "/workspace/hwlab", sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_registry_expired", now: "2026-05-23T00:00:02.000Z" }); assert.equal(expired.ok, false); assert.equal(expired.code, "session_expired"); assert.equal(expired.session.status, "expired"); assert.equal(expired.blocker.sourceIssue, "pikasTech/HWLAB#317"); }); test("code agent session registry reports busy sessions as structured blocker", () => { const registry = createCodeAgentSessionRegistry({ idFactory: () => "ses_busy" }); const first = registry.acquire({ conversationId: "cnv_busy", workspace: "/workspace/hwlab", sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_busy_1", now: "2026-05-23T00:00:00.000Z" }); assert.equal(first.ok, true); const busy = registry.acquire({ conversationId: "cnv_busy", workspace: "/workspace/hwlab", sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_busy_2", now: "2026-05-23T00:00:01.000Z" }); assert.equal(busy.ok, false); assert.equal(busy.code, "session_busy"); assert.equal(busy.session.status, "busy"); assert.equal(busy.session.currentTraceId, "trc_busy_1"); }); test("read-only runner returns session registry evidence and blocked long-lived gate", async () => { const registry = createCodeAgentSessionRegistry({ idFactory: () => "ses_chat_registry" }); const first = await handleCodeAgentChat( { conversationId: "cnv_chat_registry", traceId: "trc_chat_registry_pwd", message: "pwd" }, { now: () => "2026-05-23T00:01:00.000Z", sessionRegistry: registry, env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); validateCodeAgentChatSchema(first); assert.equal(first.status, "completed"); assert.equal(first.sessionId, "ses_chat_registry"); assert.equal(first.session.sessionId, "ses_chat_registry"); assert.equal(first.session.status, "idle"); assert.equal(first.session.workspace, process.cwd()); assert.equal(first.session.lastTraceId, "trc_chat_registry_pwd"); assert.equal(first.capabilityLevel, "read-only-session-tools"); assert.equal(first.longLivedSessionGate.status, "blocked"); assert.equal(first.longLivedSessionGate.pass, false); assert.ok(first.longLivedSessionGate.blockers.some((blocker) => blocker.code === "controlled_readonly_not_long_lived_stdio")); const second = await handleCodeAgentChat( { conversationId: "cnv_chat_registry", traceId: "trc_chat_registry_ls", message: "ls ." }, { now: () => "2026-05-23T00:01:01.000Z", sessionRegistry: registry, env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); assert.equal(second.status, "completed"); assert.equal(second.sessionId, first.sessionId); assert.equal(second.sessionReuse.reused, true); assert.equal(second.sessionReuse.turn, 2); assert.equal(second.session.lastTraceId, "trc_chat_registry_ls"); assert.equal(classifyCodexRunnerCapability(second, { httpStatus: 200 }).capabilityPass, false); }); test("expired and busy session states are surfaced as structured blockers", async () => { const expiredRegistry = createCodeAgentSessionRegistry({ idleTimeoutMs: 10, idFactory: () => "ses_expired_chat" }); const first = expiredRegistry.acquire({ conversationId: "cnv_expired_chat", workspace: process.cwd(), sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_expired_seed", now: "2026-05-23T00:02:00.000Z" }); expiredRegistry.release(first.session.sessionId, { conversationId: "cnv_expired_chat", traceId: "trc_expired_seed", now: "2026-05-23T00:02:00.001Z" }); const expired = await handleCodeAgentChat( { conversationId: "cnv_expired_chat", traceId: "trc_expired_request", message: "pwd" }, { now: () => "2026-05-23T00:02:01.000Z", sessionRegistry: expiredRegistry, env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); assert.equal(expired.status, "failed"); assert.equal(expired.error.code, "session_expired"); assert.equal(expired.session.status, "expired"); assert.equal(expired.capabilityLevel, "blocked"); assert.equal(expired.longLivedSessionGate.status, "blocked"); const busyRegistry = createCodeAgentSessionRegistry({ idFactory: () => "ses_busy_chat" }); busyRegistry.acquire({ conversationId: "cnv_busy_chat", workspace: process.cwd(), sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_busy_seed", now: "2026-05-23T00:03:00.000Z" }); const busy = await handleCodeAgentChat( { conversationId: "cnv_busy_chat", traceId: "trc_busy_request", message: "pwd" }, { now: () => "2026-05-23T00:03:01.000Z", sessionRegistry: busyRegistry, env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); assert.equal(busy.status, "failed"); assert.equal(busy.error.code, "session_busy"); assert.equal(busy.session.status, "busy"); assert.equal(busy.capabilityLevel, "blocked"); assert.equal(busy.error.blockers[0].sourceIssue, "pikasTech/HWLAB#317"); }); test("OpenAI fallback and codex one-shot do not pass the long-lived session gate", async () => { const fallback = await handleCodeAgentChat( { conversationId: "cnv_fallback_gate", traceId: "trc_fallback_gate", message: "你好" }, { now: () => "2026-05-23T00:04:00.000Z", callProvider: async ({ providerPlan }) => ({ provider: providerPlan.provider, model: providerPlan.model, backend: providerPlan.backend, content: "普通文本回复。", usage: null, providerTrace: { source: "test-provider" } }) } ); assert.equal(fallback.status, "completed"); assert.equal(fallback.runner.kind, "openai-responses-fallback"); assert.equal(fallback.capabilityLevel, "text-chat-only"); assert.equal(fallback.longLivedSessionGate.status, "blocked"); assert.ok(fallback.longLivedSessionGate.blockers.some((blocker) => blocker.code === "openai_responses_fallback_not_session")); assert.equal(classifyCodexRunnerCapability(fallback, { httpStatus: 200 }).capabilityPass, false); const oneShot = { ...fallback, provider: "codex-cli", backend: "hwlab-cloud-api/codex-cli", runner: { kind: "codex-cli-one-shot-ephemeral", codexStdio: false, writeCapable: false, durableSession: false }, sessionMode: "ephemeral-one-shot", implementationType: "codex-cli-one-shot-ephemeral", longLivedSessionGate: { status: "blocked", pass: false, blockers: [{ code: "one_shot_runner_not_long_lived", sourceIssue: "pikasTech/HWLAB#317" }] } }; assert.equal(classifyCodexRunnerCapability(oneShot, { httpStatus: 200 }).capabilityPass, false); assert.equal(classifyCodeAgentChatReadiness(oneShot, { realDevLive: true, httpStatus: 200 }).devLiveReplyPass, true); }); test("Code Agent M3 DO write uses Skill CLI to call only HWLAB API /v1/m3/io", async () => { const calls = []; const payload = await handleCodeAgentChat( { conversationId: "cnv_m3_skill_write", traceId: "trc_m3_skill_write", message: "请通过 M3 DO1 写入 true,并回读 DI1" }, { now: () => "2026-05-23T00:05:00.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), 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 control"); }, m3IoSkillRequestJson: async (url, request) => { calls.push({ url, request }); const parsed = new URL(url); assert.equal(parsed.pathname, HWLAB_M3_IO_API_ROUTE); assert.equal(parsed.hostname, "hwlab-cloud-api.hwlab-dev.svc.cluster.local"); assert.equal(request.method, "POST"); assert.equal(request.body.action, "do.write"); assert.equal(request.body.resourceId, "res_boxsimu_1"); assert.equal(request.body.port, "DO1"); assert.equal(request.body.value, true); assert.equal(request.body.source, "hwlab-agent-runtime.m3-io"); return { ok: true, status: 200, body: { serviceId: "hwlab-cloud-api", contractVersion: "m3-io-control-v1", status: "succeeded", accepted: true, action: "do.write", traceId: "trc_m3_skill_write", operationId: "op_m3_do_write_skill", auditId: "aud_m3_do_write_skill_succeeded", evidenceId: "evd_m3_do_write_skill_succeeded", auditState: { status: "written_non_durable", durableStatus: { status: "degraded" } }, evidenceState: { status: "blocked", sourceKind: "BLOCKED", blocker: "runtime_durable_not_green", writeStatus: "written_non_durable" }, durableStatus: { status: "degraded", durable: false, blocker: "runtime_durable_not_green" }, result: { value: true, targetReadback: { status: "succeeded", value: true, resourceId: "res_boxsimu_2", port: "DI1" } }, controlPath: { status: "succeeded", cloudApi: true, gatewaySimu: true, boxSimu: true, patchPanel: true, frontendBypass: false } } }; } } ); validateCodeAgentChatSchema(payload); 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, "controlled-m3-io-skill-cli"); assert.equal(payload.toolCalls.length, 1); assert.equal(payload.toolCalls[0].name, "hwlab-agent-runtime.m3-io"); assert.equal(payload.toolCalls[0].status, "completed"); assert.equal(payload.toolCalls[0].route, HWLAB_M3_IO_API_ROUTE); assert.equal(payload.toolCalls[0].accepted, true); assert.equal(payload.toolCalls[0].operationId, "op_m3_do_write_skill"); assert.equal(payload.runnerTrace.route, HWLAB_M3_IO_API_ROUTE); assert.equal(payload.runnerTrace.accepted, true); assert.equal(payload.runner.writeCapable, true); assert.deepEqual(payload.runner.toolPolicy.allowed, [`POST ${HWLAB_M3_IO_API_ROUTE}`]); assert.equal(payload.runner.safety.directGatewayCallsAllowed, false); assert.equal(payload.runner.safety.directBoxSimuCallsAllowed, false); assert.equal(payload.runner.safety.directPatchPanelCallsAllowed, false); assert.equal(payload.providerTrace.fallbackUsed, false); assert.match(payload.reply.content, /Skill CLI -> HWLAB API \/v1\/m3\/io/u); assert.equal(calls.length, 1); assert.equal(calls[0].url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`); assert.equal(calls[0].url.includes("gateway"), false); assert.equal(calls[0].url.includes("box-simu"), false); assert.equal(calls[0].url.includes("patch-panel"), false); }); test("Code Agent M3 DI read returns structured blocker from HWLAB API without fallback", async () => { const calls = []; const payload = await handleCodeAgentChat( { conversationId: "cnv_m3_skill_read_blocked", traceId: "trc_m3_skill_read_blocked", message: "读取 M3 DI1 readback" }, { now: () => "2026-05-23T00:06:00.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), 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 read"); }, m3IoSkillRequestJson: async (url, request) => { calls.push({ url, request }); 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_read_blocked", operationId: "op_m3_di_read_blocked", auditId: "aud_m3_di_read_blocked_failed", evidenceId: "evd_m3_di_read_blocked_failed", blocker: { code: "m3_gateway_session_unavailable", message: "gateway unavailable", zh: "gateway 未注册/不可用" }, blockerClassification: { category: "gateway_session" }, auditState: { status: "written_non_durable" }, evidenceState: { status: "blocked", sourceKind: "BLOCKED", blocker: "runtime_durable_not_green", writeStatus: "written_non_durable" }, durableStatus: { status: "degraded", durable: false, blocker: "runtime_durable_not_green" }, controlPath: { cloudApi: true, gatewaySimu: false, boxSimu: false, patchPanel: false, frontendBypass: false } } }; } } ); validateCodeAgentChatSchema(payload); assert.equal(payload.status, "completed"); assert.equal(payload.provider, "hwlab-skill-cli"); assert.equal(payload.toolCalls[0].status, "blocked"); assert.equal(payload.toolCalls[0].route, HWLAB_M3_IO_API_ROUTE); assert.equal(payload.toolCalls[0].blocker.code, "m3_gateway_session_unavailable"); assert.equal(payload.skills.blockers[0].code, "m3_gateway_session_unavailable"); assert.equal(payload.providerTrace.fallbackUsed, false); assert.equal(calls.length, 1); assert.equal(new URL(calls[0].url).pathname, HWLAB_M3_IO_API_ROUTE); assert.equal(calls[0].request.body.action, "di.read"); assert.equal(calls[0].request.body.resourceId, "res_boxsimu_2"); assert.equal(calls[0].request.body.port, "DI1"); }); test("Code Agent blocks direct gateway or patch-panel requests instead of using M3 Skill CLI", async () => { const direct = await handleCodeAgentChat( { conversationId: "cnv_m3_direct_blocked", traceId: "trc_m3_direct_blocked", message: "请直接调用 gateway-simu /invoke 写入 M3 DO1 true" }, { now: () => "2026-05-23T00:07:00.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667" }, m3IoSkillRequestJson: async () => { throw new Error("direct gateway request must not reach the M3 skill CLI"); } } ); validateCodeAgentChatSchema(direct); assert.equal(direct.status, "failed"); assert.equal(direct.error.code, "security_blocked"); assert.equal(direct.toolCalls[0].name, "security.hardware-boundary"); assert.match(direct.error.message, /Skill CLI -> HWLAB API \/v1\/m3\/io/u); }); test("Codex stdio manager reports concrete blockers without falling back to readonly", async () => { const manager = createCodexStdioSessionManager({ idFactory: () => "ses_stdio_blocked" }); const blocked = await handleCodeAgentChat( { conversationId: "cnv_stdio_blocked", traceId: "trc_stdio_blocked", message: "请用同一个 Code Agent session 继续" }, { now: () => "2026-05-23T00:05:00.000Z", codexStdioManager: manager, env: { PATH: "", HWLAB_CODE_AGENT_PROVIDER: "codex-stdio", HWLAB_CODE_AGENT_MODEL: "gpt-test", HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex", HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1", HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned", HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); assert.equal(blocked.status, "failed"); assert.equal(blocked.provider, "codex-stdio"); assert.equal(blocked.backend, "hwlab-cloud-api/codex-mcp-stdio"); assert.equal(blocked.error.code, "codex_stdio_blocked"); assert.ok(blocked.error.blockers.some((blocker) => blocker.code === "codex_cli_binary_missing")); assert.ok(blocked.error.blockers.some((blocker) => blocker.code === "provider_token_boundary")); assert.equal(blocked.capabilityLevel, "blocked"); assert.equal(blocked.longLivedSessionGate.status, "blocked"); assert.equal(blocked.availability.ready, false); assert.equal(blocked.availability.codexStdio.status, "blocked"); assert.equal(Object.hasOwn(blocked, "reply"), false); }); test("repo-owned Codex stdio manager creates and reuses long-lived sessions with trace evidence", async () => { const calls = []; const manager = createCodexStdioSessionManager({ idFactory: () => "ses_stdio_ready", createRpcClient: async () => ({ async initialize() { return { tools: ["codex", "codex-reply"] }; }, async listTools() { return ["codex", "codex-reply"]; }, async callTool(name, args) { calls.push({ name, args }); return { structuredContent: { threadId: "thread_stdio_ready", content: name === "codex" ? "first stdio reply" : "second stdio reply" } }; }, close() {} }) }); const env = { PATH: process.env.PATH, OPENAI_API_KEY: "test-openai-key-material", HWLAB_CODE_AGENT_PROVIDER: "codex-stdio", HWLAB_CODE_AGENT_MODEL: "gpt-test", HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1", HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned", HWLAB_CODE_AGENT_WORKSPACE: process.cwd() }; const first = await handleCodeAgentChat( { conversationId: "cnv_stdio_ready", traceId: "trc_stdio_ready_1", message: "first" }, { now: () => "2026-05-23T00:06:00.000Z", codexStdioManager: manager, env } ); validateCodeAgentChatSchema(first); assert.equal(first.status, "completed"); assert.equal(first.provider, "codex-stdio"); assert.equal(first.runner.kind, "codex-mcp-stdio-runner"); assert.equal(first.sessionMode, "codex-mcp-stdio-long-lived"); assert.equal(first.implementationType, "repo-owned-codex-mcp-stdio-session"); assert.equal(first.session.longLivedSession, true); assert.equal(first.session.codexStdio, true); assert.equal(first.runner.writeCapable, true); assert.equal(first.runner.durableSession, true); assert.equal(first.capabilityLevel, "long-lived-codex-stdio-session"); assert.equal(first.longLivedSessionGate.status, "pass"); assert.equal(first.longLivedSessionGate.pass, true); assert.equal(classifyCodexRunnerCapability(first, { httpStatus: 200 }).capabilityPass, true); const second = await handleCodeAgentChat( { conversationId: "cnv_stdio_ready", traceId: "trc_stdio_ready_2", message: "second" }, { now: () => "2026-05-23T00:06:01.000Z", codexStdioManager: manager, env } ); assert.equal(second.status, "completed"); assert.equal(second.sessionId, first.sessionId); assert.equal(second.sessionReuse.reused, true); assert.equal(second.sessionReuse.turn, 2); assert.equal(second.toolCalls[0].name, "codex-reply"); assert.equal(second.reply.content, "second stdio reply"); assert.equal(calls[0].name, "codex"); assert.equal(calls[1].name, "codex-reply"); assert.equal(calls[1].args.threadId, "thread_stdio_ready"); });