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", "thread-resume-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 HWPOD runtime API key needed by Code Agent tools", () => {
  const child = childProcessEnv({
    PATH: "/usr/bin",
    CODEX_HOME: "/tmp/codex-home",
    OPENAI_API_KEY: "test-openai-key-material",
    HWLAB_CLOUD_API_URL: "http://127.0.0.1:6667",
    HWLAB_RUNTIME_API_URL: "http://127.0.0.1:6667",
    HWLAB_API_KEY: "hwl_live_user-default-key",
    HWLAB_SESSION_COOKIE: "hwlab_session=browser-cookie",
    HWLAB_SESSION_TOKEN: "browser-session-token"
  });

  assert.equal(child.HWLAB_CLOUD_API_URL, "http://127.0.0.1:6667");
  assert.equal(child.HWLAB_RUNTIME_API_URL, "http://127.0.0.1:6667");
  assert.equal(child.HWLAB_API_KEY, "hwl_live_user-default-key");
  assert.equal(child.OPENAI_API_KEY, undefined);
  assert.equal(child.HWLAB_SESSION_COOKIE, undefined);
  assert.equal(child.HWLAB_SESSION_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 reuses failed and interrupted sessions", () => {
  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, true);
  assert.equal(failed.reused, true);
  assert.equal(failed.session.sessionId, "ses_failed");
  assert.equal(failed.session.status, "busy");
  assert.equal(failed.session.lifecycleStatus, "busy");
  assert.equal(failed.session.lastTraceId, "trc_failed_2");
  assert.equal(failed.session.currentTraceId, "trc_failed_2");
  assert.equal(failed.session.lifecycle.requiresNewSession, false);

  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, true);
  assert.equal(interrupted.reused, true);
  assert.equal(interrupted.session.status, "busy");
  assert.equal(interrupted.session.lifecycleStatus, "busy");
  assert.equal(interrupted.session.currentTraceId, "trc_interrupted_2");
});

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 and busy sessions are blockers while failed sessions are reusable", 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"
  });
  assert.equal(failedAcquire.ok, true);
  assert.equal(failedAcquire.reused, true);
  assert.equal(failedAcquire.session.sessionId, "ses_failed_chat");
  assert.equal(failedAcquire.session.status, "busy");
  assert.equal(failedAcquire.session.lifecycleStatus, "busy");
  assert.equal(failedAcquire.session.lifecycle.requiresNewSession, false);
  const failedFacts = failedRegistry.getConversationFacts("cnv_failed_chat");
  assert.equal(failedFacts.conversationId, "cnv_failed_chat");
  assert.equal(failedFacts.sessionId, "ses_failed_chat");
  assert.equal(failedFacts.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.ok(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_openai");
  assert.equal(payload.error.blocker.conversationFacts.latestTraceId, "trc_provider_context_openai");
  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, /\/app\/skills\/hwpod-cli\/SKILL\.md/u);
    assert.match(turn.args.prompt, /\/app\/skills\/hwpod-ctl\/SKILL\.md/u);
    assert.match(turn.args.prompt, /hwpod is the standard HWPOD task runner entry/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 -- <command>/u);
    assert.match(turn.args.prompt, /ps -- <script>/u);
    assert.match(turn.args.prompt, /tran upload\/download/u);
    assert.doesNotMatch(turn.args.prompt, /hwlab-gateway-shell\.mjs|--powershell-stdin|ConvertTo-HwlabJson/u);
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex stdio manager uses a per-turn app-server client and closes each client", async () => {
  const calls = [];
  const createdModels = [];
  const closedModels = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const baseEnv = {
    PATH: process.env.PATH,
    OPENAI_API_KEY: "test-openai-key-material",
    CODEX_HOME: codexHome,
    HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
    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()
  };
  const manager = createCodexStdioSessionManager({
    idFactory: () => `ses_profile_${createdModels.length + 1}`,
    createRpcClient: async ({ env }) => {
      const model = env.HWLAB_CODE_AGENT_MODEL;
      createdModels.push(model);
      const client = createFakeAppServerClient({ calls, responses: [`reply from ${model}`] });
      client.close = () => closedModels.push(model);
      return client;
    }
  });

  try {
    await manager.chat({
      conversationId: "cnv_profile_deepseek",
      traceId: "trc_profile_deepseek",
      message: "first",
      env: {
        ...baseEnv,
        HWLAB_CODE_AGENT_MODEL: "deepseek-chat",
        HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://hwlab-deepseek-proxy.hwlab-dev.svc.cluster.local:4000/v1/responses"
      }
    });
    await manager.chat({
      conversationId: "cnv_profile_codex",
      traceId: "trc_profile_codex",
      message: "second",
      env: {
        ...baseEnv,
        HWLAB_CODE_AGENT_MODEL: "gpt-5.5",
        HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
      }
    });

    assert.deepEqual(createdModels, ["deepseek-chat", "gpt-5.5"]);
    assert.deepEqual(closedModels, ["deepseek-chat", "gpt-5.5"]);
    assert.equal(calls.filter((call) => call.method === "initialize").length, 2);
    assert.deepEqual(calls.filter((call) => call.method === "turn/start").map((call) => call.args.model), ["deepseek-chat", "gpt-5.5"]);
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex stdio concurrent turns do not share notification handlers", async () => {
  const calls = [];
  const createdClients = [];
  const closedClients = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => `ses_concurrent_${createdClients.length + 1}`,
    createRpcClient: async () => {
      const clientIndex = createdClients.length + 1;
      createdClients.push(clientIndex);
      const client = createTimedFakeAppServerClient({
        calls,
        text: `reply-from-client-${clientIndex}`,
        eventEveryMs: 10,
        eventCount: 1,
        completeAfterMs: clientIndex === 1 ? 120 : 30
      });
      const originalClose = client.close;
      client.close = () => {
        closedClients.push(clientIndex);
        originalClose();
      };
      return client;
    }
  });
  const 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"
  };

  try {
    const first = manager.chat({
      conversationId: "cnv_concurrent_1",
      traceId: "trc_concurrent_1",
      message: "first concurrent turn",
      env
    });
    await new Promise((resolve) => setTimeout(resolve, 5));
    const second = manager.chat({
      conversationId: "cnv_concurrent_2",
      traceId: "trc_concurrent_2",
      message: "second concurrent turn",
      env
    });

    const [firstPayload, secondPayload] = await Promise.all([first, second]);
    assert.equal(firstPayload.content, "reply-from-client-1");
    assert.equal(secondPayload.content, "reply-from-client-2");
    assert.deepEqual(createdClients, [1, 2]);
    assert.deepEqual(closedClients.sort(), [1, 2]);
    assert.equal(firstPayload.runnerTrace.traceId, "trc_concurrent_1");
    assert.equal(secondPayload.runnerTrace.traceId, "trc_concurrent_2");
    assert.ok(firstPayload.runnerTrace.assistantStreams?.[0]?.text.includes("reply-from-client-1"));
    assert.ok(secondPayload.runnerTrace.assistantStreams?.[0]?.text.includes("reply-from-client-2"));
    assert.equal(firstPayload.runnerTrace.assistantStreams?.[0]?.text.includes("reply-from-client-2"), false);
    assert.equal(secondPayload.runnerTrace.assistantStreams?.[0]?.text.includes("reply-from-client-1"), false);
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", async () => {
  const calls = [];
  let providerCalled = false;
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const registry = createCodeAgentSessionRegistry();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_keil_gateway_stdio",
    createRpcClient: async () => createFakeAppServerClient({
      calls,
      responses: ["已通过 PC gateway 调用 Windows Keil skill，success=true，return_code=0，生成 FREQ_Controller_FW.hex。"]
    })
  });

  try {
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_keil_gateway_stdio",
        traceId: "trc_keil_gateway_stdio",
        projectId: "prj_mvp_topology",
        message: String.raw`请通过 PC gateway 调用 Windows cmd，进入 C:\Users\liang\.agents\skills\keil，然后运行 py -3 keil-cli.py build -p "F:\Work\constart\projects\71-00075-11\FirmWare\MDK-ARM\FREQ_Controller_FW.uvprojx" -t FREQ_Controller_FW；如果返回 job id，请用 py -3 keil-cli.py job-status <job_id> 轮询到终态。`
      },
      {
        now: () => "2026-05-24T14:35: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");
        },
        m3IoSkillRequestJson: async () => {
          throw new Error("Keil gateway prompt must not be routed to M3 IO");
        }
      }
    );

    validateCodeAgentChatSchema(payload);
    assert.equal(payload.status, "completed");
    assert.equal(payload.provider, "codex-stdio");
    assert.equal(payload.responseType, undefined);
    assert.notEqual(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
    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, /\/app\/skills\/hwpod-cli\/SKILL\.md/u);
    assert.match(turn.args.prompt, /\/app\/skills\/hwpod-ctl\/SKILL\.md/u);
    assert.match(turn.args.prompt, /hwpod must auto-locate from that assembled environment/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.doesNotMatch(turn.args.prompt, /hwlab-gateway-shell\.mjs|--powershell-stdin/u);
    assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills\\keil/u);
    assert.match(turn.args.prompt, /keil-cli\.py build/u);
    assert.match(turn.args.prompt, /job-status/u);
    assert.match(turn.args.prompt, /FREQ_Controller_FW\.uvprojx/u);
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

