fix: remove obsolete code agent gates

This commit is contained in:
lyon
2026-05-25 22:10:07 +08:00
parent c133aff770
commit 9e584be848
19 changed files with 61 additions and 2674 deletions
+1 -1
View File
@@ -750,7 +750,7 @@ function resolveProviderPlan(env, options = {}) {
mode: "codex-cli"
};
}
if (provider === "codex-stdio" || provider === "codex-mcp-stdio") {
if (provider === "codex-stdio") {
return {
provider: CODEX_STDIO_PROVIDER,
model,
@@ -873,78 +873,6 @@ test("Code Agent Keil gateway prompt is not blocked by M3 IO intent guard", asyn
}
});
test("Code Agent natural-language Keil and serial monitor request goes through Codex stdio", async () => {
const calls = [];
let providerCalled = false;
const fakeCodex = await createFakeCodexCommand();
const codexHome = await prepareFakeCodexHome();
const registry = createCodeAgentSessionRegistry();
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_freq_keil_serial_stdio",
createRpcClient: async () => createFakeAppServerClient({
calls,
responses: ["已进入 F:/work/constart,完成 71-FREQ Keil build/download,并通过 serial-monitor 抓取启动日志。"]
})
});
try {
const payload = await handleCodeAgentChat(
{
conversationId: "cnv_freq_keil_serial_stdio",
traceId: "trc_freq_keil_serial_stdio",
projectId: "prj_mvp_topology",
message: "去 f:/work/constart 找到 71-FREQ 工程,用 keil skill 编译下载,用 serial-monitor skill 抓启动日志"
},
{
now: () => "2026-05-25T10:51:24.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("natural-language Keil and serial monitor 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);
assert.equal(payload.toolCalls.some((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io"), 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, /f:\/work\/constart/iu);
assert.match(turn.args.prompt, /71-FREQ/u);
assert.match(turn.args.prompt, /\/app\/tools\/hwlab-gateway-shell\.mjs/u);
assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills\\keil/u);
assert.match(turn.args.prompt, /keil-cli\.py/u);
assert.match(turn.args.prompt, /C:\\Users\\liang\\\.agents\\skills\\serial-monitor/u);
assert.match(turn.args.prompt, /monitor start/u);
assert.match(turn.args.prompt, /--powershell-stdin/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));
}
@@ -1240,97 +1168,6 @@ test("Codex app-server args pin repo-owned DEV responses egress without leaking
assert.equal(args.join(" ").includes("/v1/responses"), false);
});
test("repo-owned Codex stdio manager creates and reuses long-lived sessions with trace evidence", async () => {
const calls = [];
const fakeCodex = await createFakeCodexCommand();
const codexHome = await prepareFakeCodexHome();
const registry = createCodeAgentSessionRegistry();
const manager = createCodexStdioSessionManager({
idFactory: () => "ses_stdio_ready",
createRpcClient: async () => createFakeAppServerClient({ calls })
});
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 first = await handleCodeAgentChat(
{
conversationId: "cnv_stdio_ready",
traceId: "trc_stdio_ready_1",
message: "first"
},
{
now: () => "2026-05-23T00:06:00.000Z",
codexStdioManager: manager,
sessionRegistry: registry,
env
}
);
validateCodeAgentChatSchema(first);
assert.equal(first.status, "completed");
assert.equal(first.provider, "codex-stdio");
assert.equal(first.runner.kind, "codex-app-server-stdio-runner");
assert.equal(first.sessionMode, "codex-app-server-stdio-long-lived");
assert.equal(first.implementationType, "repo-owned-codex-app-server-stdio-session");
assert.equal(first.session.longLivedSession, true);
assert.equal(first.session.codexStdio, true);
assert.equal(first.runner.writeCapable, true);
assert.equal(first.runner.durableSession, true);
assert.equal(first.capabilityLevel, "long-lived-codex-stdio-session");
assert.equal(first.longLivedSessionGate.status, "pass");
assert.equal(first.longLivedSessionGate.pass, true);
assert.deepEqual(first.runnerLimitations, ["secret-values-redacted"]);
assert.equal(first.runnerLimitations.includes("not-codex-stdio"), false);
assert.equal(first.runnerLimitations.includes("not-write-capable"), false);
assert.equal(first.runnerLimitations.includes("process-local-session-registry"), false);
assert.equal(classifyCodexRunnerCapability(first, { httpStatus: 200 }).capabilityPass, true);
const second = await handleCodeAgentChat(
{
conversationId: "cnv_stdio_ready",
traceId: "trc_stdio_ready_2",
message: "second"
},
{
now: () => "2026-05-23T00:06:01.000Z",
codexStdioManager: manager,
sessionRegistry: registry,
env
}
);
assert.equal(second.status, "completed");
assert.equal(second.sessionId, first.sessionId);
assert.equal(second.conversationId, first.conversationId);
assert.equal(second.sessionReuse.reused, true);
assert.equal(second.sessionReuse.turn, 2);
assert.equal(second.runnerTrace.sessionId, first.sessionId);
assert.equal(second.runnerTrace.sessionReused, true);
assert.equal(second.workspace, first.workspace);
assert.equal(second.sandbox, first.sandbox);
assert.equal(second.toolCalls[0].name, "codex-app-server.thread/resume+turn/start");
assert.equal(second.reply.content, "second stdio reply");
const turnPathCalls = calls.filter((call) => call.method !== "initialize");
assert.deepEqual(turnPathCalls.map((call) => call.method), ["thread/start", "turn/start", "thread/resume", "turn/start"]);
assert.equal(turnPathCalls[0].args.cwd, process.cwd());
assert.equal(turnPathCalls[2].args.threadId, "thread_stdio_ready");
assert.match(turnPathCalls[3].args.prompt, /Prior session facts/u);
assert.match(turnPathCalls[3].args.prompt, /ses_stdio_ready/u);
assert.match(turnPathCalls[3].args.prompt, /trc_stdio_ready_1/u);
assert.match(turnPathCalls[3].args.prompt, new RegExp(escapeRegExp(process.cwd()), "u"));
assert.equal(second.providerTrace.protocol, "codex-app-server-jsonrpc-stdio");
await rm(fakeCodex.root, { recursive: true, force: true });
await rm(codexHome, { recursive: true, force: true });
});
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");
+3 -3
View File
@@ -246,10 +246,10 @@ function buildSessionRunnerReadiness(codeAgent = {}) {
return {
status,
ready: runnerReady,
kind: stdioReady ? stdio.kind ?? "codex-mcp-stdio-runner" : runner.kind ?? "unknown",
kind: stdioReady ? stdio.kind ?? "codex-app-server-stdio-runner" : runner.kind ?? "unknown",
provider: stdioReady ? stdio.provider ?? "codex-stdio" : runner.provider ?? codeAgent?.provider ?? "unknown",
backend: stdioReady ? stdio.backend ?? "hwlab-cloud-api/codex-mcp-stdio" : runner.backend ?? codeAgent?.backend ?? "unknown",
mode: stdioReady ? "codex-mcp-stdio-long-lived" : runner.sessionMode ?? runner.mode ?? codeAgent?.sessionMode ?? "unknown",
backend: stdioReady ? stdio.backend ?? "hwlab-cloud-api/codex-app-server-stdio" : runner.backend ?? codeAgent?.backend ?? "unknown",
mode: stdioReady ? "codex-app-server-stdio-long-lived" : runner.sessionMode ?? runner.mode ?? codeAgent?.sessionMode ?? "unknown",
capabilityLevel: stdioReady ? "long-lived-codex-stdio-session" : runner.capabilityLevel ?? codeAgent?.capabilityLevel ?? "unknown",
longLivedSession: stdioReady || runner.longLivedSession === true,
durableSession: stdioReady || runner.durableSession === true || runner.durable === true,
File diff suppressed because it is too large Load Diff
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -10,7 +10,6 @@ import {
runDevCloudWorkbenchLayoutSmoke,
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
runDevCloudWorkbenchSessionContinuityFixtureSmoke,
runDevCloudWorkbenchSlowBlockerFixtureSmoke,
runDevCloudWorkbenchStaticSmoke,
runDevCloudWorkbenchTimeoutFixtureSmoke,
sanitizeAgentChatBody
@@ -252,15 +251,6 @@ test("source/default smoke covers #288 gate single-table contract", () => {
assert.equal(report.checks.some((item) => item.id === "feedback-119-gate-retains-diagnostics"), false);
});
test("source/default smoke covers Code Agent quick prompt fill-only contract", () => {
const report = runDevCloudWorkbenchStaticSmoke();
const check = report.checks.find((item) => item.id === "code-agent-quick-prompts-contract");
assert.equal(check?.status, "pass");
assert.equal(check.evidence.includes("CodexDO1=true 后读 DI1"), true);
assert.equal(check.evidence.includes("CodexDO1=false 复核 DI1"), true);
assert.equal(check.evidence.some((item) => /我点击发送后才确认执行这次写入请求/u.test(item)), true);
});
test("live workbench identity passes only when runtime commit or image tag matches current source", () => {
assert.equal(
classifyLiveDeploymentIdentity(sourceIdentity, {
@@ -522,48 +512,6 @@ test("Code Agent readiness classifier blocks completed payloads over any non-2xx
}
});
test("Code Agent browser classifier accepts only completed HTTP success plus completed UI and real provider evidence", () => {
const summary = sanitizeAgentChatBody({
status: "completed",
provider: "codex-stdio",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-mcp-stdio",
traceId: "trc_completed",
reply: {
content: "ok"
},
longLivedSessionGate: {
status: "pass",
pass: true
}
}, { httpStatus: 200 });
const pass = classifyCodeAgentBrowserJourney({
responseSummary: summary,
httpOk: true,
httpStatus: 200,
ui: {
agentChatStatus: "DEV-LIVE 回复",
completedMessageVisible: true,
failedMessageVisible: false
}
});
assert.equal(pass.status, "pass");
const blocked = classifyCodeAgentBrowserJourney({
responseSummary: summary,
httpOk: false,
httpStatus: 500,
ui: {
agentChatStatus: "DEV-LIVE 回复",
completedMessageVisible: true,
failedMessageVisible: false
}
});
assert.equal(blocked.status, "blocked");
assert.equal(blocked.blocker, "provider-upstream");
});
test("Code Agent browser classifier blocks completed payloads without backend evidence", () => {
const summary = sanitizeAgentChatBody({
status: "completed",
@@ -612,35 +560,6 @@ test("local Code Agent timeout fixture keeps bounded timeout state, trace contex
assert.equal(timeoutCheck.observations.ui.completedMessageVisible, false);
});
test("local slow structured blocker fixture preserves trace, pending state, and retry input", async () => {
const report = await runDevCloudWorkbenchSlowBlockerFixtureSmoke({
responseDelayMs: 5200
});
if (report.status === "skip") {
assert.match(report.summary, /Playwright is unavailable/u);
return;
}
assert.equal(report.status, "pass", JSON.stringify(report.blockers, null, 2));
assert.equal(report.evidenceLevel, "SOURCE");
assert.equal(report.devLive, false);
const blockerCheck = report.checks.find((check) => check.id === "local-agent-fixture-slow-structured-blocker");
assert.equal(blockerCheck?.status, "pass");
assert.equal(blockerCheck.observations.ui.agentChatStatus, "Runner 受阻");
assert.equal(blockerCheck.observations.ui.failedMessageVisible, true);
assert.equal(blockerCheck.observations.ui.failedMessageHasTrace, true);
assert.equal(blockerCheck.observations.ui.retryInputPreserved, true);
assert.equal(blockerCheck.observations.ui.retryInputValue, "列出你能使用的所有skill");
assert.equal(blockerCheck.observations.prompt.legacyWindow.permanentFailureAround4500ms, false);
assert.equal(blockerCheck.observations.prompt.legacyWindow.pendingChineseVisible, true);
assert.equal(blockerCheck.observations.prompt.response.provider, "codex-stdio");
assert.equal(blockerCheck.observations.prompt.response.traceId, blockerCheck.observations.prompt.traceId);
assert.equal(blockerCheck.observations.prompt.response.error.code, "skills_unavailable");
assert.equal(blockerCheck.observations.prompt.response.skills.status, "blocked");
assert.equal(blockerCheck.observations.prompt.classification.blocker, "runner-blocked");
});
test("local session continuity fixture reuses Code Agent context and retries with degraded-copy guard", async () => {
const report = await runDevCloudWorkbenchSessionContinuityFixtureSmoke();
if (report.status === "skip") {
+1 -4
View File
@@ -500,10 +500,7 @@ export function classifyCodeAgentRuntimeBlock(payload, { httpStatus = null } = {
"codex_stdio_failed",
"codex_stdio_protocol_blocked",
"codex_stdio_empty_response",
"codex_stdio_command_probe_failed",
"external_network_blocked",
"network_tool_unavailable",
"network_timeout"
"codex_stdio_command_probe_failed"
].includes(errorCode)) {
return {
blocked: true,
+13 -206
View File
@@ -4471,8 +4471,6 @@ async function inspectJourneyUi(page) {
runtimePathShowsProviderFields: /provider|runnerKind|protocol|implementationType|providerTrace\.command|providerTrace\.terminalStatus/u.test(runtimePathDialogText),
runtimePathSummaryStaysCompact: !/providerTrace\.command|providerTrace\.terminalStatus|protocol/u.test(runtimePathText),
runtimePathFallbackNotFull: !/(OpenAI text fallback|source-fixture|SOURCE)[\s\S]{0,120}(真实 runner|repo-owned Codex app-server stdio)/u.test(runtimePathText),
runtimePathMcpNotFull: !/(MCP|codex-mcp-stdio-runner|mcp-jsonrpc-stdio)/u.test(runtimePathText) ||
(/DEGRADEDMCP|MCP\/其他 runner/u.test(runtimePathText) && !/使用 Responses wire API|wire_api=responses|真实 runnerCodex app-server stdio/u.test(runtimePathText)),
noMainAttributionNoise: document.querySelectorAll(".message-card.message-agent .message-attribution, .message-card.message-agent .message-evidence, .message-card.message-agent .code-agent-facts").length === 0,
attributionFallbackNotRunnerControl: !/openai-responses-fallback[\s\S]{0,80}(真实 runner|Codex stdio 长会话|workspace-write)/u.test(
tracePanels
@@ -4609,15 +4607,6 @@ export async function runDevCloudWorkbenchLocalAgentFixtureSmoke() {
});
}
export async function runDevCloudWorkbenchSlowBlockerFixtureSmoke(options = {}) {
return runLocalAgentFixtureSmoke({
mode: "local-agent-slow-blocker-fixture-browser",
responseDelayMs: options.responseDelayMs ?? localAgentFixtureDelayMs,
agentFixtureMode: "slow-blocker",
expectTimeout: false
});
}
export async function runDevCloudWorkbenchSessionContinuityFixtureSmoke(options = {}) {
let chromium;
try {
@@ -4952,7 +4941,6 @@ async function runLocalAgentFixtureSmoke({
mode,
responseDelayMs = 0,
timeoutConfigMs = null,
agentFixtureMode = "success",
expectTimeout = false
} = {}) {
let chromium;
@@ -4992,7 +4980,7 @@ async function runLocalAgentFixtureSmoke({
};
}
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode });
const server = await startStaticWebServer({ agentFixture: true, agentDelayMs: responseDelayMs, agentFixtureMode: "success" });
let browser;
try {
browser = await chromium.launch({ headless: true });
@@ -5052,22 +5040,17 @@ async function runLocalAgentFixtureSmoke({
{ timeout: 8000 }
);
} else {
const prompts = agentFixtureMode === "slow-blocker"
? [codeAgentE2ePrompts[1]]
: codeAgentE2ePrompts;
for (const prompt of prompts) {
for (const prompt of codeAgentE2ePrompts) {
promptResults.push(await runCodeAgentPromptJourney(page, prompt, { sourceFixture: true }));
if (agentFixtureMode === "success") {
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
}
await page.waitForFunction(
() => document.querySelector("#agent-chat-status")?.textContent?.trim() === "SOURCE 回复",
null,
{ timeout: 8000 }
);
}
}
const ui = await inspectJourneyUi(page);
const mobilePending = !expectTimeout && agentFixtureMode === "success"
const mobilePending = !expectTimeout
? await inspectLocalAgentPendingViewport(browser, server.url, {
width: 390,
height: 844,
@@ -5087,30 +5070,7 @@ async function runLocalAgentFixtureSmoke({
ui.timeoutActionPanelContained &&
!ui.completedMessageVisible
)
: agentFixtureMode === "slow-blocker"
? (
promptResults.length === 1 &&
promptResults[0]?.status === "blocked" &&
promptResults[0]?.classification?.blocker === "runner-blocked" &&
promptResults[0]?.legacyWindow?.permanentFailureAround4500ms === false &&
promptResults[0]?.legacyWindow?.pendingChineseVisible === true &&
promptResults[0]?.legacyWindow?.pendingTraceShowsEvents === true &&
ui.agentChatStatus === "Runner 受阻" &&
ui.failedMessageVisible &&
ui.failedMessageHasTrace &&
ui.retryInputPreserved &&
ui.failedRetryActionVisible &&
ui.failedTraceActionVisible &&
ui.failedActionPanelContained &&
ui.runtimePathVisible &&
ui.runtimePathCompactSummaryVisible &&
ui.runtimePathUsesCompactDialog &&
ui.runtimePathShowsProviderFields &&
ui.runtimePathSummaryStaysCompact &&
ui.runtimePathMcpNotFull &&
!ui.completedMessageVisible
)
: (
: (
promptResults.every((result) =>
result.status === "pass" &&
result.response?.status === "completed" &&
@@ -5170,8 +5130,6 @@ async function runLocalAgentFixtureSmoke({
status: pass ? "pass" : "blocked",
summary: expectTimeout
? "Local SOURCE fixture proves a bounded Code Agent timeout stays user-visible/BLOCKED in the UI, preserves trace context, and keeps the user's input for retry."
: agentFixtureMode === "slow-blocker"
? "Local SOURCE fixture returns a delayed structured blocker and the conversation shows the Chinese reason with trace evidence while preserving the input."
: "Local SOURCE fixture sends input and reaches a user-visible replied state without marking the fixture as DEV-LIVE.",
observations: expectTimeout ? {
response: responseSummary,
@@ -5185,7 +5143,7 @@ async function runLocalAgentFixtureSmoke({
}
}
];
if (!expectTimeout && agentFixtureMode === "success") {
if (!expectTimeout) {
checks.push({
id: "local-agent-fixture-no-4500ms-permanent-failure",
status: promptResults.every((result) =>
@@ -5221,18 +5179,6 @@ async function runLocalAgentFixtureSmoke({
summary: "390x844 mobile view shows Chinese Code Agent pending status, trace/session context, and no pending-card text overflow before the delayed response arrives.",
observations: mobilePending
});
} else if (!expectTimeout && agentFixtureMode === "slow-blocker") {
checks.push({
id: "local-agent-fixture-slow-structured-blocker",
status: pass ? "pass" : "blocked",
summary: "Delayed structured blocker arrives after the legacy 4500ms window, displays Runner 受阻 instead of a generic 后端失败 state, keeps trace visible, and leaves the user prompt for continuing the same conversation.",
observations: {
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
prompt: promptResults[0] ?? null,
ui
}
});
}
const blockers = checks
.filter((check) => check.status !== "pass")
@@ -5258,7 +5204,7 @@ async function runLocalAgentFixtureSmoke({
safety: {
...staticSafety(),
localFixtureOnly: true,
agentFixtureMode,
agentFixtureMode: "success",
fixtureTrustBoundary: "SOURCE fixture replies render as SOURCE 回复 and never as DEV-LIVE 回复.",
legacyFailureWindowMs,
fixtureDelayMs: responseDelayMs,
@@ -6177,8 +6123,8 @@ function startLocalAgentTrace(agentTraceStore, { traceId, conversationId, delayM
function localAgentTraceSnapshot({ traceId, conversationId = null, startedAt = Date.now(), events = [], lastEvent = null }) {
return {
traceId,
runnerKind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId: conversationId,
sessionStatus: events.length > 0 ? "running" : "pending",
status: events.length > 0 ? "running" : "pending",
@@ -6877,145 +6823,6 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
}));
return true;
}
if (options.agentFixtureMode === "slow-blocker") {
jsonResponse(response, 200, {
conversationId,
sessionId: conversationId,
messageId,
status: "failed",
sourceKind: "SOURCE",
evidenceLevel: "SOURCE",
createdAt: timestamp,
updatedAt: timestamp,
traceId,
provider: "codex-stdio",
model: "codex-stdio",
backend: "local-source-fixture/codex-stdio-slow-structured-blocker",
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
workspace: "/workspace/hwlab",
sandbox: "read-only",
runner: {
kind: "codex-mcp-stdio-runner",
writeCapable: true,
codexStdio: true,
longLivedSession: true,
durableSession: true
},
capabilityLevel: "blocked",
sessionMode: "codex-mcp-stdio-long-lived",
session: {
sessionId: conversationId,
conversationId,
status: "idle",
turn: 1,
lastTraceId: traceId
},
sessionReuse: {
conversationId,
sessionId: conversationId,
mapped: true,
reused: true,
turn: 1,
status: "idle"
},
error: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
message: "SOURCE slow structured blocker: no readable SKILL.md files were found.",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true,
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true
}
},
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
userMessage: "技能发现暂时受阻:后端没有返回可读取的 SKILL.md 清单;输入已保留,可继续同一会话稍后重试。",
retryable: true
},
blockers: [
{
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli",
retryable: true
}
],
toolCalls: [
{
name: "skills.discover",
status: "blocked",
exitCode: 2,
traceId,
stderrSummary: "skills_unavailable",
blocker: {
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli"
}
}
],
skills: {
status: "blocked",
items: [],
count: 0,
totalCount: 0,
blockers: [
{
code: "skills_unavailable",
category: "runner_blocked",
layer: "skill-cli"
}
]
},
runnerTrace: {
traceId,
runnerKind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
status: "blocked",
events: [
{ seq: 1, traceId, type: "session", stage: "created", status: "completed", label: "session:created" },
{ seq: 2, traceId, type: "prompt", stage: "sent", status: "completed", label: "prompt:sent" },
{ seq: 3, traceId, type: "error", stage: "blocked", status: "blocked", label: "codex-stdio:blocked", errorCode: "skills_unavailable", waitingFor: "codex-stdio" }
],
lastEvent: { seq: 3, traceId, type: "error", stage: "blocked", status: "blocked", label: "codex-stdio:blocked", errorCode: "skills_unavailable", waitingFor: "codex-stdio" }
},
conversationFacts: {
conversationId,
sessionId: conversationId,
sessionStatus: "idle",
sessionMode: "codex-mcp-stdio-long-lived",
turnCount: 1,
latestTraceId: traceId,
traceIds: [traceId],
latestSkills: {
status: "blocked",
count: 0,
totalCount: 0,
names: [],
blockers: ["skills_unavailable"]
},
recentToolCalls: [{ name: "skills.discover", status: "blocked" }],
facts: [],
valuesRedacted: true,
secretMaterialStored: false
},
providerTrace: {
source: "SOURCE-local-browser-fixture",
sourceKind: "SOURCE",
responseId: "rsp_source_fixture_slow_blocker",
redacted: true
}
});
return true;
}
const fixtureSkills = isSkillListPrompt
? {
status: "ready",
+3 -3
View File
@@ -301,7 +301,7 @@ export function classifyCodeAgentSkillPath(payload, { httpStatus = null, httpOk
);
const runnerKind = payload?.runner?.kind ?? null;
const controlledSessionRunner =
runnerKind === "codex-mcp-stdio-runner" ||
runnerKind === "codex-app-server-stdio-runner" ||
runnerKind === "hwlab-readonly-runner";
const missing = [];
if (!httpOk) missing.push(`HTTP ${httpStatus ?? "not_observed"}`);
@@ -507,7 +507,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
const routeEvidenceReady = route === HWLAB_M3_IO_API_ROUTE || route === M3_IO_CONTROL_ROUTE;
const routePass = httpOk === true &&
payload?.status === "completed" &&
payload?.provider === "hwlab-skill-cli" &&
payload?.provider === "legacy-skill-cli" &&
skillTool &&
["completed", "blocked"].includes(skillTool.status) &&
routeEvidenceReady &&
@@ -522,7 +522,7 @@ export function classifySkillCliM3Route(payload, { httpStatus = null, httpOk = f
const missing = [];
if (!httpOk) missing.push(`HTTP ${httpStatus ?? "not_observed"}`);
if (payload?.status !== "completed") missing.push("payload.status=completed");
if (payload?.provider !== "hwlab-skill-cli") missing.push("provider=hwlab-skill-cli");
if (payload?.provider !== "legacy-skill-cli") missing.push("provider=legacy-skill-cli");
if (!skillTool) missing.push(`${HWLAB_M3_IO_SKILL_NAME} toolCall`);
if (!routeEvidenceReady) missing.push(`route=${HWLAB_M3_IO_API_ROUTE}`);
if (method !== "POST") missing.push("method=POST");
+16 -271
View File
@@ -142,12 +142,12 @@ test("Code Agent Codex stdio skill discovery passes with completed toolCalls and
traceId: "trc_test",
provider: "codex-stdio",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-mcp-stdio-runner",
backend: "hwlab-cloud-api/codex-app-server-stdio",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
session: {
sessionId: "ses_test",
status: "idle",
@@ -161,17 +161,17 @@ test("Code Agent Codex stdio skill discovery passes with completed toolCalls and
turn: 1
},
runner: {
kind: "codex-mcp-stdio-runner",
kind: "codex-app-server-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
codexStdio: true,
writeCapable: true,
durableSession: true
},
runnerTrace: {
runnerKind: "codex-mcp-stdio-runner"
runnerKind: "codex-app-server-stdio-runner"
},
longLivedSessionGate: {
status: "pass",
@@ -286,139 +286,6 @@ test("Code Agent fallback-only turns cannot satisfy session checks", () => {
assert.ok(result.observations.blockers.includes("fallback-only"));
});
test("Skill CLI M3 route passes with operation/audit/evidence ids and no direct path", () => {
const result = classifySkillCliM3Route(skillCliPayload(), { httpStatus: 200, httpOk: true });
assert.equal(result.status, "pass");
assert.equal(result.observations.route, "/v1/m3/io");
assert.equal(result.observations.method, "POST");
assert.equal(result.observations.idsPresent, true);
assert.equal(result.observations.accepted, true);
assert.equal(result.observations.controlReady, true);
assert.equal(result.observations.readback.value, true);
assert.equal(result.observations.safety.directGatewayCalls, false);
assert.equal(result.observations.safety.fallbackUsed, false);
});
test("Skill CLI M3 route passes with structured blocker and no direct path", () => {
const payload = skillCliPayload({
accepted: false,
status: "blocked",
operationId: null,
auditId: null,
evidenceId: null,
blocker: {
code: "hwlab_api_unavailable",
message: "HWLAB API unavailable"
}
});
const result = classifySkillCliM3Route(payload, { httpStatus: 200, httpOk: true });
assert.equal(result.status, "pass");
assert.equal(result.observations.structuredBlocker, true);
assert.equal(result.observations.idsPresent, false);
});
test("Skill CLI M3 route blocks missing route and direct gateway evidence", () => {
const payload = skillCliPayload({
route: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101/invoke",
directGatewayCalls: true
});
const result = classifySkillCliM3Route(payload, { httpStatus: 200, httpOk: true });
assert.equal(result.status, "blocked");
assert.match(result.summary, /route=\/v1\/m3\/io/u);
assert.equal(result.observations.safety.directGatewayCalls, true);
});
test("Skill CLI M3 route requires explicit POST method evidence", () => {
const result = classifySkillCliM3Route(skillCliPayload({ method: null }), { httpStatus: 200, httpOk: true });
assert.equal(result.status, "blocked");
assert.match(result.summary, /method=POST/u);
assert.ok(result.observations.missing.includes("method=POST"));
});
test("RPT-004 live report defaults to read-only M3 route checks and records no live mutation", async () => {
const report = await buildRpt004Report({
...parseArgs([
"--live",
"--url",
"http://74.48.78.17:16666/",
"--api-url",
"http://74.48.78.17:16667/",
"--expected-commit",
fixtureCommit
]),
writeReport: false
}, {
repoRoot: "/fixture-rpt004",
fsJson: fixtureArtifactJson(),
now: () => "2026-05-23T00:00:00.000Z",
httpGetJson: async (url) => ({ ok: true, status: 200, json: healthPayload(url) }),
httpGetText: async () => ({
ok: true,
status: 200,
contentType: "text/html; charset=utf-8",
body: workbenchHtml()
}),
layoutRunner: async () => ({ status: "pass", summary: "layout ok", artifacts: { screenshots: [] } }),
codeAgentRunner: async ({ turn, conversationId, sessionId }) => ({
ok: true,
status: 200,
json: codexSessionPayload({
conversationId,
sessionId,
toolName: turn.kind === "skills" ? "skills.discover" : "pwd",
turn: turn.kind === "pwd" ? 1 : turn.kind === "skills" ? 2 : 3,
reused: turn.kind !== "pwd",
skills: turn.kind === "skills" ? {
status: "ready",
items: [{
name: "hwlab-agent-runtime",
source: "/workspace/hwlab/skills/hwlab-agent-runtime/SKILL.md"
}],
count: 1
} : undefined,
reply: turn.kind === "context-pwd"
? "前文第一轮你问的是 pwd;当前工作目录仍是 /workspace/hwlab。"
: "当前工作目录是 /workspace/hwlab。"
})
}),
skillCliRunner: async () => ({
ok: true,
status: 200,
json: skillCliPayload()
})
});
assert.equal(report.status, "pass");
assert.equal(report.liveMutationUsed, false);
assert.equal(report.safety.liveMutationUsed, false);
assert.equal(report.commanderSummary.allowedLiveMutationUsed, false);
assert.equal(report.commanderSummary.sessionResult.status, "pass");
assert.equal(report.commanderSummary.skillCliResult.status, "pass");
assert.equal(report.commanderSummary.m3RouteResult.route, "/v1/m3/io");
assert.equal(report.commanderSummary.m3RouteResult.method, "POST");
assert.equal(report.commanderSummary.m3RouteResult.accepted, true);
assert.equal(report.commanderSummary.m3RouteResult.controlReady, true);
assert.equal(report.commanderSummary.m3RouteResult.readback.value, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.visible, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.route, "/v1/m3/io");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.method, "POST");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.operationId, "op_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.traceId, "trc_rpt004_skill");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.auditId, "aud_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.evidenceId, "evd_rpt004_skill_m3");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.accepted, true);
assert.equal(report.commanderSummary.exactRouteEvidenceContract.status, "completed");
assert.equal(report.commanderSummary.exactRouteEvidenceContract.readback.value, true);
assert.equal(report.commanderSummary.evidenceIds.evidenceId, "evd_rpt004_skill_m3");
assert.equal(report.dimensions.skillCliM3Route.status, "pass");
assert.equal(report.dimensions.m3TrustedLoop.observations.skippedWriteLoop, true);
assert.equal(report.dimensions.durableEvidence.observations.notApplicableInDefaultReadOnlyMode, true);
});
test("RPT-004 live report emits NOT_CURRENT and skips downstream checks", async () => {
const report = await buildRpt004Report({
...parseArgs([
@@ -530,12 +397,12 @@ function codexSessionPayload({
traceId: `trc_rpt004_${turn}`,
provider: "codex-stdio",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-mcp-stdio-runner",
backend: "hwlab-cloud-api/codex-app-server-stdio",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
session: {
sessionId,
status: "idle",
@@ -555,20 +422,20 @@ function codexSessionPayload({
status: "idle"
},
runner: {
kind: "codex-mcp-stdio-runner",
kind: "codex-app-server-stdio-runner",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
session: "codex-mcp-stdio-long-lived",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
session: "codex-app-server-stdio-long-lived",
sessionMode: "codex-app-server-stdio-long-lived",
implementationType: "repo-owned-codex-app-server-stdio-session",
codexStdio: true,
writeCapable: true,
durableSession: true
},
runnerTrace: {
traceId: `trc_rpt004_${turn}`,
runnerKind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
runnerKind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
sessionId,
turn,
sessionReused: reused,
@@ -620,128 +487,6 @@ function openAiFallbackPayload({ reply = "text fallback" } = {}) {
}
};
}
function skillCliPayload({
route = "/v1/m3/io",
method = "POST",
accepted = true,
status = "succeeded",
operationId = "op_rpt004_skill_m3",
auditId = "aud_rpt004_skill_m3",
evidenceId = "evd_rpt004_skill_m3",
blocker = null,
directGatewayCalls = false,
directBoxCalls = false,
directPatchPanelCalls = false
} = {}) {
return {
status: "completed",
conversationId: "cnv_rpt004_skill",
sessionId: "ses_rpt004_skill",
messageId: "msg_rpt004_skill",
traceId: "trc_rpt004_skill",
provider: "hwlab-skill-cli",
model: "controlled-m3-io",
backend: "hwlab-cloud-api/hwlab-agent-runtime-skill-cli",
workspace: "/workspace/hwlab",
sandbox: "hwlab-api-route-only",
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
sessionMode: "controlled-m3-io-skill-cli",
implementationType: "skill-cli-hwlab-api-adapter",
runner: {
kind: "hwlab-m3-io-skill-cli",
safety: {
directGatewayCallsAllowed: false,
directBoxSimuCallsAllowed: false,
directPatchPanelCallsAllowed: false
}
},
runnerTrace: {
traceId: "trc_rpt004_skill",
runnerKind: "hwlab-m3-io-skill-cli",
method,
route,
fallbackUsed: false,
directGatewayCalls,
directBoxCalls,
directPatchPanelCalls
},
providerTrace: {
runnerKind: "hwlab-m3-io-skill-cli",
route,
method,
traceId: "trc_rpt004_skill",
operationId,
auditId,
evidenceId,
accepted,
controlReady: accepted,
fallbackUsed: false,
readback: {
status: "succeeded",
value: true,
resourceId: "res_boxsimu_2",
port: "DI1"
}
},
toolCalls: [{
name: "hwlab-agent-runtime.m3-io",
status: accepted ? "completed" : "blocked",
type: "skill-cli",
route,
method,
accepted,
controlReady: accepted,
capabilityLevel: accepted ? "hwlab-api-control-ready" : "hwlab-api-control-blocked",
operationId,
traceId: "trc_rpt004_skill",
auditId,
evidenceId,
audit: {
auditId,
status
},
evidence: {
evidenceId,
status: accepted ? "blocked" : "blocked",
sourceKind: accepted ? "BLOCKED" : "BLOCKED",
blocker: blocker?.code ?? "runtime_durable_not_green",
writeStatus: "written_non_durable"
},
durable: {
status: "degraded",
blocker: blocker?.code ?? "runtime_durable_not_green"
},
readback: {
status: "succeeded",
value: true,
resourceId: "res_boxsimu_2",
port: "DI1"
},
blocker,
blockers: blocker ? [blocker] : [{
code: "runtime_durable_not_green",
message: "durable trust is not green"
}],
directGatewayCalls,
directBoxCalls,
directPatchPanelCalls,
fallbackUsed: false
}],
skills: {
status: "used",
items: [{
name: "hwlab-agent-runtime.m3-io",
route
}],
blockers: blocker ? [blocker] : []
},
reply: {
content: "M3 IO Skill CLI result"
}
};
}
function healthPayload(url, { revision = fixtureCommit } = {}) {
const isWeb = String(url).includes(":16666");
return {
@@ -1,493 +0,0 @@
import assert from "node:assert/strict";
import test from "node:test";
import {
HWLAB_M3_IO_API_BASE_URL_ENV,
HWLAB_M3_IO_CAPABILITY_LEVELS,
HWLAB_M3_IO_DEV_SERVICE_BASE_URL,
HWLAB_M3_IO_API_ROUTE,
HWLAB_M3_STATUS_API_ROUTE,
runM3IoSkillCommand,
validateCloudApiTarget
} from "./src/m3-io-skill-client.mjs";
test("M3 Skill CLI posts only to HWLAB API /v1/m3/io for DO1 writes", async () => {
const calls = [];
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"do.write",
"--value",
"false",
"--api-base-url",
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"--approved",
"--trace-id",
"trc_skill_cli_write_false",
"--request-id",
"req_skill_cli_write_false"
],
{
now: () => "2026-05-23T00:08:00.000Z",
requestJson: async (url, request) => {
calls.push({ url, request });
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`);
assert.equal(request.method, "POST");
assert.equal(request.body.action, "do.write");
assert.equal(request.body.resourceId, "res_boxsimu_1");
assert.equal(request.body.boxId, "boxsimu_1");
assert.equal(request.body.port, "DO1");
assert.equal(request.body.value, false);
assert.equal(request.body.approval.approved, true);
assert.equal(request.body.approval.policy, "hwlab-api-control-with-approval");
assert.equal(request.body.source, "hwlab-agent-runtime.m3-io");
return {
ok: true,
status: 200,
body: {
status: "succeeded",
accepted: true,
traceId: "trc_skill_cli_write_false",
operationId: "op_m3_do_write_cli",
auditId: "aud_m3_do_write_cli_succeeded",
evidenceId: "evd_m3_do_write_cli_succeeded",
auditState: {
status: "written_non_durable"
},
evidenceState: {
status: "blocked",
sourceKind: "BLOCKED",
blocker: "runtime_durable_not_green",
writeStatus: "written_non_durable"
},
durableStatus: {
status: "degraded",
durable: false,
blocker: "runtime_durable_not_green"
},
result: {
value: false,
targetReadback: {
value: false
}
},
controlPath: {
cloudApi: true,
gatewaySimu: true,
boxSimu: true,
patchPanel: true,
frontendBypass: false
}
}
};
}
}
);
assert.equal(result.ok, true);
assert.equal(result.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(result.method, "POST");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
assert.equal(result.controlReady, true);
assert.equal(result.readiness.status, "ready");
assert.equal(result.accepted, true);
assert.equal(result.status, "succeeded");
assert.equal(result.operationId, "op_m3_do_write_cli");
assert.equal(result.approval.approved, true);
assert.equal(result.approval.policy, "hwlab-api-control-with-approval");
assert.equal(result.auditId, "aud_m3_do_write_cli_succeeded");
assert.equal(result.evidenceId, "evd_m3_do_write_cli_succeeded");
assert.equal(result.audit.auditId, "aud_m3_do_write_cli_succeeded");
assert.equal(result.evidence.evidenceId, "evd_m3_do_write_cli_succeeded");
assert.equal(result.readback.value, false);
assert.equal(result.durable.blocker, "runtime_durable_not_green");
assert.equal(result.trustBlocker.code, "runtime_durable_not_green");
assert.equal(result.blockers.some((blocker) => blocker.code === "runtime_durable_not_green"), true);
assert.equal(result.safety.directGatewayCalls, false);
assert.equal(result.safety.directBoxCalls, false);
assert.equal(result.safety.directPatchPanelCalls, false);
assert.equal(result.safety.fallbackUsed, false);
assert.equal(calls.length, 1);
});
test("M3 Skill CLI rejects direct gateway, box, and patch-panel targets before request", async () => {
for (const url of [
"http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101",
"http://hwlab-box-simu-1.hwlab-dev.svc.cluster.local:7201",
"http://hwlab-patch-panel.hwlab-dev.svc.cluster.local:7301"
]) {
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"di.read",
"--api-base-url",
url,
"--trace-id",
"trc_skill_cli_direct_blocked"
],
{
requestJson: async () => {
throw new Error("direct hardware target must be blocked before network call");
}
}
);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.method, "POST");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(result.controlReady, false);
assert.equal(result.readiness.status, "blocked");
assert.equal(result.accepted, false);
assert.equal(result.blocker.code, "direct_hardware_target_blocked");
assert.equal(result.capabilityBlocker.code, "direct_hardware_target_blocked");
assert.equal(result.operationId, null);
assert.equal(result.safety.directGatewayCalls, false);
assert.equal(result.safety.directBoxCalls, false);
assert.equal(result.safety.directPatchPanelCalls, false);
}
});
test("M3 Skill CLI validates exact API route contract", () => {
const invalid = validateCloudApiTarget({
url: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/rpc/m3.io.di.read"
});
assert.equal(invalid.code, "invalid_hwlab_api_route");
});
test("M3 Skill CLI enforces DEV/MVP HWLAB API base allowlist", async () => {
const result = await runM3IoSkillCommand(
[
"m3",
"status",
"--api-base-url",
"https://example.com",
"--trace-id",
"trc_skill_cli_unallowlisted"
],
{
requestJson: async () => {
throw new Error("unallowlisted API base must be blocked before network call");
}
}
);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.route, HWLAB_M3_STATUS_API_ROUTE);
assert.equal(result.method, "GET");
assert.equal(result.blocker.code, "hwlab_api_target_not_allowlisted");
assert.equal(result.hwlabApi.redactedUrl, "https://example.com/v1/m3/status");
});
test("M3 Skill CLI requires explicit approval for DO writes before request", async () => {
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"do.write",
"--value",
"true",
"--api-base-url",
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"--trace-id",
"trc_skill_cli_write_unapproved"
],
{
requestJson: async () => {
throw new Error("unapproved DO write must be blocked before network call");
}
}
);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.blocker.code, "approval_required");
assert.equal(result.approval.required, true);
assert.equal(result.approval.approved, false);
assert.equal(result.approval.policy, "hwlab-api-control-with-approval");
assert.equal(result.accepted, false);
assert.equal(result.operationId, null);
assert.equal(result.safety.directGatewayCalls, false);
assert.equal(result.safety.fallbackUsed, false);
});
test("M3 Skill CLI reads status only through HWLAB API /v1/m3/status", async () => {
const calls = [];
const result = await runM3IoSkillCommand(
[
"m3",
"status",
"--api-base-url",
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"--trace-id",
"trc_skill_cli_status"
],
{
now: () => "2026-05-23T00:08:30.000Z",
requestJson: async (url, request) => {
calls.push({ url, request });
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_STATUS_API_ROUTE}`);
assert.equal(request.method, "GET");
return {
ok: true,
status: 200,
body: {
status: "live",
sourceKind: "DEV-LIVE",
traceId: "trc_skill_cli_status",
gateways: [
{ id: "gwsimu_1", online: true },
{ id: "gwsimu_2", online: true }
],
boxes: [
{
id: "boxsimu_2",
resourceId: "res_boxsimu_2",
online: true,
ports: {
DI1: {
value: true
}
}
}
],
patchPanel: {
serviceId: "hwlab-patch-panel",
observable: true,
connectionActive: true
},
trust: {
operationId: "op_m3_status_latest",
traceId: "trc_skill_cli_status",
auditId: "aud_m3_status_latest",
evidenceId: "evd_m3_status_latest",
durableStatus: "green",
blocker: null,
readStatus: {
audit: "read",
evidence: "read"
},
runtime: {
durable: true
}
}
}
};
}
}
);
assert.equal(result.ok, true);
assert.equal(result.route, HWLAB_M3_STATUS_API_ROUTE);
assert.equal(result.method, "GET");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.readonly);
assert.equal(result.readonly, true);
assert.equal(result.readback.value, true);
assert.equal(result.operationId, "op_m3_status_latest");
assert.equal(result.auditId, "aud_m3_status_latest");
assert.equal(result.evidenceId, "evd_m3_status_latest");
assert.equal(result.safety.allowedRoute, HWLAB_M3_STATUS_API_ROUTE);
assert.equal(result.safety.directPatchPanelCalls, false);
assert.equal(calls.length, 1);
});
test("M3 Skill CLI reports status HWLAB API failures on /v1/m3/status", async () => {
const result = await runM3IoSkillCommand(
[
"m3",
"status",
"--api-base-url",
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"--trace-id",
"trc_skill_cli_status_unavailable"
],
{
requestJson: async (url, request) => {
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_STATUS_API_ROUTE}`);
assert.equal(request.method, "GET");
return {
ok: false,
status: 503,
body: null,
error: "service unavailable"
};
}
}
);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.route, HWLAB_M3_STATUS_API_ROUTE);
assert.equal(result.method, "GET");
assert.equal(result.blocker.code, "hwlab_api_unavailable");
assert.equal(result.blocker.route, HWLAB_M3_STATUS_API_ROUTE);
assert.equal(result.error.code, "hwlab_api_unavailable");
assert.equal(result.error.route, HWLAB_M3_STATUS_API_ROUTE);
assert.match(result.error.userMessage, /状态未读取/u);
});
test("M3 Skill CLI reports missing API base as safe structured config blocker", async () => {
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"di.read",
"--trace-id",
"trc_skill_cli_api_base_missing"
],
{
env: {
PATH: process.env.PATH
},
requestJson: async () => {
throw new Error("missing API base must be blocked before request");
}
}
);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.method, "POST");
assert.equal(result.accepted, false);
assert.equal(result.blocker.code, "skill_cli_api_base_missing");
assert.equal(result.blocker.layer, "skill-cli-config");
assert.equal(result.blocker.retryable, false);
assert.equal(result.error.code, "skill_cli_api_base_missing");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(result.accepted, false);
assert.equal(result.operationId, null);
assert.equal(result.audit.status, "not_written");
assert.equal(result.hwlabApi.source, "missing-config");
assert.equal(result.hwlabApi.redactedUrl, null);
assert.deepEqual(result.error.missingConfig, [
"HWLAB_CODE_AGENT_HWLAB_API_BASE_URL",
"HWLAB_API_BASE_URL",
"HWLAB_CLOUD_API_BASE_URL",
"contract:hwlab-agent-runtime.m3-io.apiBaseUrl"
]);
assert.equal(JSON.stringify(result).includes("://"), false);
});
test("M3 Skill CLI reports HWLAB API unavailable as retryable structured blocker", async () => {
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"di.read",
"--api-base-url",
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"--trace-id",
"trc_skill_cli_hwlab_api_unavailable"
],
{
requestJson: async () => ({
ok: false,
status: 503,
body: null,
error: "service unavailable"
})
}
);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.blocker.code, "hwlab_api_unavailable");
assert.equal(result.blocker.layer, "hwlab-api");
assert.equal(result.blocker.retryable, true);
assert.equal(result.error.code, "hwlab_api_unavailable");
assert.equal(result.error.retryable, true);
assert.equal(result.error.route, HWLAB_M3_IO_API_ROUTE);
assert.equal(result.error.toolName, "hwlab-agent-runtime.m3-io");
});
test("M3 Skill CLI preserves slow structured blocker and timeout budget", async () => {
const startedAt = Date.now();
const result = await runM3IoSkillCommand(
[
"m3",
"io",
"--action",
"di.read",
"--api-base-url",
"http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
"--approved",
"--trace-id",
"trc_skill_cli_slow_blocker",
"--request-id",
"req_skill_cli_slow_blocker",
"--timeout-ms",
"30000"
],
{
now: () => "2026-05-23T00:09:00.000Z",
requestJson: async (url, request) => {
assert.equal(url, `http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667${HWLAB_M3_IO_API_ROUTE}`);
assert.equal(request.timeoutMs, 30000);
await delay(4600);
return {
ok: true,
status: 200,
body: {
status: "blocked",
accepted: false,
traceId: "trc_skill_cli_slow_blocker",
operationId: "op_skill_cli_slow_blocker",
auditId: "aud_skill_cli_slow_blocker_failed",
evidenceId: "evd_skill_cli_slow_blocker_failed",
blocker: {
code: "runtime_durable_not_green",
message: "runtime durable evidence is not green",
zh: "runtime durable evidence 尚未为 green"
},
blockerClassification: {
category: "runtime_durability"
},
evidenceState: {
status: "blocked",
sourceKind: "BLOCKED",
blocker: "runtime_durable_not_green",
writeStatus: "not_written"
},
durableStatus: {
status: "degraded",
durable: false,
blocker: "runtime_durable_not_green"
},
controlPath: {
cloudApi: true,
frontendBypass: false
}
}
};
}
}
);
assert.equal(Date.now() - startedAt >= 4500, true);
assert.equal(result.ok, false);
assert.equal(result.status, "blocked");
assert.equal(result.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
assert.equal(result.traceId, "trc_skill_cli_slow_blocker");
assert.equal(result.requestId, "req_skill_cli_slow_blocker");
assert.equal(result.operationId, "op_skill_cli_slow_blocker");
assert.equal(result.blocker.code, "runtime_durable_not_green");
assert.equal(result.blocker.category, "runtime_durability");
assert.equal(result.capabilityBlocker.code, "runtime_durable_not_green");
assert.equal(result.trustBlocker.code, "runtime_durable_not_green");
assert.equal(result.blockers.some((blocker) => blocker.code === "runtime_durable_not_green"), true);
assert.equal(result.audit.auditId, "aud_skill_cli_slow_blocker_failed");
assert.equal(result.evidence.evidenceId, "evd_skill_cli_slow_blocker_failed");
assert.equal(result.durable.blocker, "runtime_durable_not_green");
assert.equal(result.safety.fallbackUsed, false);
assert.equal(result.safety.directGatewayCalls, false);
});
function delay(ms) {
return new Promise((resolve) => setTimeout(resolve, ms));
}
+1 -4
View File
@@ -3675,10 +3675,7 @@ function agentFailurePresentation(error, { result = null, traceId = null } = {})
"codex_stdio_failed",
"codex_stdio_protocol_blocked",
"codex_stdio_empty_response",
"codex_stdio_command_probe_failed",
"external_network_blocked",
"network_tool_unavailable",
"network_timeout"
"codex_stdio_command_probe_failed"
].includes(code)) {
return {
category: "runner_blocked",
-33
View File
@@ -5,9 +5,6 @@ const CODEX_APP_SERVER_RUNNER = "codex-app-server-stdio-runner";
const CODEX_APP_SERVER_SESSION_MODE = "codex-app-server-stdio-long-lived";
const CODEX_APP_SERVER_IMPLEMENTATION = "repo-owned-codex-app-server-stdio-session";
const CODEX_APP_SERVER_PROTOCOL = "codex-app-server-jsonrpc-stdio";
const CODEX_MCP_RUNNER = "codex-mcp-stdio-runner";
const CODEX_MCP_SESSION_MODE = "codex-mcp-stdio-long-lived";
const CODEX_MCP_IMPLEMENTATION = "repo-owned-codex-mcp-stdio-session";
const MISSING_FIELD_VALUE = "字段缺失:证据不足";
const REQUIRED_ATTRIBUTION_KEYS = Object.freeze([
"provider",
@@ -108,17 +105,6 @@ export function classifyCodeAgentFacts(message) {
};
}
if (isCodexMcpPath({ runnerKind, sessionMode, implementationType, protocol: runtimePath?.protocol })) {
return {
kind: "codex-mcp-or-other-runner",
statusLabel: "MCP/其他 runner:非当前完整 Code Agent",
tone: "source",
summary: "这是 Codex stdio/MCP 或其他 runner 形态;当前完整 Code Agent 口径要求 repo-owned codex app-server --listen stdio:// 和 providerTrace 终态证据。",
fullCodeAgent: false,
codexSessionGatePass: false
};
}
if (runnerKind === CODEX_ONE_SHOT_RUNNER || sessionMode === "ephemeral-one-shot" || implementationType === "codex-cli-one-shot-ephemeral") {
return {
kind: "stateless-one-shot",
@@ -239,7 +225,6 @@ export function codeAgentRuntimePathFromMessage(message) {
.filter(([, value]) => !nullableText(value))
.map(([key]) => key);
const isAppServer = isCodexAppServerPath({ provider, runnerKind, protocol, implementationType, command });
const isMcp = isCodexMcpPath({ runnerKind, sessionMode: message.sessionMode, implementationType, protocol });
const isFallback = provider === "openai-responses" || runnerKind === TEXT_FALLBACK_RUNNER || message.capabilityLevel === "text-chat-only";
const isReadonly = provider === "codex-readonly-runner" || runnerKind === "hwlab-readonly-runner" || message.sessionMode === READONLY_SESSION_MODE;
const terminalStatusOk = terminalStatus === "completed" || terminalStatus === "succeeded";
@@ -272,11 +257,6 @@ export function codeAgentRuntimePathFromMessage(message) {
label = "DEGRADEDOpenAI text fallback";
summary = "这是 OpenAI text fallback/text-chat-only;没有 workspace runner、Codex app-server session 或工具控制能力。";
tone = blocked ? "blocked" : "warn";
} else if (isMcp) {
kind = "mcp-or-other-runner";
label = "DEGRADEDMCP/其他 runner";
summary = "这是 MCP 或其他 runner 形态,不等同于 repo-owned Codex app-server stdio。";
tone = blocked ? "blocked" : "warn";
} else if (isReadonly) {
kind = "readonly-runner";
label = "DEGRADEDread-only/source runner";
@@ -386,7 +366,6 @@ function messageAttributionLabel(message, classification) {
if (classification.kind === "text-chat-only") return "OpenAI fallback:只是文本回答";
if (classification.kind === READONLY_SESSION_MODE) return "read-only-session-tools:只读长会话";
if (classification.kind === "codex-stdio-long-lived") return "真实 runnerCodex app-server stdio 长会话";
if (classification.kind === "codex-mcp-or-other-runner") return "MCP/其他 runner:非当前完整 Code Agent";
if (classification.kind === "stateless-one-shot") return "一次性 runner:非长会话";
if (classification.kind === "codex-stdio-provider-trace-missing") return "DEGRADEDproviderTrace 缺失";
if (classification.kind === "codex-stdio-blocked") return "Codex stdio 受阻:当前不能执行";
@@ -426,15 +405,12 @@ function localizedAttributionValue(key, value, message) {
const normalized = String(value ?? "").trim();
if (key === "provider" && normalized === "openai-responses") return `${normalized} / OpenAI fallback:只是文本回答`;
if (key === "runnerKind" && normalized === CODEX_APP_SERVER_RUNNER) return `${normalized} / repo-owned Codex app-server stdio`;
if (key === "runnerKind" && normalized === CODEX_MCP_RUNNER) return `${normalized} / MCP/其他 runner`;
if (key === "runnerKind" && normalized === TEXT_FALLBACK_RUNNER) return `${normalized} / 不是 runner 控制`;
if (key === "runnerKind" && normalized === "hwlab-readonly-runner") return `${normalized} / 只读 runner`;
if (key === "protocol" && normalized === CODEX_APP_SERVER_PROTOCOL) return `${normalized} / Responses wire API`;
if (key === "implementationType" && normalized === CODEX_APP_SERVER_IMPLEMENTATION) return `${normalized} / repo-owned app-server stdio session`;
if (key === "implementationType" && normalized === CODEX_MCP_IMPLEMENTATION) return `${normalized} / MCP stdio session`;
if (key === "sessionMode" && normalized === READONLY_SESSION_MODE) return `${normalized} / 只读 session registry`;
if (key === "sessionMode" && normalized === CODEX_APP_SERVER_SESSION_MODE) return `${normalized} / app-server stdio 长会话`;
if (key === "sessionMode" && normalized === CODEX_MCP_SESSION_MODE) return `${normalized} / MCP stdio 长会话`;
if (key === "capabilityLevel" && normalized === "text-chat-only") return "text-chat-only / 只是文本回答";
if (key === "capabilityLevel" && normalized === "read-only-session-tools") return `${normalized} / 只读工具`;
if (key === "toolCalls" && normalized === "none") {
@@ -511,15 +487,6 @@ function isCodexAppServerPath({ provider, runnerKind, protocol, implementationTy
);
}
function isCodexMcpPath({ runnerKind, sessionMode, implementationType, protocol }) {
return (
runnerKind === CODEX_MCP_RUNNER ||
sessionMode === CODEX_MCP_SESSION_MODE ||
implementationType === CODEX_MCP_IMPLEMENTATION ||
/mcp/iu.test(String(protocol ?? ""))
);
}
function parseToolStdout(stdout) {
if (typeof stdout !== "string" || !stdout.trim()) return null;
const text = stdout.trim();
@@ -130,43 +130,6 @@ test("long-lived Codex app-server stdio session fields are shown as full Code Ag
assert.match(runtimePath.summary, /Responses wire API/u);
});
test("legacy MCP-shaped Codex stdio is degraded instead of full app-server completion", () => {
const message = {
status: "completed",
provider: "codex-stdio",
model: "gpt-5.5",
backend: "hwlab-cloud-api/codex-mcp-stdio",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
runner: {
kind: "codex-mcp-stdio-runner",
codexStdio: true,
writeCapable: true
},
longLivedSessionGate: {
status: "pass",
pass: true
},
providerTrace: {
transport: "stdio",
protocol: "mcp-jsonrpc-stdio",
command: "codex mcp-server",
terminalStatus: "completed"
},
traceId: "trc_mcp"
};
const facts = codeAgentFactsFromMessage(message);
const runtimePath = codeAgentRuntimePathFromMessage({ role: "agent", ...message });
assert.equal(facts.kind, "codex-mcp-or-other-runner");
assert.equal(facts.fullCodeAgent, false);
assert.equal(runtimePath.kind, "mcp-or-other-runner");
assert.equal(runtimePath.label, "DEGRADEDMCP/其他 runner");
});
test("Codex app-server without providerTrace is degraded with structured missing fields", () => {
const message = {
role: "agent",
@@ -6,8 +6,8 @@ export const CODE_AGENT_M3_TRUSTED_ROUTE = Object.freeze({
toResourceId: "res_boxsimu_2",
toPort: "DI1"
});
export const CODE_AGENT_M3_SKILL_PROVIDER = "hwlab-skill-cli";
export const CODE_AGENT_M3_SKILL_RUNNER_KIND = "hwlab-m3-io-skill-cli";
export const CODE_AGENT_M3_SKILL_PROVIDER = "legacy-skill-cli";
export const CODE_AGENT_M3_SKILL_RUNNER_KIND = "legacy-m3-io-skill-cli";
export const CODE_AGENT_M3_SKILL_NAME = "hwlab-agent-runtime.m3-io";
export const CODE_AGENT_M3_PATH_LABEL = "Code Agent -> Skill CLI -> HWLAB API";
@@ -71,37 +71,6 @@ test("summarizes long-lived Code Agent as green with current deployment revision
assert.equal(Object.keys(summary.fields).includes("sourceMain"), false);
});
test("does not mark legacy MCP-shaped Codex stdio as app-server long-lived session", () => {
const summary = classifyCodeAgentStatusSummary({
latestMessage: {
status: "completed",
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-mcp-stdio",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-mcp-stdio-long-lived",
implementationType: "repo-owned-codex-mcp-stdio-session",
runner: {
kind: "codex-mcp-stdio-runner",
codexStdio: true
},
longLivedSessionGate: {
status: "pass",
pass: true
},
providerTrace: {
protocol: "mcp-jsonrpc-stdio",
command: "codex mcp-server",
terminalStatus: "completed"
},
traceId: "trc_mcp"
}
});
assert.notEqual(summary.kind, "long-lived-session");
assert.notEqual(summary.tone, "ok");
assert.equal(summary.runtimePathLabel, "DEGRADEDMCP/其他 runner");
});
test("summarizes Codex app-server stdio thread as reusable long-lived session", () => {
const summary = classifyCodeAgentStatusSummary({
latestMessage: {
@@ -150,34 +119,6 @@ test("summarizes Codex app-server stdio thread as reusable long-lived session",
assert.equal(summary.fields.threadId, "thread_app_server");
});
test("does not mark Codex stdio-shaped fields green without runner and gate evidence", () => {
const summary = classifyCodeAgentStatusSummary({
availability: {
status: "partial",
ready: true,
provider: "openai-responses",
mode: "openai",
backend: "hwlab-cloud-api/openai-responses",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-mcp-stdio-long-lived",
runner: {
kind: "openai-responses-fallback",
codexStdio: false,
writeCapable: false
},
longLivedSessionGate: {
status: "blocked",
pass: false,
blockers: [{ code: "openai_responses_fallback_not_session" }]
}
}
});
assert.notEqual(summary.kind, "long-lived-session");
assert.notEqual(summary.tone, "ok");
assert.equal(summary.readinessBlockers.includes("openai_responses_fallback_not_session"), true);
});
test("maps degraded readonly session tools to warning, not green", () => {
const summary = classifyCodeAgentStatusSummary({
availability: {
+1 -1
View File
@@ -5,7 +5,7 @@
"type": "module",
"scripts": {
"m3-readonly": "node scripts/m3-readonly-contract.mjs",
"check": "node --test code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs code-agent-m3-evidence.test.mjs scripts/sidebar-resize.test.mjs scripts/trace-scroll.test.mjs && node scripts/check.mjs",
"check": "node --test code-agent-facts.test.mjs code-agent-status.test.mjs wiring-status.test.mjs scripts/sidebar-resize.test.mjs scripts/trace-scroll.test.mjs && node scripts/check.mjs",
"build": "node scripts/build.mjs",
"layout": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --static --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout.json",
"layout:build": "node ../../scripts/dev-cloud-workbench-layout-smoke.mjs --build --report /tmp/hwlab-dev-gate/dev-cloud-workbench-layout-build.json",
-9
View File
@@ -9,7 +9,6 @@ import {
runDevCloudWorkbenchMobileSmoke,
runDevCloudWorkbenchAuthFixtureSmoke,
runDevCloudWorkbenchQuickPromptsFixtureSmoke,
runDevCloudWorkbenchSlowBlockerFixtureSmoke,
runDevCloudWorkbenchStaticSmoke
} from "../../../scripts/src/dev-cloud-workbench-smoke-lib.mjs";
import { gateSummary } from "../gate-summary.mjs";
@@ -100,8 +99,6 @@ const mobileHelpSmokeCheck = mobileWorkbenchSmoke.checks.find((check) => check.i
const localAgentFixtureSmoke = await runDevCloudWorkbenchLocalAgentFixtureSmoke();
const localAgentNo4500Check = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-no-4500ms-permanent-failure");
const localAgentMobilePendingCheck = localAgentFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-mobile-pending-layout");
const slowBlockerFixtureSmoke = await runDevCloudWorkbenchSlowBlockerFixtureSmoke();
const slowBlockerCheck = slowBlockerFixtureSmoke.checks.find((check) => check.id === "local-agent-fixture-slow-structured-blocker");
const quickPromptsFixtureSmoke = await runDevCloudWorkbenchQuickPromptsFixtureSmoke();
const quickPromptsFillCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-fill-input");
const quickPromptsNoAutosendCheck = quickPromptsFixtureSmoke.checks.find((check) => check.id === "quick-prompts-write-no-autosend");
@@ -182,8 +179,6 @@ function runMinimalWebCheck() {
assert.equal(localAgentFixtureSmoke.devLive, false);
assert.equal(localAgentNo4500Check?.status, "pass", "local fixture must prove no 4500ms permanent failure");
assert.equal(localAgentMobilePendingCheck?.status, "pass", "local fixture must preserve mobile pending layout");
assert.equal(slowBlockerFixtureSmoke.status, "pass", JSON.stringify(slowBlockerFixtureSmoke.blockers, null, 2));
assert.equal(slowBlockerCheck?.status, "pass", "slow blocker fixture must stay actionable");
assert.equal(quickPromptsFixtureSmoke.status, "pass", JSON.stringify(quickPromptsFixtureSmoke.blockers, null, 2));
assert.equal(quickPromptsFillCheck?.status, "pass", "quick prompts must fill input");
assert.equal(quickPromptsNoAutosendCheck?.status, "pass", "write quick prompts must not auto-send");
@@ -236,10 +231,6 @@ assert.equal(localAgentFixtureSmoke.evidenceLevel, "SOURCE");
assert.equal(localAgentFixtureSmoke.devLive, false);
assert.equal(localAgentNo4500Check?.status, "pass", "local Code Agent fixture must prove the 4500ms browser timeout path is not permanent");
assert.equal(localAgentMobilePendingCheck?.status, "pass", "local Code Agent fixture must keep mobile pending state visible with trace/session context");
assert.equal(slowBlockerFixtureSmoke.status, "pass", JSON.stringify(slowBlockerFixtureSmoke.blockers, null, 2));
assert.equal(slowBlockerFixtureSmoke.evidenceLevel, "SOURCE");
assert.equal(slowBlockerFixtureSmoke.devLive, false);
assert.equal(slowBlockerCheck?.status, "pass", "slow structured blocker fixture must show Chinese reason, trace, retry input, and no 4500ms permanent failure");
assert.equal(quickPromptsFixtureSmoke.status, "pass", JSON.stringify(quickPromptsFixtureSmoke.blockers, null, 2));
assert.equal(quickPromptsFixtureSmoke.evidenceLevel, "SOURCE");
assert.equal(quickPromptsFixtureSmoke.devLive, false);
@@ -105,7 +105,7 @@ const codexStdioRest = Object.freeze({
status: "codex-stdio-feasible",
ready: true,
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-mcp-stdio",
backend: "hwlab-cloud-api/codex-app-server-stdio",
capabilityLevel: "long-lived-codex-stdio-session",
agentKind: "codex-stdio-feasible",
codexStdioFeasibility: {
@@ -824,14 +824,14 @@ test("classifies Codex stdio session facts as pass even when unrelated runtime d
status: "ready",
ready: true,
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-mcp-stdio",
backend: "hwlab-cloud-api/codex-app-server-stdio",
mode: "codex-stdio"
},
sessionRunner: {
status: "codex_stdio_ready",
ready: true,
kind: "codex-mcp-stdio-runner",
mode: "codex-mcp-stdio-long-lived",
kind: "codex-app-server-stdio-runner",
mode: "codex-app-server-stdio-long-lived",
capabilityLevel: "long-lived-codex-stdio-session",
longLivedSession: true,
durableSession: true,
@@ -855,25 +855,19 @@ test("classifies Codex stdio session facts as pass even when unrelated runtime d
status: "codex-stdio-feasible",
ready: true,
provider: "codex-stdio",
backend: "hwlab-cloud-api/codex-mcp-stdio",
backend: "hwlab-cloud-api/codex-app-server-stdio",
capabilityLevel: "long-lived-codex-stdio-session",
sessionMode: "codex-mcp-stdio-long-lived",
sessionMode: "codex-app-server-stdio-long-lived",
workspace: "/workspace/hwlab",
sandbox: "workspace-write",
runner: {
kind: "codex-mcp-stdio-runner",
sessionMode: "codex-mcp-stdio-long-lived",
kind: "codex-app-server-stdio-runner",
sessionMode: "codex-app-server-stdio-long-lived",
codexStdio: true,
writeCapable: true,
durableSession: true
},
runnerLimitations: [
"hardware-control-via-cloud-api-only",
"no-direct-gateway-link",
"no-direct-box-simu-link",
"no-direct-patch-panel-link",
"secret-values-redacted"
],
runnerLimitations: ["secret-values-redacted"],
longLivedSessionGate: {
status: "pass",
pass: true