Files
pikasTech-HWLAB/internal/cloud/code-agent-session-registry.test.ts
T

2307 lines
94 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 cmdcmd /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 skillsuccess=truereturn_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 });
});