function delay(ms) {
  return new Promise((resolve) => setTimeout(resolve, ms));
}

function escapeRegExp(value) {
  return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
}

async function createFakeCodexCommand() {
  const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-"));
  const packageRoot = path.join(root, "node_modules", "@openai", "codex");
  const command = path.join(packageRoot, "bin", "codex.js");
  const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex");
  await mkdir(path.dirname(command), { recursive: true });
  await mkdir(path.dirname(native), { recursive: true });
  await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8");
  await writeFile(command, "#!/usr/bin/env node\nconsole.log('codex 0.128.0');\n", "utf8");
  await writeFile(native, "#!/bin/sh\nexit 0\n", "utf8");
  await chmod(command, 0o755);
  await chmod(native, 0o755);
  return { root, command };
}

async function prepareFakeCodexHome(root = null) {
  const codexHome = root ?? await mkdtemp(path.join(os.tmpdir(), "hwlab-codex-home-"));
  await mkdir(codexHome, { recursive: true });
  await writeFile(path.join(codexHome, "config.toml"), "model = \"gpt-test\"\n", "utf8");
  await writeFile(path.join(codexHome, "auth.json"), "{\"auth\":\"redacted-test-placeholder\"}\n", "utf8");
  return codexHome;
}

function createFakeAppServerClient({ calls, responses = ["first stdio reply", "second stdio reply"], failedTurn = null, retryingTurn = null, staleResumeThreads = [] } = {}) {
  let notificationHandler = null;
  let turn = 0;
  const staleThreads = new Set(staleResumeThreads);
  return {
    async initialize() {
      calls?.push({ method: "initialize" });
      return { initialized: true };
    },
    setNotificationHandler(handler) {
      notificationHandler = typeof handler === "function" ? handler : null;
    },
    async startThread(args) {
      calls?.push({ method: "thread/start", args });
      notificationHandler?.({ method: "thread/started", params: { thread: { id: "thread_stdio_ready" } } });
      return { threadId: "thread_stdio_ready" };
    },
    async resumeThread(args) {
      calls?.push({ method: "thread/resume", args });
      if (staleThreads.has(args.threadId)) {
        staleThreads.delete(args.threadId);
        throw new Error(`no rollout found for thread id ${args.threadId}`);
      }
      notificationHandler?.({ method: "thread/started", params: { thread: { id: args.threadId } } });
      return { threadId: args.threadId };
    },
    async startTurn(args) {
      calls?.push({ method: "turn/start", args });
      turn += 1;
      const turnId = `turn_stdio_${turn}`;
      notificationHandler?.({ method: "turn/started", params: { turn: { id: turnId } } });
      if (retryingTurn && turn === retryingTurn.turn) {
        notificationHandler?.({
          method: "error",
          params: {
            threadId: args.threadId,
            turnId,
            willRetry: true,
            error: { message: retryingTurn.message }
          }
        });
      }
      if (failedTurn && turn === failedTurn.turn) {
        notificationHandler?.({
          method: "turn/completed",
          params: { turn: { id: turnId, status: "failed", error: { message: failedTurn.message } } }
        });
        return { turnId };
      }
      const text = responses[turn - 1] ?? responses.at(-1) ?? "stdio reply";
      notificationHandler?.({ method: "item/agentMessage/delta", params: { itemId: `item_${turn}`, delta: text } });
      notificationHandler?.({ method: "item/completed", params: { item: { id: `item_${turn}`, type: "agentMessage", text } } });
      notificationHandler?.({ method: "turn/completed", params: { turn: { id: turnId, status: "completed" } } });
      return { turnId };
    },
    close() {}
  };
}

