Files
pikasTech-HWLAB/internal/cloud/code-agent-session-registry.test.mjs
T
2026-05-26 15:37:01 +08:00

1773 lines
72 KiB
JavaScript
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 "node:test";
import { createCodeAgentSessionRegistry } from "./code-agent-session-registry.mjs";
import { handleCodeAgentChat, validateCodeAgentChatSchema } from "./code-agent-chat.mjs";
import {
codexAppServerArgs,
codexAppServerProviderBaseUrl,
createCodexStdioSessionManager
} from "./codex-stdio-session.mjs";
import {
classifyCodeAgentChatReadiness,
classifyCodexRunnerCapability
} from "../../scripts/src/code-agent-response-contract.mjs";
import {
HWLAB_M3_IO_API_BASE_URL_ENV,
HWLAB_M3_IO_CAPABILITY_LEVELS,
HWLAB_M3_IO_API_ROUTE,
HWLAB_M3_STATUS_API_ROUTE
} from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
test("code agent session registry creates, reuses, and expires sessions without storing secrets", () => {
const registry = createCodeAgentSessionRegistry({
idleTimeoutMs: 1000,
idFactory: () => "ses_test-registry"
});
const first = registry.acquire({
conversationId: "cnv_registry",
workspace: "/workspace/hwlab",
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_registry_1",
now: "2026-05-23T00:00:00.000Z"
});
assert.equal(first.ok, true);
assert.equal(first.reused, false);
assert.equal(first.session.sessionId, "ses_test-registry");
assert.equal(first.session.status, "busy");
assert.equal(first.session.lifecycleStatus, "busy");
assert.equal(first.session.lifecycle.busy, true);
assert.equal(first.session.workspace, "/workspace/hwlab");
assert.equal(first.session.sandbox, "read-only");
assert.equal(first.session.runnerKind, "hwlab-readonly-runner");
assert.equal(first.session.sessionMode, "controlled-readonly-session-registry");
assert.equal(first.session.capabilityLevel, "read-only-session-tools");
assert.equal(first.session.longLivedSession, true);
assert.equal(first.session.durableSession, false);
assert.equal(first.session.codexStdio, false);
assert.equal(first.session.writeCapable, false);
assert.equal(first.session.readOnly, true);
assert.equal(first.session.idleTimeoutMs, 1000);
assert.equal(first.session.lastTraceId, "trc_registry_1");
assert.equal(first.session.secretMaterialStored, false);
assert.equal(first.session.valuesRedacted, true);
const describedCreating = registry.describe();
assert.equal(describedCreating.sessionMode, "controlled-readonly-session-registry");
assert.equal(describedCreating.longLivedSession, true);
assert.equal(describedCreating.longLivedCodexStdio, false);
assert.equal(describedCreating.recentSessions[0].sessionId, "ses_test-registry");
assert.equal(describedCreating.recentSessions[0].sessionMode, "controlled-readonly-session-registry");
assert.equal(describedCreating.recentSessions[0].workspace, "/workspace/hwlab");
assert.equal(describedCreating.recentSessions[0].secretMaterialStored, false);
assert.deepEqual(describedCreating.lifecycleStatuses, ["creating", "ready", "busy", "idle", "interrupted", "expired", "failed"]);
assert.equal(describedCreating.statusAliases.canceled, "interrupted");
registry.release(first.session.sessionId, {
conversationId: "cnv_registry",
traceId: "trc_registry_1",
reused: first.session.reused,
now: "2026-05-23T00:00:00.010Z"
});
const second = registry.acquire({
conversationId: "cnv_registry",
workspace: "/workspace/hwlab",
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_registry_2",
now: "2026-05-23T00:00:00.500Z"
});
assert.equal(second.ok, true);
assert.equal(second.reused, true);
assert.equal(second.session.sessionId, "ses_test-registry");
assert.equal(second.session.turn, 2);
assert.equal(second.session.lastTraceId, "trc_registry_2");
assert.equal(second.session.lifecycleStatus, "busy");
registry.release(second.session.sessionId, {
conversationId: "cnv_registry",
traceId: "trc_registry_2",
reused: second.session.reused,
now: "2026-05-23T00:00:00.510Z"
});
const expired = registry.acquire({
conversationId: "cnv_registry",
workspace: "/workspace/hwlab",
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_registry_expired",
now: "2026-05-23T00:00:02.000Z"
});
assert.equal(expired.ok, false);
assert.equal(expired.code, "session_expired");
assert.equal(expired.session.status, "expired");
assert.equal(expired.session.lifecycleStatus, "expired");
assert.match(expired.session.lifecycle.userMessage, /会话已过期/u);
assert.equal(expired.blocker.sourceIssue, "pikasTech/HWLAB#317");
});
test("code agent session registry stores bounded non-sensitive conversation facts", () => {
const registry = createCodeAgentSessionRegistry({
maxConversationFacts: 2,
maxFactSkills: 2,
idFactory: () => "ses_fact_registry"
});
const acquired = registry.acquire({
conversationId: "cnv_fact_registry",
workspace: "/workspace/hwlab",
traceId: "trc_fact_registry_1",
now: "2026-05-23T00:00:00.000Z"
});
registry.release(acquired.session.sessionId, {
conversationId: "cnv_fact_registry",
traceId: "trc_fact_registry_1",
now: "2026-05-23T00:00:00.010Z"
});
registry.recordFact("cnv_fact_registry", {
traceId: "trc_fact_registry_1",
provider: "codex-readonly-runner",
backend: "hwlab-cloud-api/codex-readonly-runner",
workspace: "/workspace/hwlab",
sandbox: "read-only",
session: acquired.session,
sessionMode: "controlled-readonly-session-registry",
capabilityLevel: "read-only-session-tools",
runner: {
kind: "hwlab-readonly-runner",
sessionId: "ses_fact_registry"
},
runnerTrace: {
traceId: "trc_fact_registry_1",
runnerKind: "hwlab-readonly-runner",
sessionMode: "controlled-readonly-session-registry",
events: ["tool:pwd:completed"],
valuesPrinted: false
},
toolCalls: [{
name: "pwd",
status: "completed",
stdout: "must-not-store-output sk-secret-value OPENAI_API_KEY=secret",
traceId: "trc_fact_registry_1"
}],
skills: {
status: "ready",
items: [
{ name: "alpha" },
{ name: "beta" },
{ name: "gamma" }
],
count: 3,
totalCount: 3
}
}, { now: "2026-05-23T00:00:00.020Z" });
registry.recordFact("cnv_fact_registry", {
traceId: "trc_fact_registry_2",
workspace: "/workspace/hwlab",
runner: { kind: "hwlab-readonly-runner" },
toolCalls: [{
name: "skills.discover",
status: "completed",
stdout: "must-not-store-output sk-secret-value OPENAI_API_KEY=secret"
}],
skills: {
status: "ready",
items: [
{ name: "alpha" },
{ name: "beta" },
{ name: "gamma" }
],
count: 3,
totalCount: 3
}
}, { now: "2026-05-23T00:00:00.030Z" });
registry.recordFact("cnv_fact_registry", {
traceId: "trc_fact_registry_3",
workspace: "/workspace/hwlab",
runner: { kind: "hwlab-readonly-runner" },
toolCalls: [{ name: "session.context", status: "completed" }]
}, { now: "2026-05-23T00:00:00.040Z" });
const facts = registry.getConversationFacts("cnv_fact_registry");
assert.equal(facts.conversationId, "cnv_fact_registry");
assert.equal(facts.sessionId, "ses_fact_registry");
assert.equal(facts.sessionLifecycleStatus, "idle");
assert.equal(facts.workspace, "/workspace/hwlab");
assert.equal(facts.turnCount, 2);
assert.deepEqual(facts.traceIds, ["trc_fact_registry_2", "trc_fact_registry_3"]);
assert.equal(facts.valuesRedacted, true);
assert.equal(facts.secretMaterialStored, false);
assert.deepEqual(facts.latestSkills.names, ["alpha", "beta"]);
assert.equal(facts.latestSkills.truncated, true);
assert.equal(JSON.stringify(facts).includes("must-not-store-output"), false);
assert.equal(JSON.stringify(facts).includes("sk-secret-value"), false);
assert.equal(JSON.stringify(facts).includes("OPENAI_API_KEY=secret"), false);
assert.match(facts.summary, /workspace=\/workspace\/hwlab/u);
});
test("code agent session registry reports busy sessions as structured blocker", () => {
const registry = createCodeAgentSessionRegistry({
idFactory: () => "ses_busy"
});
const first = registry.acquire({
conversationId: "cnv_busy",
workspace: "/workspace/hwlab",
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_busy_1",
now: "2026-05-23T00:00:00.000Z"
});
assert.equal(first.ok, true);
const busy = registry.acquire({
conversationId: "cnv_busy",
workspace: "/workspace/hwlab",
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_busy_2",
now: "2026-05-23T00:00:01.000Z"
});
assert.equal(busy.ok, false);
assert.equal(busy.code, "session_busy");
assert.equal(busy.session.status, "busy");
assert.equal(busy.session.lifecycleStatus, "busy");
assert.match(busy.session.lifecycle.userMessage, /会话忙\/请等待/u);
assert.equal(busy.session.currentTraceId, "trc_busy_1");
});
test("code agent session registry blocks failed and interrupted sessions without reusing them", () => {
const failedRegistry = createCodeAgentSessionRegistry({
idFactory: () => "ses_failed"
});
const failedSeed = failedRegistry.acquire({
conversationId: "cnv_failed",
workspace: "/workspace/hwlab",
traceId: "trc_failed_1",
now: "2026-05-23T00:00:00.000Z"
});
assert.equal(failedSeed.ok, true);
failedRegistry.fail(failedSeed.session.sessionId, {
conversationId: "cnv_failed",
traceId: "trc_failed_1",
now: "2026-05-23T00:00:00.010Z"
});
const failed = failedRegistry.acquire({
conversationId: "cnv_failed",
workspace: "/workspace/hwlab",
traceId: "trc_failed_2",
now: "2026-05-23T00:00:01.000Z"
});
assert.equal(failed.ok, false);
assert.equal(failed.code, "session_failed");
assert.equal(failed.session.status, "failed");
assert.equal(failed.session.lifecycleStatus, "failed");
assert.match(failed.session.lifecycle.userMessage, /需重新发送建立新会话/u);
assert.equal(failed.blocker.sourceIssue, "pikasTech/HWLAB#317");
const interruptedRegistry = createCodeAgentSessionRegistry({
idFactory: () => "ses_interrupted"
});
const interruptedSeed = interruptedRegistry.acquire({
conversationId: "cnv_interrupted",
workspace: "/workspace/hwlab",
traceId: "trc_interrupted_1",
now: "2026-05-23T00:00:00.000Z"
});
assert.equal(interruptedSeed.ok, true);
interruptedRegistry.interrupt(interruptedSeed.session.sessionId, {
conversationId: "cnv_interrupted",
traceId: "trc_interrupted_1",
now: "2026-05-23T00:00:00.010Z"
});
const interrupted = interruptedRegistry.acquire({
conversationId: "cnv_interrupted",
workspace: "/workspace/hwlab",
traceId: "trc_interrupted_2",
now: "2026-05-23T00:00:01.000Z"
});
assert.equal(interrupted.ok, false);
assert.equal(interrupted.code, "session_interrupted");
assert.equal(interrupted.session.status, "interrupted");
assert.equal(interrupted.session.lifecycleStatus, "interrupted");
});
test("Codex stdio readiness blocker does not use read-only session fallback", async () => {
const registry = createCodeAgentSessionRegistry({
idFactory: () => "ses_chat_registry"
});
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_chat_registry",
traceId: "trc_chat_registry_pwd",
message: "pwd"
},
{
now: () => "2026-05-23T00:01:00.000Z",
sessionRegistry: registry,
env: {
PATH: "",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_CODEX_COMMAND: path.join(os.tmpdir(), "hwlab-missing-codex"),
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_CODE_AGENT_CODEX_WORKSPACE: process.cwd()
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "failed");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
assert.equal(payload.error.code, "codex_cli_binary_missing");
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
assert.equal(payload.runner.kind === "hwlab-readonly-runner", false);
assert.equal(payload.capabilityLevel, "blocked");
assert.equal(payload.skills.status, "not_requested");
assert.deepEqual(payload.toolCalls, []);
assert.equal(payload.session, null);
assert.equal(payload.sessionLifecycleStatus, "failed");
assert.equal(payload.sessionSummary.degraded, true);
assert.equal(payload.longLivedSessionGate.status, "blocked");
assert.equal(payload.longLivedSessionGate.pass, false);
assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "codex_cli_binary_missing"));
assert.equal(payload.runnerLimitations.includes("no-controlled-readonly-fallback"), true);
assert.equal(Object.hasOwn(payload, "reply"), false);
assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false);
});
test("stale read-only conversation facts do not mask Codex stdio blockers", async () => {
const registry = createCodeAgentSessionRegistry({
idFactory: () => "ses_stale_readonly"
});
const acquired = registry.acquire({
conversationId: "cnv_stale_readonly",
workspace: process.cwd(),
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_stale_readonly_seed",
now: "2026-05-23T00:02:00.000Z"
});
registry.release(acquired.session.sessionId, {
conversationId: "cnv_stale_readonly",
traceId: "trc_stale_readonly_seed",
now: "2026-05-23T00:02:00.001Z"
});
registry.recordFact("cnv_stale_readonly", {
traceId: "trc_stale_readonly_seed",
workspace: process.cwd(),
session: acquired.session,
runner: { kind: "hwlab-readonly-runner" },
sessionMode: "controlled-readonly-session-registry",
capabilityLevel: "read-only-session-tools",
toolCalls: [{ name: "skills.discover", status: "completed" }],
skills: {
status: "ready",
items: [{ name: "alpha-skill" }],
count: 1,
totalCount: 1
}
}, { now: "2026-05-23T00:02:00.002Z" });
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_stale_readonly",
traceId: "trc_stale_readonly_request",
message: "根据上一轮列出你能使用的所有skill"
},
{
now: () => "2026-05-23T00:02:01.000Z",
sessionRegistry: registry,
env: {
PATH: "",
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
HWLAB_CODE_AGENT_CODEX_COMMAND: path.join(os.tmpdir(), "hwlab-missing-codex"),
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_CODE_AGENT_CODEX_WORKSPACE: process.cwd()
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "failed");
assert.equal(payload.error.code, "codex_cli_binary_missing");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
assert.equal(payload.capabilityLevel, "blocked");
assert.equal(payload.skills.status, "not_requested");
assert.equal(Object.hasOwn(payload, "reply"), false);
assert.equal(JSON.stringify(payload).includes("alpha-skill"), true);
assert.equal(payload.conversationFacts.latestSkills.names[0], "alpha-skill");
assert.equal(payload.error.blocker.conversationFacts.latestSkills.names[0], "alpha-skill");
assert.equal(payload.runner.kind === "hwlab-readonly-runner", false);
assert.equal(payload.provider === "openai-responses", false);
});
test("expired, busy, and failed session states are surfaced as structured blockers", async () => {
const expiredRegistry = createCodeAgentSessionRegistry({
idleTimeoutMs: 10,
idFactory: () => "ses_expired_chat"
});
const first = expiredRegistry.acquire({
conversationId: "cnv_expired_chat",
workspace: process.cwd(),
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_expired_seed",
now: "2026-05-23T00:02:00.000Z"
});
expiredRegistry.release(first.session.sessionId, {
conversationId: "cnv_expired_chat",
traceId: "trc_expired_seed",
now: "2026-05-23T00:02:00.001Z"
});
expiredRegistry.recordFact("cnv_expired_chat", {
traceId: "trc_expired_seed",
workspace: process.cwd(),
session: first.session,
runner: { kind: "hwlab-readonly-runner" },
sessionMode: "controlled-readonly-session-registry",
capabilityLevel: "read-only-session-tools",
toolCalls: [{ name: "pwd", status: "completed" }]
}, { now: "2026-05-23T00:02:00.002Z" });
const expiredAcquire = expiredRegistry.acquire({
conversationId: "cnv_expired_chat",
workspace: process.cwd(),
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_expired_request",
now: "2026-05-23T00:02:01.000Z"
});
const expired = codeAgentBlockedPayloadFromAcquire(expiredAcquire, {
conversationId: "cnv_expired_chat",
traceId: "trc_expired_request",
conversationFacts: expiredRegistry.getConversationFacts("cnv_expired_chat")
});
assert.equal(expired.status, "failed");
assert.equal(expired.error.code, "session_expired");
assert.equal(expired.error.layer, "session");
assert.equal(expired.error.retryable, true);
assert.match(expired.error.userMessage, /session 已过期/u);
assert.equal(expired.error.blocker.code, "session_expired");
assert.equal(expired.error.blocker.traceId, "trc_expired_request");
assert.equal(expired.session.status, "expired");
assert.equal(expired.sessionLifecycleStatus, "expired");
assert.equal(expired.sessionSummary.requiresNewSession, true);
assert.match(expired.sessionSummary.userMessage, /会话已过期/u);
assert.equal(expired.capabilityLevel, "blocked");
assert.equal(expired.longLivedSessionGate.status, "blocked");
assert.equal(expired.conversationFacts.conversationId, "cnv_expired_chat");
assert.equal(expired.conversationFacts.sessionId, "ses_expired_chat");
assert.equal(expired.conversationFacts.turnCount, 1);
assert.equal(expired.conversationFacts.recentToolCalls[0].name, "pwd");
assert.equal(expired.error.blocker.conversationFacts.sessionId, "ses_expired_chat");
const busyRegistry = createCodeAgentSessionRegistry({
idFactory: () => "ses_busy_chat"
});
busyRegistry.acquire({
conversationId: "cnv_busy_chat",
workspace: process.cwd(),
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_busy_seed",
now: "2026-05-23T00:03:00.000Z"
});
busyRegistry.recordFact("cnv_busy_chat", {
traceId: "trc_busy_seed",
workspace: process.cwd(),
sessionId: "ses_busy_chat",
runner: { kind: "hwlab-readonly-runner" },
sessionMode: "controlled-readonly-session-registry",
capabilityLevel: "read-only-session-tools",
toolCalls: [{ name: "skills.discover", status: "completed" }],
skills: {
status: "ready",
items: [{ name: "alpha-skill" }],
count: 1,
totalCount: 1
}
}, { now: "2026-05-23T00:03:00.001Z" });
const busyAcquire = busyRegistry.acquire({
conversationId: "cnv_busy_chat",
workspace: process.cwd(),
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_busy_request",
now: "2026-05-23T00:03:01.000Z"
});
const busy = codeAgentBlockedPayloadFromAcquire(busyAcquire, {
conversationId: "cnv_busy_chat",
traceId: "trc_busy_request",
conversationFacts: busyRegistry.getConversationFacts("cnv_busy_chat")
});
assert.equal(busy.status, "failed");
assert.equal(busy.error.code, "session_busy");
assert.equal(busy.error.layer, "session");
assert.equal(busy.error.retryable, true);
assert.match(busy.error.userMessage, /正在处理上一轮/u);
assert.equal(busy.error.blocker.code, "session_busy");
assert.equal(busy.session.status, "busy");
assert.equal(busy.sessionLifecycleStatus, "busy");
assert.equal(busy.sessionSummary.busy, true);
assert.match(busy.sessionSummary.userMessage, /会话忙\/请等待/u);
assert.equal(busy.capabilityLevel, "blocked");
assert.equal(busy.error.blockers[0].sourceIssue, "pikasTech/HWLAB#317");
assert.equal(busy.conversationFacts.conversationId, "cnv_busy_chat");
assert.equal(busy.conversationFacts.sessionId, "ses_busy_chat");
assert.equal(busy.conversationFacts.latestSkills.names[0], "alpha-skill");
const failedRegistry = createCodeAgentSessionRegistry({
idFactory: () => "ses_failed_chat"
});
const failedSeed = failedRegistry.acquire({
conversationId: "cnv_failed_chat",
workspace: process.cwd(),
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_failed_seed",
now: "2026-05-23T00:04:00.000Z"
});
failedRegistry.fail(failedSeed.session.sessionId, {
conversationId: "cnv_failed_chat",
traceId: "trc_failed_seed",
now: "2026-05-23T00:04:00.001Z"
});
failedRegistry.recordFact("cnv_failed_chat", {
traceId: "trc_failed_seed",
workspace: process.cwd(),
session: failedSeed.session,
runner: { kind: "hwlab-readonly-runner" },
sessionMode: "controlled-readonly-session-registry",
capabilityLevel: "read-only-session-tools",
toolCalls: [{ name: "pwd", status: "completed" }]
}, { now: "2026-05-23T00:04:00.002Z" });
const failedAcquire = failedRegistry.acquire({
conversationId: "cnv_failed_chat",
workspace: process.cwd(),
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_failed_request",
now: "2026-05-23T00:04:01.000Z"
});
const failed = codeAgentBlockedPayloadFromAcquire(failedAcquire, {
conversationId: "cnv_failed_chat",
traceId: "trc_failed_request",
conversationFacts: failedRegistry.getConversationFacts("cnv_failed_chat")
});
assert.equal(failed.status, "failed");
assert.equal(failed.error.code, "session_failed");
assert.equal(failed.error.layer, "session");
assert.equal(failed.error.retryable, true);
assert.match(failed.error.userMessage, /session 已失败/u);
assert.equal(failed.error.blocker.code, "session_failed");
assert.equal(failed.session.status, "failed");
assert.equal(failed.sessionLifecycleStatus, "failed");
assert.equal(failed.sessionSummary.requiresNewSession, true);
assert.match(failed.sessionSummary.userMessage, /需重新发送建立新会话/u);
assert.equal(failed.capabilityLevel, "blocked");
assert.equal(failed.conversationFacts.conversationId, "cnv_failed_chat");
assert.equal(failed.conversationFacts.sessionId, "ses_failed_chat");
assert.equal(failed.conversationFacts.recentToolCalls[0].name, "pwd");
});
test("OpenAI fallback hook is not used when Codex stdio is unavailable", async () => {
let providerCalled = false;
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_fallback_gate",
traceId: "trc_fallback_gate",
message: "你好"
},
{
now: () => "2026-05-23T00:04:00.000Z",
env: {
PATH: "",
HWLAB_CODE_AGENT_PROVIDER: "openai",
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "1",
HWLAB_CODE_AGENT_CODEX_COMMAND: path.join(os.tmpdir(), "hwlab-missing-codex"),
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_CODE_AGENT_CODEX_WORKSPACE: process.cwd()
},
callProvider: async () => {
providerCalled = true;
return {
provider: "openai-responses",
model: "gpt-test",
backend: "hwlab-cloud-api/openai-responses",
content: "普通文本回复。",
usage: null,
providerTrace: { source: "test-provider" }
};
}
}
);
validateCodeAgentChatSchema(payload);
assert.equal(payload.status, "failed");
assert.equal(payload.error.code, "codex_cli_binary_missing");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
assert.equal(payload.capabilityLevel, "blocked");
assert.equal(payload.runner.kind === "openai-responses-fallback", false);
assert.equal(payload.provider === "openai-responses", false);
assert.equal(Object.hasOwn(payload, "reply"), false);
assert.equal(providerCalled, false);
assert.equal(payload.sessionLifecycleStatus, "failed");
assert.equal(payload.sessionSummary.degraded, true);
assert.equal(payload.runnerLimitations.includes("no-text-fallback"), true);
assert.equal(classifyCodexRunnerCapability(payload, { httpStatus: 200 }).capabilityPass, false);
const oneShot = {
...payload,
provider: "codex-cli",
backend: "hwlab-cloud-api/codex-cli",
runner: {
kind: "codex-cli-one-shot-ephemeral",
codexStdio: false,
writeCapable: false,
durableSession: false,
longLivedSession: false
},
sessionMode: "ephemeral-one-shot",
implementationType: "codex-cli-one-shot-ephemeral",
longLivedSessionGate: {
status: "blocked",
pass: false,
blockers: [{ code: "one_shot_runner_not_long_lived", sourceIssue: "pikasTech/HWLAB#317" }]
}
};
assert.equal(classifyCodexRunnerCapability(oneShot, { httpStatus: 200 }).capabilityPass, false);
assert.equal(classifyCodeAgentChatReadiness(oneShot, { realDevLive: true, httpStatus: 200 }).devLiveReplyPass, false);
});
test("OpenAI provider mode still preserves facts but does not fallback without Codex stdio", async () => {
const registry = createCodeAgentSessionRegistry({
idFactory: () => "ses_provider_context"
});
let providerCalled = false;
const env = {
PATH: "",
HWLAB_CODE_AGENT_PROVIDER: "openai",
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "1",
HWLAB_CODE_AGENT_CODEX_COMMAND: path.join(os.tmpdir(), "hwlab-missing-codex"),
HWLAB_CODE_AGENT_WORKSPACE: process.cwd(),
HWLAB_CODE_AGENT_CODEX_WORKSPACE: process.cwd()
};
const seed = registry.acquire({
conversationId: "cnv_provider_context",
workspace: process.cwd(),
sandbox: "read-only",
runnerKind: "hwlab-readonly-runner",
capabilityLevel: "read-only-session-tools",
traceId: "trc_provider_context_pwd",
now: "2026-05-23T00:04:09.000Z"
});
const released = registry.release(seed.session.sessionId, {
conversationId: "cnv_provider_context",
traceId: "trc_provider_context_pwd",
now: "2026-05-23T00:04:09.010Z"
});
registry.recordFact("cnv_provider_context", {
traceId: "trc_provider_context_pwd",
workspace: process.cwd(),
session: released,
runner: { kind: "hwlab-readonly-runner" },
sessionMode: "controlled-readonly-session-registry",
capabilityLevel: "read-only-session-tools",
toolCalls: [{ name: "pwd", status: "completed" }]
}, { now: "2026-05-23T00:04:09.020Z" });
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_provider_context",
traceId: "trc_provider_context_openai",
message: "请根据刚才的上下文简要说明"
},
{
now: () => "2026-05-23T00:04:11.000Z",
sessionRegistry: registry,
env,
callProvider: async () => {
providerCalled = true;
return {
provider: "openai-responses",
model: "gpt-test",
backend: "hwlab-cloud-api/openai-responses",
content: "普通 fallback 回复不应被调用。",
usage: null,
providerTrace: { source: "test-provider" }
};
}
}
);
assert.equal(payload.status, "failed");
assert.equal(payload.error.code, "codex_cli_binary_missing");
assert.equal(payload.provider, "codex-stdio");
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
assert.equal(payload.conversationFacts.turnCount, 1);
assert.equal(payload.conversationFacts.sessionId, released.sessionId);
assert.equal(payload.conversationFacts.recentToolCalls[0].name, "pwd");
assert.equal(payload.conversationFacts.latestTraceId, "trc_provider_context_pwd");
assert.equal(payload.error.blocker.conversationFacts.sessionId, released.sessionId);
assert.equal(Object.hasOwn(payload, "reply"), false);
assert.equal(providerCalled, false);
assert.equal(JSON.stringify(payload.conversationFacts).includes("sk-"), false);
});
test("Code Agent PC gateway prompt reaches Codex stdio instead of internal hardware shortcut", async () => {
const calls = [];
let providerCalled = false;
const fakeCodex = await createFakeCodexCommand();
const codexHome = await prepareFakeCodexHome();
const registry = createCodeAgentSessionRegistry();
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_pc_gateway_stdio",
createRpcClient: async () => createFakeAppServerClient({
calls,
responses: ["已通过受控 wrapper 执行 PC gateway 命令。"]
})
});
try {
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_pc_gateway_stdio",
traceId: "trc_pc_gateway_stdio",
projectId: "prj_mvp_topology",
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
resourceId: "res_windows_host",
capabilityId: "cap_windows_cmd_exec",
message: "请通过 PC gateway shell.exec 执行 Windows 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://172.26.26.227:17680/v1/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-shell\.mjs/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, /--powershell-stdin/u);
assert.match(turn.args.prompt, /-EncodedCommand/u);
assert.match(turn.args.prompt, /Windows filesystem inventory/u);
assert.match(turn.args.prompt, /Select-Object -First/u);
assert.match(turn.args.prompt, /ConvertTo-HwlabJson/u);
} finally {
await rm(fakeCodex.root, { recursive: true, force: true });
await rm(codexHome, { recursive: true, force: true });
}
});
test("Codex stdio manager rebuilds app-server client when provider profile config changes", 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://172.26.26.227:17680/v1/responses"
}
});
assert.deepEqual(createdModels, ["deepseek-chat", "gpt-5.5"]);
assert.deepEqual(closedModels, ["deepseek-chat"]);
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("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`请用 /app/tools/hwlab-gateway-shell.mjs 通过 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://172.26.26.227:17680/v1/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-shell\.mjs/u);
assert.match(turn.args.prompt, /--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 } = {}) {
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_ready" } } });
return { threadId: "thread_stdio_ready" };
},
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_${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 session 已失败,可重新发送建立新的 session。";
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 } = {}) {
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 (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 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://172.26.26.227:17680/v1/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://172.26.26.227:17680/v1/responses",
HWLAB_CODE_AGENT_MODEL: "deepseek-chat",
OPENAI_API_KEY: "test-openai-key-material"
};
assert.equal(codexAppServerProviderBaseUrl(env), "http://172.26.26.227:17680/v1");
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://172.26.26.227:17680/v1\""));
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://172.26.26.227:17680/v1/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://172.26.26.227:17680/v1/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 and blocks long-lived gate", 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://172.26.26.227:17680/v1/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, "blocked");
assert.equal(payload.longLivedSessionGate.pass, false);
assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "session_failed"));
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://172.26.26.227:17680/v1/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
})
});
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://172.26.26.227:17680/v1/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"));
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://172.26.26.227:17680/v1/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.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(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 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://172.26.26.227:17680/v1/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.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://172.26.26.227:17680/v1/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, "blocked");
assert.ok(payload.longLivedSessionGate.blockers.some((blocker) => blocker.code === "session_failed"));
assert.equal(payload.error.message.includes("sk-test-secret"), false);
assert.equal(payload.error.message.includes("TOKEN=abc123"), false);
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 });
});