import assert from "node:assert/strict"; import { existsSync } from "node:fs"; import { chmod, mkdir, mkdtemp, rm, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { test } from "bun:test"; import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.ts"; import { handleCodeAgentChat, validateCodeAgentChatSchema } from "./code-agent-chat.ts"; import { codexAppServerArgs, codexAppServerProviderBaseUrl, createCodexStdioSessionManager } from "./codex-stdio-session.ts"; import { childProcessEnv } from "./codex-stdio-session-helpers.ts"; import { classifyCodeAgentChatReadiness, classifyCodexRunnerCapability } from "../../scripts/src/code-agent-response-contract.mjs"; import { HWLAB_M3_IO_API_BASE_URL_ENV, HWLAB_M3_IO_CAPABILITY_LEVELS, HWLAB_M3_IO_API_ROUTE, HWLAB_M3_STATUS_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.lifecycleStatus, "busy"); assert.equal(first.session.lifecycle.busy, true); assert.equal(first.session.workspace, "/workspace/hwlab"); assert.equal(first.session.sandbox, "read-only"); assert.equal(first.session.runnerKind, "hwlab-readonly-runner"); assert.equal(first.session.sessionMode, "controlled-readonly-session-registry"); assert.equal(first.session.capabilityLevel, "read-only-session-tools"); assert.equal(first.session.longLivedSession, true); assert.equal(first.session.durableSession, false); assert.equal(first.session.codexStdio, false); assert.equal(first.session.writeCapable, false); assert.equal(first.session.readOnly, true); 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); const describedCreating = registry.describe(); assert.equal(describedCreating.sessionMode, "controlled-readonly-session-registry"); assert.equal(describedCreating.longLivedSession, true); assert.equal(describedCreating.longLivedCodexStdio, false); assert.equal(describedCreating.recentSessions[0].sessionId, "ses_test-registry"); assert.equal(describedCreating.recentSessions[0].sessionMode, "controlled-readonly-session-registry"); assert.equal(describedCreating.recentSessions[0].workspace, "/workspace/hwlab"); assert.equal(describedCreating.recentSessions[0].secretMaterialStored, false); assert.deepEqual(describedCreating.lifecycleStatuses, ["creating", "ready", "busy", "idle", "interrupted", "expired", "failed"]); assert.equal(describedCreating.statusAliases.canceled, "interrupted"); 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"); assert.equal(second.session.lifecycleStatus, "busy"); 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.session.lifecycleStatus, "expired"); assert.match(expired.session.lifecycle.userMessage, /会话已过期/u); assert.equal(expired.blocker.sourceIssue, "pikasTech/HWLAB#317"); }); test("code agent session registry stores bounded non-sensitive conversation facts", () => { const registry = createCodeAgentSessionRegistry({ maxConversationFacts: 2, maxFactSkills: 2, idFactory: () => "ses_fact_registry" }); const acquired = registry.acquire({ conversationId: "cnv_fact_registry", workspace: "/workspace/hwlab", traceId: "trc_fact_registry_1", now: "2026-05-23T00:00:00.000Z" }); registry.release(acquired.session.sessionId, { conversationId: "cnv_fact_registry", traceId: "trc_fact_registry_1", now: "2026-05-23T00:00:00.010Z" }); registry.recordFact("cnv_fact_registry", { traceId: "trc_fact_registry_1", provider: "codex-readonly-runner", backend: "hwlab-cloud-api/codex-readonly-runner", workspace: "/workspace/hwlab", sandbox: "read-only", session: acquired.session, sessionMode: "controlled-readonly-session-registry", capabilityLevel: "read-only-session-tools", runner: { kind: "hwlab-readonly-runner", sessionId: "ses_fact_registry" }, runnerTrace: { traceId: "trc_fact_registry_1", runnerKind: "hwlab-readonly-runner", sessionMode: "controlled-readonly-session-registry", events: ["tool:pwd:completed"], valuesPrinted: false }, toolCalls: [{ name: "pwd", status: "completed", stdout: "must-not-store-output sk-secret-value OPENAI_API_KEY=secret", traceId: "trc_fact_registry_1" }], skills: { status: "ready", items: [ { name: "alpha" }, { name: "beta" }, { name: "gamma" } ], count: 3, totalCount: 3 } }, { now: "2026-05-23T00:00:00.020Z" }); registry.recordFact("cnv_fact_registry", { traceId: "trc_fact_registry_2", workspace: "/workspace/hwlab", runner: { kind: "hwlab-readonly-runner" }, toolCalls: [{ name: "skills.discover", status: "completed", stdout: "must-not-store-output sk-secret-value OPENAI_API_KEY=secret" }], skills: { status: "ready", items: [ { name: "alpha" }, { name: "beta" }, { name: "gamma" } ], count: 3, totalCount: 3 } }, { now: "2026-05-23T00:00:00.030Z" }); registry.recordFact("cnv_fact_registry", { traceId: "trc_fact_registry_3", workspace: "/workspace/hwlab", runner: { kind: "hwlab-readonly-runner" }, toolCalls: [{ name: "session.context", status: "completed" }] }, { now: "2026-05-23T00:00:00.040Z" }); const facts = registry.getConversationFacts("cnv_fact_registry"); assert.equal(facts.conversationId, "cnv_fact_registry"); assert.equal(facts.sessionId, "ses_fact_registry"); assert.equal(facts.sessionLifecycleStatus, "idle"); assert.equal(facts.workspace, "/workspace/hwlab"); assert.equal(facts.turnCount, 2); assert.deepEqual(facts.traceIds, ["trc_fact_registry_2", "trc_fact_registry_3"]); assert.equal(facts.valuesRedacted, true); assert.equal(facts.secretMaterialStored, false); assert.deepEqual(facts.latestSkills.names, ["alpha", "beta"]); assert.equal(facts.latestSkills.truncated, true); assert.equal(JSON.stringify(facts).includes("must-not-store-output"), false); assert.equal(JSON.stringify(facts).includes("sk-secret-value"), false); assert.equal(JSON.stringify(facts).includes("OPENAI_API_KEY=secret"), false); assert.match(facts.summary, /workspace=\/workspace\/hwlab/u); }); test("Codex child env carries only device-pod auth needed by Code Agent tools", () => { const child = childProcessEnv({ PATH: "/usr/bin", CODEX_HOME: "/tmp/codex-home", OPENAI_API_KEY: "test-openai-key-material", HWLAB_DEVICE_POD_API_URL: "http://127.0.0.1:6667", HWLAB_CLOUD_API_URL: "http://127.0.0.1:6667", HWLAB_DEVICE_POD_SESSION_TOKEN: "device-pod-session-token", HWLAB_SESSION_COOKIE: "hwlab_session=browser-cookie", HWLAB_SESSION_TOKEN: "browser-session-token", HWLAB_BEARER_TOKEN: "browser-bearer-token" }); assert.equal(child.HWLAB_DEVICE_POD_API_URL, "http://127.0.0.1:6667"); assert.equal(child.HWLAB_CLOUD_API_URL, "http://127.0.0.1:6667"); assert.equal(child.HWLAB_DEVICE_POD_SESSION_TOKEN, "device-pod-session-token"); assert.equal(child.OPENAI_API_KEY, undefined); assert.equal(child.HWLAB_SESSION_COOKIE, undefined); assert.equal(child.HWLAB_SESSION_TOKEN, undefined); assert.equal(child.HWLAB_BEARER_TOKEN, undefined); }); test("code agent session registry inspect locates conversation, session, and trace summary", () => { const registry = createCodeAgentSessionRegistry({ idFactory: () => "ses_inspect_registry" }); const acquired = registry.acquire({ conversationId: "cnv_inspect_registry", workspace: "/workspace/hwlab", sandbox: "workspace-write", runnerKind: "codex-app-server-stdio-runner", capabilityLevel: "long-lived-codex-stdio-session", traceId: "trc_inspect_registry_1", now: "2026-05-23T00:00:00.000Z" }); registry.release(acquired.session.sessionId, { conversationId: "cnv_inspect_registry", traceId: "trc_inspect_registry_1", now: "2026-05-23T00:00:00.010Z" }); registry.recordFact("cnv_inspect_registry", { traceId: "trc_inspect_registry_1", provider: "codex-stdio", backend: "hwlab-cloud-api/codex-app-server-stdio", workspace: "/workspace/hwlab", sandbox: "workspace-write", session: acquired.session, runner: { kind: "codex-app-server-stdio-runner", sessionId: acquired.session.sessionId }, runnerTrace: { traceId: "trc_inspect_registry_1", runnerKind: "codex-app-server-stdio-runner", sessionMode: "codex-app-server-stdio-long-lived", valuesPrinted: false } }, { now: "2026-05-23T00:00:00.020Z" }); const byConversation = registry.inspect({ conversationId: "cnv_inspect_registry" }); assert.equal(byConversation.ok, true); assert.equal(byConversation.status, "found"); assert.equal(byConversation.session.sessionId, "ses_inspect_registry"); assert.equal(byConversation.conversationFacts.conversationId, "cnv_inspect_registry"); assert.deepEqual(byConversation.traceIds, ["trc_inspect_registry_1"]); assert.equal(byConversation.latestTraceId, "trc_inspect_registry_1"); assert.equal(byConversation.valuesRedacted, true); assert.equal(byConversation.secretMaterialStored, false); const byTrace = registry.inspect({ traceId: "trc_inspect_registry_1" }); assert.equal(byTrace.ok, true); assert.equal(byTrace.session.sessionId, "ses_inspect_registry"); const missingTrace = registry.inspect({ traceId: "trc_inspect_registry_missing" }); assert.equal(missingTrace.ok, false); assert.equal(missingTrace.status, "not_found"); assert.deepEqual(missingTrace.traceIds, []); const missing = registry.inspect({ conversationId: "cnv_inspect_registry_missing" }); assert.equal(missing.ok, false); assert.equal(missing.status, "not_found"); assert.equal(missing.conversationFacts.turnCount, 0); assert.deepEqual(missing.traceIds, []); }); 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.lifecycleStatus, "busy"); assert.match(busy.session.lifecycle.userMessage, /会话忙\/请等待/u); assert.equal(busy.session.currentTraceId, "trc_busy_1"); }); test("code agent session registry blocks failed and interrupted sessions without reusing them", () => { const failedRegistry = createCodeAgentSessionRegistry({ idFactory: () => "ses_failed" }); const failedSeed = failedRegistry.acquire({ conversationId: "cnv_failed", workspace: "/workspace/hwlab", traceId: "trc_failed_1", now: "2026-05-23T00:00:00.000Z" }); assert.equal(failedSeed.ok, true); failedRegistry.fail(failedSeed.session.sessionId, { conversationId: "cnv_failed", traceId: "trc_failed_1", now: "2026-05-23T00:00:00.010Z" }); const failed = failedRegistry.acquire({ conversationId: "cnv_failed", workspace: "/workspace/hwlab", traceId: "trc_failed_2", now: "2026-05-23T00:00:01.000Z" }); assert.equal(failed.ok, false); assert.equal(failed.code, "session_failed"); assert.equal(failed.session.status, "failed"); assert.equal(failed.session.lifecycleStatus, "failed"); assert.match(failed.session.lifecycle.userMessage, /需重新发送建立新会话/u); assert.equal(failed.blocker.sourceIssue, "pikasTech/HWLAB#317"); const interruptedRegistry = createCodeAgentSessionRegistry({ idFactory: () => "ses_interrupted" }); const interruptedSeed = interruptedRegistry.acquire({ conversationId: "cnv_interrupted", workspace: "/workspace/hwlab", traceId: "trc_interrupted_1", now: "2026-05-23T00:00:00.000Z" }); assert.equal(interruptedSeed.ok, true); interruptedRegistry.interrupt(interruptedSeed.session.sessionId, { conversationId: "cnv_interrupted", traceId: "trc_interrupted_1", now: "2026-05-23T00:00:00.010Z" }); const interrupted = interruptedRegistry.acquire({ conversationId: "cnv_interrupted", workspace: "/workspace/hwlab", traceId: "trc_interrupted_2", now: "2026-05-23T00:00:01.000Z" }); assert.equal(interrupted.ok, false); assert.equal(interrupted.code, "session_interrupted"); assert.equal(interrupted.session.status, "interrupted"); assert.equal(interrupted.session.lifecycleStatus, "interrupted"); }); test("Codex stdio readiness blocker does not use read-only session fallback", async () => { const registry = createCodeAgentSessionRegistry({ idFactory: () => "ses_chat_registry" }); const payload = await handleCodeAgentChat( { conversationId: "cnv_chat_registry", traceId: "trc_chat_registry_pwd", message: "pwd" }, { now: () => "2026-05-23T00:01:00.000Z", sessionRegistry: registry, env: { PATH: "", HWLAB_CODE_AGENT_PROVIDER: "codex-stdio", HWLAB_CODE_AGENT_CODEX_COMMAND: path.join(os.tmpdir(), "hwlab-missing-codex"), HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), HWLAB_CODE_AGENT_CODEX_WORKSPACE: process.cwd() } } ); 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.error.code, "codex_cli_binary_missing"); assert.equal(payload.runner.kind, "codex-app-server-stdio-runner"); assert.equal(payload.runner.kind === "hwlab-readonly-runner", false); assert.equal(payload.capabilityLevel, "blocked"); assert.equal(payload.skills.status, "not_requested"); assert.deepEqual(payload.toolCalls, []); assert.equal(payload.session, null); assert.equal(payload.sessionLifecycleStatus, "failed"); assert.equal(payload.sessionSummary.degraded, true); assert.equal(payload.longLivedSessionGate.status, "blocked"); assert.equal(payload.longLivedSessionGate.pass, false); assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "codex_cli_binary_missing")); assert.equal(payload.runnerLimitations.includes("no-controlled-readonly-fallback"), true); assert.equal(Object.hasOwn(payload, "reply"), false); assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false); }); test("stale read-only conversation facts do not mask Codex stdio blockers", async () => { const registry = createCodeAgentSessionRegistry({ idFactory: () => "ses_stale_readonly" }); const acquired = registry.acquire({ conversationId: "cnv_stale_readonly", workspace: process.cwd(), sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_stale_readonly_seed", now: "2026-05-23T00:02:00.000Z" }); registry.release(acquired.session.sessionId, { conversationId: "cnv_stale_readonly", traceId: "trc_stale_readonly_seed", now: "2026-05-23T00:02:00.001Z" }); registry.recordFact("cnv_stale_readonly", { traceId: "trc_stale_readonly_seed", workspace: process.cwd(), session: acquired.session, runner: { kind: "hwlab-readonly-runner" }, sessionMode: "controlled-readonly-session-registry", capabilityLevel: "read-only-session-tools", toolCalls: [{ name: "skills.discover", status: "completed" }], skills: { status: "ready", items: [{ name: "alpha-skill" }], count: 1, totalCount: 1 } }, { now: "2026-05-23T00:02:00.002Z" }); const payload = await handleCodeAgentChat( { conversationId: "cnv_stale_readonly", traceId: "trc_stale_readonly_request", message: "根据上一轮列出你能使用的所有skill" }, { now: () => "2026-05-23T00:02:01.000Z", sessionRegistry: registry, env: { PATH: "", HWLAB_CODE_AGENT_PROVIDER: "codex-stdio", HWLAB_CODE_AGENT_CODEX_COMMAND: path.join(os.tmpdir(), "hwlab-missing-codex"), HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), HWLAB_CODE_AGENT_CODEX_WORKSPACE: process.cwd() } } ); validateCodeAgentChatSchema(payload); assert.equal(payload.status, "failed"); assert.equal(payload.error.code, "codex_cli_binary_missing"); assert.equal(payload.provider, "codex-stdio"); assert.equal(payload.runner.kind, "codex-app-server-stdio-runner"); assert.equal(payload.capabilityLevel, "blocked"); assert.equal(payload.skills.status, "not_requested"); assert.equal(Object.hasOwn(payload, "reply"), false); assert.equal(JSON.stringify(payload).includes("alpha-skill"), true); assert.equal(payload.conversationFacts.latestSkills.names[0], "alpha-skill"); assert.equal(payload.error.blocker.conversationFacts.latestSkills.names[0], "alpha-skill"); assert.equal(payload.runner.kind === "hwlab-readonly-runner", false); assert.equal(payload.provider === "openai-responses", false); }); test("expired, busy, and failed 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" }); expiredRegistry.recordFact("cnv_expired_chat", { traceId: "trc_expired_seed", workspace: process.cwd(), session: first.session, runner: { kind: "hwlab-readonly-runner" }, sessionMode: "controlled-readonly-session-registry", capabilityLevel: "read-only-session-tools", toolCalls: [{ name: "pwd", status: "completed" }] }, { now: "2026-05-23T00:02:00.002Z" }); const expiredAcquire = expiredRegistry.acquire({ conversationId: "cnv_expired_chat", workspace: process.cwd(), sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_expired_request", now: "2026-05-23T00:02:01.000Z" }); const expired = codeAgentBlockedPayloadFromAcquire(expiredAcquire, { conversationId: "cnv_expired_chat", traceId: "trc_expired_request", conversationFacts: expiredRegistry.getConversationFacts("cnv_expired_chat") }); assert.equal(expired.status, "failed"); assert.equal(expired.error.code, "session_expired"); assert.equal(expired.error.layer, "session"); assert.equal(expired.error.retryable, true); assert.match(expired.error.userMessage, /session 已过期/u); assert.equal(expired.error.blocker.code, "session_expired"); assert.equal(expired.error.blocker.traceId, "trc_expired_request"); assert.equal(expired.session.status, "expired"); assert.equal(expired.sessionLifecycleStatus, "expired"); assert.equal(expired.sessionSummary.requiresNewSession, true); assert.match(expired.sessionSummary.userMessage, /会话已过期/u); assert.equal(expired.capabilityLevel, "blocked"); assert.equal(expired.longLivedSessionGate.status, "blocked"); assert.equal(expired.conversationFacts.conversationId, "cnv_expired_chat"); assert.equal(expired.conversationFacts.sessionId, "ses_expired_chat"); assert.equal(expired.conversationFacts.turnCount, 1); assert.equal(expired.conversationFacts.recentToolCalls[0].name, "pwd"); assert.equal(expired.error.blocker.conversationFacts.sessionId, "ses_expired_chat"); 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" }); busyRegistry.recordFact("cnv_busy_chat", { traceId: "trc_busy_seed", workspace: process.cwd(), sessionId: "ses_busy_chat", runner: { kind: "hwlab-readonly-runner" }, sessionMode: "controlled-readonly-session-registry", capabilityLevel: "read-only-session-tools", toolCalls: [{ name: "skills.discover", status: "completed" }], skills: { status: "ready", items: [{ name: "alpha-skill" }], count: 1, totalCount: 1 } }, { now: "2026-05-23T00:03:00.001Z" }); const busyAcquire = busyRegistry.acquire({ conversationId: "cnv_busy_chat", workspace: process.cwd(), sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_busy_request", now: "2026-05-23T00:03:01.000Z" }); const busy = codeAgentBlockedPayloadFromAcquire(busyAcquire, { conversationId: "cnv_busy_chat", traceId: "trc_busy_request", conversationFacts: busyRegistry.getConversationFacts("cnv_busy_chat") }); assert.equal(busy.status, "failed"); assert.equal(busy.error.code, "session_busy"); assert.equal(busy.error.layer, "session"); assert.equal(busy.error.retryable, true); assert.match(busy.error.userMessage, /正在处理上一轮/u); assert.equal(busy.error.blocker.code, "session_busy"); assert.equal(busy.session.status, "busy"); assert.equal(busy.sessionLifecycleStatus, "busy"); assert.equal(busy.sessionSummary.busy, true); assert.match(busy.sessionSummary.userMessage, /会话忙\/请等待/u); assert.equal(busy.capabilityLevel, "blocked"); assert.equal(busy.error.blockers[0].sourceIssue, "pikasTech/HWLAB#317"); assert.equal(busy.conversationFacts.conversationId, "cnv_busy_chat"); assert.equal(busy.conversationFacts.sessionId, "ses_busy_chat"); assert.equal(busy.conversationFacts.latestSkills.names[0], "alpha-skill"); const failedRegistry = createCodeAgentSessionRegistry({ idFactory: () => "ses_failed_chat" }); const failedSeed = failedRegistry.acquire({ conversationId: "cnv_failed_chat", workspace: process.cwd(), sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_failed_seed", now: "2026-05-23T00:04:00.000Z" }); failedRegistry.fail(failedSeed.session.sessionId, { conversationId: "cnv_failed_chat", traceId: "trc_failed_seed", now: "2026-05-23T00:04:00.001Z" }); failedRegistry.recordFact("cnv_failed_chat", { traceId: "trc_failed_seed", workspace: process.cwd(), session: failedSeed.session, runner: { kind: "hwlab-readonly-runner" }, sessionMode: "controlled-readonly-session-registry", capabilityLevel: "read-only-session-tools", toolCalls: [{ name: "pwd", status: "completed" }] }, { now: "2026-05-23T00:04:00.002Z" }); const failedAcquire = failedRegistry.acquire({ conversationId: "cnv_failed_chat", workspace: process.cwd(), sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_failed_request", now: "2026-05-23T00:04:01.000Z" }); const failed = codeAgentBlockedPayloadFromAcquire(failedAcquire, { conversationId: "cnv_failed_chat", traceId: "trc_failed_request", conversationFacts: failedRegistry.getConversationFacts("cnv_failed_chat") }); assert.equal(failed.status, "failed"); assert.equal(failed.error.code, "session_failed"); assert.equal(failed.error.layer, "session"); assert.equal(failed.error.retryable, true); assert.match(failed.error.userMessage, /session 已失败/u); assert.equal(failed.error.blocker.code, "session_failed"); assert.equal(failed.session.status, "failed"); assert.equal(failed.sessionLifecycleStatus, "failed"); assert.equal(failed.sessionSummary.requiresNewSession, true); assert.match(failed.sessionSummary.userMessage, /需重新发送建立新会话/u); assert.equal(failed.capabilityLevel, "blocked"); assert.equal(failed.conversationFacts.conversationId, "cnv_failed_chat"); assert.equal(failed.conversationFacts.sessionId, "ses_failed_chat"); assert.equal(failed.conversationFacts.recentToolCalls[0].name, "pwd"); }); test("OpenAI fallback hook is not used when Codex stdio is unavailable", async () => { let providerCalled = false; const payload = await handleCodeAgentChat( { conversationId: "cnv_fallback_gate", traceId: "trc_fallback_gate", message: "你好" }, { now: () => "2026-05-23T00:04:00.000Z", env: { PATH: "", HWLAB_CODE_AGENT_PROVIDER: "openai", HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "1", HWLAB_CODE_AGENT_CODEX_COMMAND: path.join(os.tmpdir(), "hwlab-missing-codex"), HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), HWLAB_CODE_AGENT_CODEX_WORKSPACE: process.cwd() }, callProvider: async () => { providerCalled = true; return { provider: "openai-responses", model: "gpt-test", backend: "hwlab-cloud-api/openai-responses", content: "普通文本回复。", usage: null, providerTrace: { source: "test-provider" } }; } } ); validateCodeAgentChatSchema(payload); assert.equal(payload.status, "failed"); assert.equal(payload.error.code, "codex_cli_binary_missing"); assert.equal(payload.provider, "codex-stdio"); assert.equal(payload.runner.kind, "codex-app-server-stdio-runner"); assert.equal(payload.capabilityLevel, "blocked"); assert.equal(payload.runner.kind === "openai-responses-fallback", false); assert.equal(payload.provider === "openai-responses", false); assert.equal(Object.hasOwn(payload, "reply"), false); assert.equal(providerCalled, false); assert.equal(payload.sessionLifecycleStatus, "failed"); assert.equal(payload.sessionSummary.degraded, true); assert.equal(payload.runnerLimitations.includes("no-text-fallback"), true); assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false); const oneShot = { ...payload, provider: "codex-cli", backend: "hwlab-cloud-api/codex-cli", runner: { kind: "codex-cli-one-shot-ephemeral", codexStdio: false, writeCapable: false, durableSession: false, longLivedSession: 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, false); }); test("OpenAI provider mode still preserves facts but does not fallback without Codex stdio", async () => { const registry = createCodeAgentSessionRegistry({ idFactory: () => "ses_provider_context" }); let providerCalled = false; const env = { PATH: "", HWLAB_CODE_AGENT_PROVIDER: "openai", HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "1", HWLAB_CODE_AGENT_CODEX_COMMAND: path.join(os.tmpdir(), "hwlab-missing-codex"), HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), HWLAB_CODE_AGENT_CODEX_WORKSPACE: process.cwd() }; const seed = registry.acquire({ conversationId: "cnv_provider_context", workspace: process.cwd(), sandbox: "read-only", runnerKind: "hwlab-readonly-runner", capabilityLevel: "read-only-session-tools", traceId: "trc_provider_context_pwd", now: "2026-05-23T00:04:09.000Z" }); const released = registry.release(seed.session.sessionId, { conversationId: "cnv_provider_context", traceId: "trc_provider_context_pwd", now: "2026-05-23T00:04:09.010Z" }); registry.recordFact("cnv_provider_context", { traceId: "trc_provider_context_pwd", workspace: process.cwd(), session: released, runner: { kind: "hwlab-readonly-runner" }, sessionMode: "controlled-readonly-session-registry", capabilityLevel: "read-only-session-tools", toolCalls: [{ name: "pwd", status: "completed" }] }, { now: "2026-05-23T00:04:09.020Z" }); const payload = await handleCodeAgentChat( { conversationId: "cnv_provider_context", traceId: "trc_provider_context_openai", message: "请根据刚才的上下文简要说明" }, { now: () => "2026-05-23T00:04:11.000Z", sessionRegistry: registry, env, callProvider: async () => { providerCalled = true; return { provider: "openai-responses", model: "gpt-test", backend: "hwlab-cloud-api/openai-responses", content: "普通 fallback 回复不应被调用。", usage: null, providerTrace: { source: "test-provider" } }; } } ); assert.equal(payload.status, "failed"); assert.equal(payload.error.code, "codex_cli_binary_missing"); assert.equal(payload.provider, "codex-stdio"); assert.equal(payload.runner.kind, "codex-app-server-stdio-runner"); assert.equal(payload.conversationFacts.turnCount, 1); assert.equal(payload.conversationFacts.sessionId, released.sessionId); assert.equal(payload.conversationFacts.recentToolCalls[0].name, "pwd"); assert.equal(payload.conversationFacts.latestTraceId, "trc_provider_context_pwd"); assert.equal(payload.error.blocker.conversationFacts.sessionId, released.sessionId); assert.equal(Object.hasOwn(payload, "reply"), false); assert.equal(providerCalled, false); assert.equal(JSON.stringify(payload.conversationFacts).includes("sk-"), false); }); test("Code Agent PC gateway prompt reaches Codex stdio instead of internal hardware shortcut", async () => { const calls = []; let providerCalled = false; const fakeCodex = await createFakeCodexCommand(); const codexHome = await prepareFakeCodexHome(); const registry = createCodeAgentSessionRegistry(); const manager = createCodexStdioSessionManager({ idFactory: () => "ses_pc_gateway_stdio", createRpcClient: async () => createFakeAppServerClient({ calls, responses: ["已通过受控 wrapper 执行 PC gateway 命令。"] }) }); try { const payload = await handleCodeAgentChat( { conversationId: "cnv_pc_gateway_stdio", traceId: "trc_pc_gateway_stdio", projectId: "prj_mvp_topology", gatewaySessionId: "gws_DESKTOP-1MHOD9I", resourceId: "res_windows_host", capabilityId: "cap_windows_cmd_exec", message: "请通过 PC gateway shell.exec 执行 Windows cmd:cmd /c echo HWLAB-PC-GW && hostname" }, { now: () => "2026-05-23T00:08:00.000Z", 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_SANDBOX: "danger-full-access", HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses" }, sessionRegistry: registry, codexStdioManager: manager, callProvider: async () => { providerCalled = true; throw new Error("text fallback must not be used"); } } ); 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.sandbox, "danger-full-access"); assert.equal(payload.runner.kind, "codex-app-server-stdio-runner"); assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session"); assert.equal(payload.toolCalls.some((toolCall) => toolCall.name === "hardware.invoke.shell"), false); assert.equal(providerCalled, false); const turn = calls.find((call) => call.method === "turn/start"); assert.ok(turn, "Codex stdio turn/start should be called"); assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-tran\.mjs/u); assert.match(turn.args.prompt, /canonical skill location is \/app\/skills\/device-pod-cli\/SKILL\.md/u); assert.match(turn.args.prompt, /node \/app\/skills\/device-pod-cli\/scripts\/device-pod-cli\.mjs/u); assert.match(turn.args.prompt, /Do not pass --api-base-url, --api-url, --cloud-api-url, --base-url, or session tokens to hwpod/u); assert.match(turn.args.prompt, /gws_DESKTOP-1MHOD9I:\/f\/work/u); assert.match(turn.args.prompt, /gws_DESKTOP-1MHOD9I:f:\/work\/hwlab\/\.tmp/u); assert.match(turn.args.prompt, /improve \/app\/tools\/hwlab-gateway-tran\.mjs first/u); assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills/u); assert.match(turn.args.prompt, /cmd \/c echo HWLAB-PC-GW && hostname/u); assert.match(turn.args.prompt, /cmd -- /u); assert.match(turn.args.prompt, /ps --