function codeAgentBlockedPayloadFromAcquire(acquireResult, { conversationId, traceId, conversationFacts }) {
  const code = acquireResult.code;
  const userMessage = code === "session_busy"
    ? "Code Agent session 正在处理上一轮请求，请稍后重试。"
    : code === "session_expired"
      ? "Code Agent session 已过期，可重新发送建立新的 session。"
      : "Code Agent 当前 turn 已失败，session/thread 已保留，可继续发送下一条消息。";
  const sessionSummary = acquireResult.session.lifecycle;
  const blocker = {
    code,
    sourceIssue: acquireResult.blocker.sourceIssue,
    summary: acquireResult.blocker.summary,
    traceId,
    conversationFacts
  };
  return {
    status: "failed",
    conversationId,
    traceId,
    error: {
      code,
      layer: "session",
      retryable: true,
      userMessage,
      blocker,
      blockers: [acquireResult.blocker]
    },
    blocker,
    session: acquireResult.session,
    sessionLifecycleStatus: sessionSummary.status,
    sessionSummary,
    capabilityLevel: "blocked",
    longLivedSessionGate: {
      status: "blocked"
    },
    conversationFacts
  };
}

function createTimedFakeAppServerClient({ calls, text = "timed stdio reply", eventEveryMs = 20, eventCount = 0, completeAfterMs = null, commandExecution = null } = {}) {
  let notificationHandler = null;
  let turn = 0;
  const timers = new Set();
  const later = (ms, callback) => {
    const timer = setTimeout(() => {
      timers.delete(timer);
      callback();
    }, ms);
    timers.add(timer);
  };
  return {
    async initialize() {
      calls?.push({ method: "initialize" });
      return { initialized: true };
    },
    setNotificationHandler(handler) {
      notificationHandler = typeof handler === "function" ? handler : null;
    },
    async startThread(args) {
      calls?.push({ method: "thread/start", args });
      notificationHandler?.({ method: "thread/started", params: { thread: { id: "thread_stdio_timed" } } });
      return { threadId: "thread_stdio_timed" };
    },
    async resumeThread(args) {
      calls?.push({ method: "thread/resume", args });
      notificationHandler?.({ method: "thread/started", params: { thread: { id: args.threadId } } });
      return { threadId: args.threadId };
    },
    async startTurn(args) {
      calls?.push({ method: "turn/start", args });
      turn += 1;
      const turnId = `turn_stdio_timed_${turn}`;
      notificationHandler?.({ method: "turn/started", params: { turn: { id: turnId } } });
      for (let index = 1; index <= eventCount; index += 1) {
        later(eventEveryMs * index, () => {
          notificationHandler?.({
            method: "item/commandExecution/outputDelta",
            params: { delta: `progress-${index}` }
          });
        });
      }
      if (commandExecution) {
        const commandItem = {
          id: commandExecution.id ?? "cmd_stdio_timed_1",
          type: "commandExecution",
          ...commandExecution
        };
        later(Math.max(1, Math.floor(eventEveryMs / 2)), () => {
          notificationHandler?.({ method: "item/started", params: { item: { ...commandItem, status: "running" } } });
        });
        later(eventEveryMs, () => {
          notificationHandler?.({ method: "item/completed", params: { item: { ...commandItem, status: commandExecution.status ?? "completed" } } });
        });
      }
      if (Number.isInteger(completeAfterMs)) {
        later(completeAfterMs, () => {
          notificationHandler?.({ method: "item/agentMessage/delta", params: { itemId: `item_${turn}`, delta: text } });
          notificationHandler?.({ method: "item/completed", params: { item: { id: `item_${turn}`, type: "agentMessage", text } } });
          notificationHandler?.({ method: "turn/completed", params: { turn: { id: turnId, status: "completed" } } });
        });
      }
      return { turnId };
    },
    close() {
      for (const timer of timers) clearTimeout(timer);
      timers.clear();
    }
  };
}

function createPartialAssistantNoTurnCompletedClient({ calls, text = "partial assistant output" } = {}) {
  let notificationHandler = null;
  let turn = 0;
  return {
    async initialize() {
      calls?.push({ method: "initialize" });
      return { initialized: true };
    },
    setNotificationHandler(handler) {
      notificationHandler = typeof handler === "function" ? handler : null;
    },
    async startThread(args) {
      calls?.push({ method: "thread/start", args });
      notificationHandler?.({ method: "thread/started", params: { thread: { id: "thread_stdio_partial" } } });
      return { threadId: "thread_stdio_partial" };
    },
    async resumeThread(args) {
      calls?.push({ method: "thread/resume", args });
      notificationHandler?.({ method: "thread/started", params: { thread: { id: args.threadId } } });
      return { threadId: args.threadId };
    },
    async startTurn(args) {
      calls?.push({ method: "turn/start", args });
      turn += 1;
      const turnId = `turn_stdio_partial_${turn}`;
      notificationHandler?.({ method: "turn/started", params: { turn: { id: turnId } } });
      notificationHandler?.({ method: "item/agentMessage/delta", params: { itemId: `item_${turn}`, delta: text } });
      notificationHandler?.({ method: "item/completed", params: { item: { id: `item_${turn}`, type: "agentMessage", text } } });
      return { turnId };
    },
    close() {}
  };
}

test("Codex stdio resumes an idle-expired thread instead of returning no-event timeout", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idleTimeoutMs: 10,
    idFactory: () => "ses_stdio_idle_resume",
    createRpcClient: async () => createFakeAppServerClient({
      calls,
      responses: ["first response", "resumed response"]
    })
  });
  const 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: process.cwd(),
    HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  try {
    const first = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_idle_resume",
        traceId: "trc_stdio_idle_resume_first",
        message: "第一轮"
      },
      {
        now: () => "2026-05-31T00:00:00.000Z",
        env,
        codexStdioManager: manager
      }
    );
    validateCodeAgentChatSchema(first);
    assert.equal(first.status, "completed");
    assert.equal(first.session.threadId, "thread_stdio_ready");

    const second = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_idle_resume",
        sessionId: first.sessionId,
        threadId: first.session.threadId,
        traceId: "trc_stdio_idle_resume_second",
        message: "一小时后继续"
      },
      {
        now: () => "2026-05-31T01:00:00.000Z",
        env,
        codexStdioManager: manager
      }
    );

    validateCodeAgentChatSchema(second);
    assert.equal(second.status, "completed");
    assert.equal(second.error, undefined);
    assert.equal(second.session.sessionId, "ses_stdio_idle_resume");
    assert.equal(second.session.threadId, "thread_stdio_ready");
    assert.equal(second.session.turn, 2);
    assert.equal(second.sessionReuse.reused, true);
    assert.deepEqual(calls.filter((call) => call.method === "thread/start").map((call) => call.args.serviceName), ["hwlab-cloud-api"]);
    assert.deepEqual(calls.filter((call) => call.method === "thread/resume").map((call) => call.args.threadId), ["thread_stdio_ready"]);
    assert.equal(calls.filter((call) => call.method === "turn/start").length, 2);
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex stdio uses a persisted thread id after manager restart", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_should_not_replace_persisted",
    createRpcClient: async () => createFakeAppServerClient({
      calls,
      responses: ["resumed after restart"]
    })
  });
  const 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: process.cwd(),
    HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  try {
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_restart_resume",
        sessionId: "ses_stdio_restart_resume",
        threadId: "thread_persisted_account",
        traceId: "trc_stdio_restart_resume",
        message: "恢复上次账号会话"
      },
      {
        now: () => "2026-05-31T01:05:00.000Z",
        env,
        codexStdioManager: manager
      }
    );

    validateCodeAgentChatSchema(payload);
    assert.equal(payload.status, "completed");
    assert.equal(payload.session.sessionId, "ses_stdio_restart_resume");
    assert.equal(payload.session.threadId, "thread_persisted_account");
    assert.deepEqual(calls.filter((call) => call.method === "thread/start"), []);
    assert.deepEqual(calls.filter((call) => call.method === "thread/resume").map((call) => call.args.threadId), ["thread_persisted_account"]);
    assert.equal(calls.filter((call) => call.method === "turn/start")[0].args.threadId, "thread_persisted_account");
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex stdio starts a fresh thread when a restored browser thread is stale", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_stale_browser_thread",
    createRpcClient: async () => createFakeAppServerClient({
      calls,
      responses: ["fresh thread response"],
      staleResumeThreads: ["thread_stale_browser_restore"]
    })
  });
  const 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: process.cwd(),
    HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  try {
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_stale_browser_thread",
        sessionId: "ses_stale_browser_thread",
        threadId: "thread_stale_browser_restore",
        traceId: "trc_stale_browser_thread",
        message: "刷新浏览器后继续发送"
      },
      {
        now: () => "2026-05-31T01:06:00.000Z",
        env,
        codexStdioManager: manager
      }
    );

    validateCodeAgentChatSchema(payload);
    assert.equal(payload.status, "completed");
    assert.equal(payload.error, undefined);
    assert.equal(payload.session.sessionId, "ses_stale_browser_thread");
    assert.equal(payload.session.threadId, "thread_stdio_ready");
    assert.equal(payload.providerTrace.threadId, "thread_stdio_ready");
    assert.equal(payload.providerTrace.terminalStatus, "completed");
    assert.ok(payload.runnerTrace.events.some((event) => event.label === "thread:resume:stale"));
    assert.deepEqual(calls.filter((call) => call.method !== "initialize").map((call) => call.method), [
      "thread/resume",
      "thread/start",
      "turn/start"
    ]);
    assert.equal(calls.find((call) => call.method === "turn/start")?.args.threadId, "thread_stdio_ready");
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex stdio session_busy includes runnerTrace and providerTrace for visible retry", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_busy_visible",
    createRpcClient: async () => createTimedFakeAppServerClient({
      calls,
      text: "busy owner eventually completes",
      eventEveryMs: 20,
      eventCount: 1,
      completeAfterMs: 200
    })
  });
  const 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: process.cwd(),
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  try {
    const first = handleCodeAgentChat(
      {
        conversationId: "cnv_busy_visible",
        sessionId: "ses_busy_visible",
        traceId: "trc_busy_visible_owner",
        message: "第一轮长请求"
      },
      {
        now: () => "2026-05-31T09:45:00.000Z",
        env,
        codexStdioManager: manager
      }
    );

    await new Promise((resolve) => setTimeout(resolve, 40));
    const busy = await handleCodeAgentChat(
      {
        conversationId: "cnv_busy_visible",
        sessionId: "ses_busy_visible",
        traceId: "trc_busy_visible_retry",
        message: "第二轮快速重试"
      },
      {
        now: () => "2026-05-31T09:45:01.000Z",
        env,
        codexStdioManager: manager
      }
    );

    validateCodeAgentChatSchema(busy);
    assert.equal(busy.status, "failed");
    assert.equal(busy.error.code, "session_busy");
    assert.equal(busy.providerTrace.terminalStatus, "blocked");
    assert.equal(busy.providerTrace.protocol, "codex-app-server-jsonrpc-stdio");
    assert.equal(busy.providerTrace.toolName, "codex-stdio.session_busy");
    assert.equal(busy.runnerTrace.traceId, "trc_busy_visible_retry");
    assert.ok(busy.runnerTrace.events.some((event) => event.label === "session:session_busy"));
    assert.equal(busy.runnerTrace.lastEvent.label, "session:session_busy");
    assert.equal(busy.runnerTrace.lastEvent.terminal, true);
    assert.equal(busy.error.blockers[0].currentTraceId, "trc_busy_visible_owner");
    assert.equal(busy.error.blockers[0].currentTraceStatus, "running");
    assert.match(busy.error.userMessage, /正在处理上一轮/u);

    const completed = await first;
    assert.equal(completed.status, "completed");
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

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-app-server-stdio");
  assert.equal(blocked.error.code, "codex_cli_binary_missing");
  assert.equal(blocked.error.layer, "runner");
  assert.equal(blocked.error.retryable, false);
  assert.match(blocked.error.userMessage, /Codex CLI|Codex stdio/u);
  assert.equal(blocked.error.blocker.runner, "codex-app-server-stdio-runner");
  assert.ok(blocked.error.blockers.some((blocker) => blocker.code === "codex_cli_binary_missing"));
  assert.ok(blocked.availability.codexStdio.blockerCodes.includes("stdio_protocol_not_wired"));
  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("Codex stdio feasibility verifies writable workspace, CODEX_HOME, and version-probed command", async () => {
  const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-stdio-workspace-"));
  const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-codex-home-"));
  const fakeCodex = await createFakeCodexCommand();
  try {
    await prepareFakeCodexHome(codexHome);
    const manager = createCodexStdioSessionManager();
    const availability = manager.describe({
      env: {
        PATH: process.env.PATH,
        OPENAI_API_KEY: "test-openai-key-material",
        CODEX_HOME: codexHome,
        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_CODEX_SANDBOX: "workspace-write",
        HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
      }
    });

    assert.equal(availability.command, fakeCodex.command);
    assert.equal(availability.binary.present, true);
    assert.equal(availability.binary.versionDetected, true);
    assert.equal(availability.workspace, workspace);
    assert.equal(availability.workspaceState.exists, true);
    assert.equal(availability.workspaceState.readable, true);
    assert.equal(availability.workspaceState.writable, true);
    assert.equal(availability.runtimeContract.workspaceMount.status, "ready");
    assert.equal(availability.codexHome, codexHome);
    assert.equal(availability.codexHomeState.exists, true);
    assert.equal(availability.codexHomeState.readable, true);
    assert.equal(availability.codexHomeState.writable, true);
    assert.equal(availability.runtimeContract.codexHome.status, "ready");
    assert.equal(availability.codexHomeFiles.configPresent, true);
    assert.equal(availability.codexHomeFiles.authPresent, true);
    assert.equal(availability.childEnvBoundary.forbiddenEnvPresent, false);
    assert.ok(availability.childEnvBoundary.strippedEnvKeys.includes("OPENAI_API_KEY"));
    assert.ok(availability.childEnvBoundary.strippedEnvKeys.includes("HWLAB_CODE_AGENT_OPENAI_BASE_URL"));
    assert.deepEqual(availability.childEnvBoundary.noProxy.missing, []);
    assert.equal(availability.startupReady, true);
    assert.equal(availability.canStartLongLivedCodexStdio, true);
    assert.equal(availability.ready, false);
    assert.ok(availability.blockerCodes.includes("stdio_protocol_not_wired"));
    assert.equal(availability.blockerCodes.includes("workspace_mount_missing"), false);
    assert.equal(availability.blockerCodes.includes("workspace_write_boundary_blocked"), false);
    assert.equal(availability.blockerCodes.includes("codex_home_missing"), false);
    assert.equal(availability.blockerCodes.includes("codex_home_write_blocked"), false);
  } finally {
    await rm(workspace, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
    await rm(fakeCodex.root, { recursive: true, force: true });
  }
});

test("Codex app-server args pin repo-owned DEV responses egress without leaking provider env", () => {
  const env = {
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses",
    HWLAB_CODE_AGENT_MODEL: "deepseek-chat",
    OPENAI_API_KEY: "test-openai-key-material"
  };

  assert.equal(codexAppServerProviderBaseUrl(env), "http://127.0.0.1:49280");
  const args = codexAppServerArgs(env);
  assert.ok(args.indexOf("app-server") > args.indexOf("review_model=\"deepseek-chat\""));
  assert.deepEqual(args.slice(-2), ["--listen", "stdio://"]);
  assert.ok(args.includes("model_provider=\"OpenAI\""));
  assert.ok(args.includes("model_providers.OpenAI.base_url=\"http://127.0.0.1:49280\""));
  assert.ok(args.includes("model_providers.OpenAI.name=\"OpenAI\""));
  assert.ok(args.includes("model_providers.OpenAI.wire_api=\"responses\""));
  assert.ok(args.includes("model=\"deepseek-chat\""));
  assert.ok(args.includes("review_model=\"deepseek-chat\""));
  assert.equal(args.join(" ").includes("test-openai-key-material"), false);
  assert.equal(args.join(" ").includes("/v1/responses"), false);
});

test("Codex stdio skills discovery returns bounded manifest facts instead of generic model text", async () => {
  const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-stdio-skills-ready-"));
  const workspace = path.join(root, "workspace");
  const skillsDir = path.join(root, "skills");
  const codexHome = await prepareFakeCodexHome(path.join(root, "codex-home"));
  const fakeCodex = await createFakeCodexCommand();
  const calls = [];
  await mkdir(path.join(workspace), { recursive: true });
  await mkdir(path.join(skillsDir, "alpha"), { recursive: true });
  await writeFile(path.join(skillsDir, "alpha", "SKILL.md"), [
    "---",
    "name: alpha-skill",
    "description: Alpha manifest summary from SKILL.md.",
    "version: v1.2.3",
    "commitId: abc123def456",
    "---",
    "",
    "# Alpha"
  ].join("\n"), "utf8");

  try {
    const manager = createCodexStdioSessionManager({
      idFactory: () => "ses_stdio_skills_ready",
      createRpcClient: async () => createFakeAppServerClient({
        calls,
        responses: ["模型泛化回答：我可以使用很多 skill。"]
      })
    });
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_skills_ready",
        traceId: "trc_stdio_skills_ready",
        message: "列出你能使用的所有skill"
      },
      {
        now: () => "2026-05-24T00:10:00.000Z",
        codexStdioManager: manager,
        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:49280/responses"
        }
      }
    );

    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.runner.kind, "codex-app-server-stdio-runner");
    assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
    assert.equal(payload.skills.status, "ready");
    assert.equal(payload.skills.count, 1);
    assert.equal(payload.skills.totalCount, 1);
    assert.equal(payload.skills.items[0].name, "alpha-skill");
    assert.equal(payload.skills.items[0].summary, "Alpha manifest summary from SKILL.md.");
    assert.equal(payload.skills.items[0].version, "v1.2.3");
    assert.equal(payload.skills.items[0].commitId, "abc123def456");
    assert.equal(payload.skills.items[0].source, path.join(skillsDir, "alpha", "SKILL.md"));
    assert.equal(payload.skills.items[0].manifest.path, path.join(skillsDir, "alpha", "SKILL.md"));
    assert.match(payload.reply.content, /真实 skill manifest 清单/u);
    assert.match(payload.reply.content, /数量：本次返回 1 个；已发现总数 1 个/u);
    assert.match(payload.reply.content, /alpha-skill/u);
    assert.match(payload.reply.content, /version=v1\.2\.3/u);
    assert.match(payload.reply.content, /commitId=abc123def456/u);
    assert.doesNotMatch(payload.reply.content, /模型泛化回答/u);
    assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "completed"));
    assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "codex-app-server.thread/start+turn/start" && toolCall.status === "completed"));
    const turnPrompt = calls.find((call) => call.method === "turn/start")?.args?.prompt ?? "";
    assert.match(turnPrompt, /Current skills discovery facts/u);
    assert.match(turnPrompt, /alpha-skill/u);
    assert.match(turnPrompt, /commitId=abc123def456/u);
    assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
  } finally {
    await rm(root, { recursive: true, force: true });
    await rm(fakeCodex.root, { recursive: true, force: true });
  }
});

test("Codex stdio skills discovery returns structured blocker when manifests are missing", async () => {
  const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-stdio-skills-missing-"));
  const workspace = path.join(root, "workspace");
  const codexHome = await prepareFakeCodexHome(path.join(root, "codex-home"));
  const fakeCodex = await createFakeCodexCommand();
  await mkdir(workspace, { recursive: true });

  try {
    const manager = createCodexStdioSessionManager({
      idFactory: () => "ses_stdio_skills_missing",
      createRpcClient: async () => createFakeAppServerClient({
        responses: ["模型泛化回答：我可以使用 alpha-skill 和 beta-skill。"]
      })
    });
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_skills_missing",
        traceId: "trc_stdio_skills_missing",
        message: "列出你可用的skills"
      },
      {
        now: () => "2026-05-24T00:10:30.000Z",
        codexStdioManager: manager,
        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(root, "missing-skills"),
          HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
          HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
        }
      }
    );

    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.runner.kind, "codex-app-server-stdio-runner");
    assert.equal(payload.runnerTrace.runnerKind, "codex-app-server-stdio-runner");
    assert.equal(payload.error.code, "skills_unavailable");
    assert.equal(payload.error.retryable, false);
    assert.equal(payload.skills.status, "blocked");
    assert.equal(payload.skills.count, 0);
    assert.deepEqual(payload.skills.sourceIssues, ["pikasTech/HWLAB#136", "pikasTech/HWLAB#237"]);
    assert.ok(payload.skills.blockers.some((blocker) => blocker.sourceIssue === "pikasTech/HWLAB#136"));
    assert.ok(payload.skills.blockers.some((blocker) => blocker.sourceIssue === "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.notEqual(payload.runner.kind, "openai-responses-fallback");
    assert.notEqual(payload.provider, "openai-responses");
    assert.equal(JSON.stringify(payload).includes("alpha-skill"), false);
    assert.ok(payload.runnerTrace.events.some((event) => event.label === "stdio:ready"));
    assert.ok(payload.runnerTrace.events.some((event) => event.label === "skills.discover:blocked"));
  } finally {
    await rm(root, { recursive: true, force: true });
    await rm(fakeCodex.root, { recursive: true, force: true });
  }
});

test("Codex stdio runner startup failure marks session failed but keeps the session resumable", async () => {
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_stdio_start_failed",
    createProbeRpcClient: async () => ({
      async initialize() {
        return { initialized: true };
      },
      close() {}
    }),
    createRpcClient: async () => ({
      async initialize() {
        throw new Error("app-server initialize failed with sk-test-secret and TOKEN=abc123");
      },
      close() {}
    })
  });
  const 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_WORKSPACE: process.cwd(),
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  const payload = await handleCodeAgentChat(
    {
      conversationId: "cnv_stdio_start_failed",
      traceId: "trc_stdio_start_failed",
      message: "启动后回答"
    },
    {
      now: () => "2026-05-23T00:08:00.000Z",
      codexStdioManager: manager,
      env
    }
  );

  validateCodeAgentChatSchema(payload);
  assert.equal(payload.status, "failed");
  assert.equal(payload.provider, "codex-stdio");
  assert.equal(payload.error.code, "codex_stdio_failed");
  assert.equal(payload.session.sessionId, "ses_stdio_start_failed");
  assert.equal(payload.session.status, "failed");
  assert.equal(payload.session.sessionMode, "codex-app-server-stdio-long-lived");
  assert.equal(payload.session.statusReason, "codex_stdio_failed");
  assert.equal(payload.longLivedSessionGate.status, "pass");
  assert.equal(payload.longLivedSessionGate.pass, true);
  assert.equal(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "session_failed"), false);
  assert.equal(payload.capabilityLevel, "blocked");
  assert.equal(payload.runnerTrace.sessionStatus, "failed");
  assert.equal(payload.runnerTrace.valuesPrinted, false);
  const serialized = JSON.stringify(payload);
  assert.equal(serialized.includes("sk-test-secret"), false);
  assert.equal(serialized.includes("TOKEN=abc123"), false);
  assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false);
  await rm(fakeCodex.root, { recursive: true, force: true });
  await rm(codexHome, { recursive: true, force: true });
});

test("Codex app-server retrying error notification waits for completed turn", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_stdio_retrying_turn",
    createRpcClient: async () => createFakeAppServerClient({
      calls,
      responses: ["retry recovered stdio reply"],
      retryingTurn: { turn: 1, message: "Reconnecting... 1/5" }
    })
  });
  const 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_WORKSPACE: process.cwd(),
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  const payload = await handleCodeAgentChat(
    {
      conversationId: "cnv_stdio_retrying_turn",
      traceId: "trc_stdio_retrying_turn",
      message: "需要长会话"
    },
    {
      now: () => "2026-05-23T00:08:45.000Z",
      codexStdioManager: manager,
      env
    }
  );

  validateCodeAgentChatSchema(payload);
  assert.equal(payload.status, "completed");
  assert.equal(payload.reply.content, "retry recovered stdio reply");
  assert.equal(payload.session.status, "idle");
  assert.equal(payload.longLivedSessionGate.status, "pass");
  assert.equal(payload.runnerTrace.valuesPrinted, false);
  assert.ok(payload.runnerTrace.events.some((event) => event.label === "app-server:retrying"));
  assert.ok(payload.runnerTrace.events.some((event) => event.label === "turn:completed:completed"));
  assert.equal(payload.runnerTrace.events.some((event) => event.label === "app-server:error"), false);
  assert.deepEqual(calls.filter((call) => call.method !== "initialize").map((call) => call.method), ["thread/start", "turn/start"]);
  assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, true);
  await rm(fakeCodex.root, { recursive: true, force: true });
  await rm(codexHome, { recursive: true, force: true });
});

test("Codex app-server activity timeout does not fail an active turn", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_stdio_active_turn",
    createRpcClient: async () => createTimedFakeAppServerClient({
      calls,
      text: "active turn completed",
      eventEveryMs: 20,
      eventCount: 5,
      completeAfterMs: 130,
      commandExecution: {
        id: "cmd_stdio_trace_summary",
        command: ["tran_cmd", "gws_DESKTOP:/f/work/constart", "dir"],
        exitCode: 0,
        durationMs: 456,
        aggregatedOutput: "FREQ_Controller_FW.uvprojx\nkeil\n"
      }
    })
  });
  const 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_WORKSPACE: process.cwd(),
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  try {
    const started = Date.now();
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_active_turn",
        traceId: "trc_stdio_active_turn",
        message: "需要超过 activity timeout 但持续输出事件"
      },
      {
        now: () => "2026-05-24T00:09:00.000Z",
        codexStdioManager: manager,
        env,
        timeoutMs: 35,
        hardTimeoutMs: 500
      }
    );

    validateCodeAgentChatSchema(payload);
    assert.equal(payload.status, "completed");
    assert.equal(payload.reply.content, "active turn completed");
    assert.ok(Date.now() - started >= 100);
    assert.ok(payload.runnerTrace.events.some((event) => event.label === "item/commandExecution/outputDelta"));
    const commandEvent = payload.runnerTrace.events.find((event) => event.label === "item/commandExecution:completed");
    assert.ok(commandEvent, JSON.stringify(payload.runnerTrace.events));
    assert.equal(commandEvent.toolName, "commandExecution");
    assert.equal(commandEvent.command, "tran_cmd gws_DESKTOP:/f/work/constart dir");
    assert.equal(commandEvent.exitCode, 0);
    assert.equal(commandEvent.durationMs, 456);
    assert.match(commandEvent.stdoutSummary, /FREQ_Controller_FW\.uvprojx/);
    assert.equal(payload.runnerTrace.events.some((event) => event.label === "timeout:no_activity"), false);
    assert.equal(payload.error, undefined);
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex app-server activity timeout fails only after no new events", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_stdio_idle_timeout",
    createRpcClient: async () => createTimedFakeAppServerClient({
      calls,
      eventCount: 0,
      completeAfterMs: null
    })
  });
  const 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_WORKSPACE: process.cwd(),
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  try {
    const started = Date.now();
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_idle_timeout",
        traceId: "trc_stdio_idle_timeout",
        message: "停止输出事件后触发 idle timeout"
      },
      {
        now: () => "2026-05-24T00:09:30.000Z",
        codexStdioManager: manager,
        env,
        timeoutMs: 30,
        hardTimeoutMs: 500
      }
    );

    validateCodeAgentChatSchema(payload);
    assert.equal(payload.status, "timeout");
    assert.equal(payload.error.code, "codex_stdio_idle_timeout");
    assert.match(payload.error.message, /no new events/u);
    assert.equal(payload.error.timeoutMs, 30);
    assert.ok(payload.error.idleMs >= 30);
    assert.match(payload.error.lastActivityAt, /^\d{4}-\d{2}-\d{2}T/u);
    assert.equal(payload.error.waitingFor, "turn/completed");
    assert.equal(payload.error.diagnosis.code, "codex_kernel_stalled");
    assert.equal(payload.diagnosis.code, "codex_kernel_stalled");
    assert.ok(Date.now() - started >= 30);
    const timeoutEvent = payload.runnerTrace.events.find((event) => event.label === "timeout:no_activity");
    assert.ok(timeoutEvent);
    assert.equal(timeoutEvent.timeoutMs, 30);
    assert.ok(timeoutEvent.idleMs >= 30);
    assert.match(timeoutEvent.lastActivityAt, /^\d{4}-\d{2}-\d{2}T/u);
    assert.equal(timeoutEvent.waitingFor, "turn/completed");
    assert.equal(timeoutEvent.diagnosisCode, "codex_kernel_stalled");
    assert.equal(payload.runnerTrace.events.some((event) => event.label === "timeout:hard_cap"), false);
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex app-server no-progress diagnosis identifies tool-result ack stalls", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_stdio_tool_ack_stall",
    createRpcClient: async () => createTimedFakeAppServerClient({
      calls,
      eventEveryMs: 12,
      eventCount: 0,
      completeAfterMs: null,
      commandExecution: {
        id: "cmd_stdio_tool_ack_stall",
        command: ["hwpod", "workspace", "ls", "."],
        exitCode: 0,
        durationMs: 123,
        aggregatedOutput: "workspace listing completed\n"
      }
    })
  });
  const 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_WORKSPACE: process.cwd(),
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  try {
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_tool_ack_stall",
        traceId: "trc_stdio_tool_ack_stall",
        message: "工具结果已返回但 turn 不结束"
      },
      {
        now: () => "2026-05-31T16:00:00.000Z",
        codexStdioManager: manager,
        env,
        timeoutMs: 35,
        hardTimeoutMs: 500
      }
    );

    validateCodeAgentChatSchema(payload);
    assert.equal(payload.status, "timeout");
    assert.equal(payload.error.code, "codex_stdio_idle_timeout");
    assert.equal(payload.error.diagnosis.code, "app_server_tool_result_ack_stalled");
    assert.equal(payload.error.diagnosis.layer, "codex-kernel");
    assert.equal(payload.error.diagnosis.lastRoutedEvent.label, "item/commandExecution:completed");
    assert.equal(payload.error.blocker.diagnosis.code, "app_server_tool_result_ack_stalled");
    const timeoutEvent = payload.runnerTrace.events.find((event) => event.label === "timeout:no_activity");
    assert.ok(timeoutEvent, JSON.stringify(payload.runnerTrace.events));
    assert.equal(timeoutEvent.diagnosisCode, "app_server_tool_result_ack_stalled");
    assert.equal(timeoutEvent.diagnosis.lastRoutedEvent.label, "item/commandExecution:completed");
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex app-server partial assistant output without turn completion is a timeout, not completed", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_stdio_partial_timeout",
    createRpcClient: async () => createPartialAssistantNoTurnCompletedClient({
      calls,
      text: "partial text that must not be trusted as completed"
    })
  });
  const 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_WORKSPACE: process.cwd(),
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  try {
    const payload = await handleCodeAgentChat(
      {
        conversationId: "cnv_stdio_partial_timeout",
        traceId: "trc_stdio_partial_timeout",
        message: "输出半截后不发 turn/completed"
      },
      {
        now: () => "2026-05-24T00:09:45.000Z",
        codexStdioManager: manager,
        env,
        timeoutMs: 30,
        hardTimeoutMs: 500
      }
    );

    validateCodeAgentChatSchema(payload);
    assert.equal(payload.status, "timeout");
    assert.equal(payload.error.code, "codex_stdio_idle_timeout");
    assert.equal(Object.hasOwn(payload, "reply"), false);
    assert.equal(payload.session.status, "timeout");
    assert.equal(payload.providerTrace, undefined);
    assert.equal(payload.runnerTrace.events.some((event) => event.label === "assistant:chunk"), false);
    assert.equal(payload.runnerTrace.assistantStreams?.[0]?.chunkCount, 1);
    assert.ok(payload.runnerTrace.events.some((event) => event.label === "assistant:item_completed"));
    assert.ok(payload.runnerTrace.events.some((event) => event.label === "timeout:no_activity"));
    assert.equal(payload.runnerTrace.events.some((event) => event.label === "turn:completed:idle_timeout_after_assistant"), false);
    assert.equal(payload.runnerTrace.events.some((event) => event.label === "assistant:completed"), false);
    assert.deepEqual(calls.filter((call) => call.method !== "initialize").map((call) => call.method), ["thread/start", "turn/start"]);
  } finally {
    await rm(fakeCodex.root, { recursive: true, force: true });
    await rm(codexHome, { recursive: true, force: true });
  }
});

test("Codex app-server failed turn is not wrapped as completed assistant output", async () => {
  const calls = [];
  const fakeCodex = await createFakeCodexCommand();
  const codexHome = await prepareFakeCodexHome();
  const manager = createCodexStdioSessionManager({
    idFactory: () => "ses_stdio_failed_turn",
    createRpcClient: async () => createFakeAppServerClient({
      calls,
      failedTurn: { turn: 1, message: "provider network failed with sk-test-secret and TOKEN=abc123" }
    })
  });
  const 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_WORKSPACE: process.cwd(),
    HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:49280/responses"
  };

  const payload = await handleCodeAgentChat(
    {
      conversationId: "cnv_stdio_failed_turn",
      traceId: "trc_stdio_failed_turn",
      message: "需要长会话"
    },
    {
      now: () => "2026-05-23T00:08:30.000Z",
      codexStdioManager: manager,
      env
    }
  );

  validateCodeAgentChatSchema(payload);
  assert.equal(payload.status, "failed");
  assert.equal(payload.error.code, "codex_stdio_failed");
  assert.equal(Object.hasOwn(payload, "reply"), false);
  assert.equal(payload.session.status, "failed");
  assert.equal(payload.longLivedSessionGate.status, "pass");
  assert.equal(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "session_failed"), false);
  assert.equal(payload.error.message.includes("sk-test-secret"), false);
  assert.equal(payload.error.message.includes("TOKEN=abc123"), false);
  assert.equal(payload.providerTrace.protocol, "codex-app-server-jsonrpc-stdio");
  assert.equal(payload.providerTrace.terminalStatus, "failed");
  assert.equal(payload.providerTrace.toolName, "codex-app-server.thread/start+turn/start");
  assert.deepEqual(calls.filter((call) => call.method !== "initialize").map((call) => call.method), ["thread/start", "turn/start"]);
  assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false);
  await rm(fakeCodex.root, { recursive: true, force: true });
  await rm(codexHome, { recursive: true, force: true });
});
