merge main updates into G14
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -46,26 +46,26 @@ const codeAgentExternalNetworkPrompt = Object.freeze({
|
||||
const codeAgentQuickPromptFixtures = Object.freeze([
|
||||
{
|
||||
id: "pwd",
|
||||
label: "Skill CLI:pwd",
|
||||
prompt: "通过 Skill CLI 执行 pwd,列出当前工作目录。",
|
||||
label: "Codex:pwd",
|
||||
prompt: "交给 Codex 执行 pwd,列出当前工作目录。",
|
||||
writable: false
|
||||
},
|
||||
{
|
||||
id: "skills",
|
||||
label: "Skill CLI:列出 skill",
|
||||
prompt: "通过 Skill CLI 列出你能使用的所有 skill。",
|
||||
label: "Codex:列出 skill",
|
||||
prompt: "交给 Codex 列出你能使用的所有 skill。",
|
||||
writable: false
|
||||
},
|
||||
{
|
||||
id: "do1-true-di1",
|
||||
label: "HWLAB API:DO1=true 后读 DI1",
|
||||
prompt: "通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
|
||||
label: "Codex:DO1=true 后读 DI1",
|
||||
prompt: "交给 Codex 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
|
||||
writable: true
|
||||
},
|
||||
{
|
||||
id: "do1-false-di1",
|
||||
label: "HWLAB API:DO1=false 复核 DI1",
|
||||
prompt: "通过 HWLAB API / Skill CLI 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
|
||||
label: "Codex:DO1=false 复核 DI1",
|
||||
prompt: "交给 Codex 把 res_boxsimu_1 的 DO1 写成 false,并复核 res_boxsimu_2 的 DI1;我点击发送后才确认执行这次写入请求。",
|
||||
writable: true
|
||||
}
|
||||
]);
|
||||
@@ -326,10 +326,7 @@ const requiredCodeAgentEvidenceTerms = Object.freeze([
|
||||
"echo/mock/stub",
|
||||
"untrusted_completion",
|
||||
"SOURCE fixture",
|
||||
"Code Agent SOURCE 回复",
|
||||
"hwlab-skill-cli",
|
||||
"hwlab-m3-io-skill-cli",
|
||||
"message-m3-evidence"
|
||||
"Code Agent SOURCE 回复"
|
||||
]);
|
||||
|
||||
const blockedCodeAgentUiLabels = Object.freeze([
|
||||
@@ -562,11 +559,6 @@ function runStaticSmoke() {
|
||||
evidence: requiredTrustedRecordTerms
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "hardware-status-rich-structure", hardwareStatusStructureContract(files), "Right hardware status renders Gateway/Box, DI/DO/AI/AO/FREQ, patch-panel, and DO1 -> DI1 source-labeled link state.", {
|
||||
blocker: "contract_blocker",
|
||||
evidence: requiredHardwareStatusTerms
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "m3-control-user-flow", m3ControlUserFlowContract(files), "Right control panel exposes an operator-readable DO1 -> patch-panel -> DI1 action summary, current action, DO1 target, DI1 observation, trusted/durable state, and recent trace navigation.", {
|
||||
blocker: "contract_blocker",
|
||||
evidence: requiredM3ControlUserFlowTerms
|
||||
@@ -601,22 +593,22 @@ function runStaticSmoke() {
|
||||
]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "same-origin-readonly-boundary", hasSameOriginReadOnlyBoundary(files.app), "Workbench data access stays same-origin and JSON-RPC diagnostics stay read-only.", {
|
||||
blocker: "safety_blocker",
|
||||
evidence: ["/health/live", "/v1", "/v1/live-builds", "/v1/diagnostics/gate", "/v1/agent/chat", "/json-rpc", ...readOnlyRpcMethods]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "api-live-status-attribution", hasApiLiveStatusAttribution(files), "Default workbench status maps raw API health into pass, error, unverified, or read-only with concrete service/API attribution.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: ["API 正常", "API 错误", "等待验证", "只读模式", "hwlab-cloud-api /health/live", "/v1/agent/chat", "/v1/m3/io", "raw degraded kept as internalRawStatuses"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "workbench-live-surface-timeout-contract", hasWorkbenchLiveSurfaceTimeoutContract(files), "Workbench live status uses a dedicated live-surface timeout instead of the 4500ms light API budget.", {
|
||||
blocker: "runtime_blocker",
|
||||
evidence: ["DEFAULT_LIVE_SURFACE_TIMEOUT_MS=12000", "liveSurfaceFetch", "liveSurfaceRpc", "/v1/m3/status", "system.health"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-chat-primary-flow", hasCodeAgentChatContract(files), "Center Agent conversation sends to the controlled Code Agent chat endpoint and no longer uses 添加草稿 as the primary flow.", {
|
||||
blocker: "runtime_blocker",
|
||||
evidence: ["command-send", "agent-chat-status", "/v1/agent/chat", "conversationId/messageId/status/timestamps/error.message"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-quick-prompts-contract", hasCodeAgentQuickPromptContract(files), "Code Agent quick prompts fill the input with Skill CLI / HWLAB API examples while hardware-write prompts require explicit send confirmation.", {
|
||||
addCheck(checks, blockers, "code-agent-quick-prompts-contract", hasCodeAgentQuickPromptContract(files), "Code Agent quick prompts fill the input with Codex examples while hardware-write prompts require explicit send confirmation.", {
|
||||
blocker: "safety_blocker",
|
||||
evidence: codeAgentQuickPromptFixtures.flatMap((prompt) => [prompt.id, prompt.label, prompt.prompt])
|
||||
});
|
||||
@@ -626,9 +618,9 @@ function runStaticSmoke() {
|
||||
evidence: ["DEFAULT_API_TIMEOUT_MS=4500 for light probes", `DEFAULT_CODE_AGENT_TIMEOUT_MS=${codeAgentLongTimeoutMs}`, "sendAgentMessage passes timeoutMs"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels disclose full trace access and preserve open/scroll state while live trace updates.", {
|
||||
addCheck(checks, blockers, "code-agent-trace-replay-disclosure", hasCodeAgentTraceReplayDisclosure(files), "Trace replay panels auto-replay full trace, avoid compressed-window UI, and preserve open/scroll state while live trace updates.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: ["显示全部 N / 原始 N", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"]
|
||||
evidence: ["显示全部可读事件 / 完整 trace 回放中", "复制 JSON", "下载 trace", "traceDetailsOpen", "traceScrollPositions", "internal scroll for full trace"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-provider-readiness-visibility", hasCodeAgentReadinessVisibility(files), "Workbench shows provider/stdio blockers without exposing credential internals and only long-lived Codex stdio replies can become full Code Agent completion.", {
|
||||
@@ -636,14 +628,13 @@ function runStaticSmoke() {
|
||||
evidence: ["服务受阻 or legacy BLOCKED 凭证缺口", "provider_unavailable classifier", "completed -> dev-live guard", "default workspace hides credential internals"]
|
||||
});
|
||||
|
||||
addCheck(checks, blockers, "code-agent-status-summary", hasCodeAgentStatusSummaryContract(files), "Code Agent area has a compact collapsible status summary for fallback, one-shot, read-only session, skill CLI control, blockers, trace, and current deployed revision.", {
|
||||
addCheck(checks, blockers, "code-agent-status-summary", hasCodeAgentStatusSummaryContract(files), "Code Agent area has a compact collapsible status summary for Codex stdio, blockers, trace, and current deployed revision.", {
|
||||
blocker: "observability_blocker",
|
||||
evidence: [
|
||||
"code-agent-summary",
|
||||
"fallback-text-chat-only",
|
||||
"stateless-one-shot",
|
||||
"read-only-session-tools",
|
||||
"skill-cli-api-control",
|
||||
"当前部署 revision"
|
||||
]
|
||||
});
|
||||
@@ -1977,6 +1968,8 @@ function hasDefaultLoginEntry({ html, app, auth, styles }, artifactPublisher = "
|
||||
/DEFAULT_AUTH_PASSWORD\s*=\s*["']hwlab2026["']/u.test(auth) &&
|
||||
/HWLAB_CLOUD_WEB_CONFIG\?\.auth/u.test(auth) &&
|
||||
/AUTH_STORAGE_KEY\s*=\s*["']hwlab\.cloudWorkbench\.auth\.v1["']/u.test(auth) &&
|
||||
/const localSession = readLocalSession\(config\)/u.test(auth) &&
|
||||
/writeLocalSession\(config\)/u.test(functionBody(auth, "attemptLogin")) &&
|
||||
/账号或密码不正确,请重新输入。/u.test(auth) &&
|
||||
/ensureWorkbenchAuth/u.test(app) &&
|
||||
/await\s+ensureWorkbenchAuth/u.test(app) &&
|
||||
@@ -2498,14 +2491,14 @@ function hasCodeAgentQuickPromptContract({ html, app, styles }) {
|
||||
/我点击发送后才确认执行这次写入请求/u.test(tag)
|
||||
);
|
||||
});
|
||||
const promptCopyUsesApiBoundary = codeAgentQuickPromptFixtures.every((prompt) =>
|
||||
/通过 (?:Skill CLI|HWLAB API \/ Skill CLI)/u.test(prompt.prompt) &&
|
||||
!/(?:\bgateway\b|\bbox\b|\bpatch-panel\b|PROD|真实硬件直控)/iu.test(prompt.prompt)
|
||||
const promptCopyUsesCodexRoute = codeAgentQuickPromptFixtures.every((prompt) =>
|
||||
/交给 Codex/u.test(prompt.prompt) &&
|
||||
!/PROD/iu.test(prompt.prompt)
|
||||
);
|
||||
return (
|
||||
labels &&
|
||||
writeButtonsRequireExplicitSend &&
|
||||
promptCopyUsesApiBoundary &&
|
||||
promptCopyUsesCodexRoute &&
|
||||
/data-agent-quick-prompt/u.test(app) &&
|
||||
/function\s+fillAgentQuickPrompt\s*\(/u.test(app) &&
|
||||
/el\.commandInput\.value = prompt/u.test(app) &&
|
||||
@@ -2645,13 +2638,42 @@ function hasCodeAgentLongTimeoutContract(files) {
|
||||
);
|
||||
}
|
||||
|
||||
function hasWorkbenchLiveSurfaceTimeoutContract(files) {
|
||||
const app = files.app;
|
||||
return (
|
||||
/DEFAULT_API_TIMEOUT_MS\s*=\s*4500/u.test(app) &&
|
||||
/DEFAULT_LIVE_SURFACE_TIMEOUT_MS\s*=\s*12000/u.test(app) &&
|
||||
/LIVE_SURFACE_TIMEOUT_MS\s*=\s*resolveTimeoutMs\("liveSurfaceTimeoutMs"/u.test(app) &&
|
||||
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "liveSurfaceFetch")) &&
|
||||
/timeoutName:\s*`工作台实况 \$\{path\}`/u.test(functionBody(app, "liveSurfaceFetch")) &&
|
||||
/callRpc\(method,\s*params,\s*\{/u.test(functionBody(app, "liveSurfaceRpc")) &&
|
||||
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "liveSurfaceRpc")) &&
|
||||
/timeoutMs:\s*options\?\.timeoutMs \?\? API_TIMEOUT_MS/u.test(functionBody(app, "callRpc")) &&
|
||||
/liveSurfaceFetch\("\/health\/live"\)/u.test(functionBody(app, "loadLiveSurface")) &&
|
||||
/liveSurfaceFetch\("\/v1\/m3\/status"\)/u.test(functionBody(app, "loadLiveSurface")) &&
|
||||
/liveSurfaceRpc\("system\.health"\)/u.test(functionBody(app, "loadLiveSurface")) &&
|
||||
/timeoutMs:\s*LIVE_SURFACE_TIMEOUT_MS/u.test(functionBody(app, "loadGateDiagnostics")) &&
|
||||
/timeoutName:\s*"工作台 M3 状态刷新"/u.test(functionBody(app, "runM3IoAction"))
|
||||
);
|
||||
}
|
||||
|
||||
function hasCodeAgentTraceReplayDisclosure({ app, styles }) {
|
||||
const tracePanelBody = functionBody(app, "messageTracePanel");
|
||||
const traceToolbarBody = functionBody(app, "messageTraceToolbar");
|
||||
const renderConversationBody = functionBody(app, "renderConversation");
|
||||
return (
|
||||
/function\s+messageTraceCountText\s*\(/u.test(app) &&
|
||||
/显示全部\s+\$\{displayTotal\}\s+\/\s+原始\s+\$\{rawTotal\}/u.test(app) &&
|
||||
/显示全部可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) &&
|
||||
/完整 trace 回放中\s+\/\s+当前可读事件\s+\$\{readableTotal\}\s+\/\s+已载入原始\s+\$\{loadedTotal\}\s+\/\s+后端原始\s+\$\{rawTotal\}/u.test(app) &&
|
||||
!/压缩窗口\s+\$\{readableTotal\}/u.test(app) &&
|
||||
/CODE_AGENT_SESSION_STORAGE_KEY\s*=\s*"hwlab\.workbench\.codeAgentSession\.v1"/u.test(app) &&
|
||||
/function\s+restoreCodeAgentSessionState\s*\(/u.test(app) &&
|
||||
/function\s+persistCodeAgentSessionState\s*\(/u.test(app) &&
|
||||
/window\.localStorage\?\.setItem\(CODE_AGENT_SESSION_STORAGE_KEY/u.test(app) &&
|
||||
/window\.localStorage\?\.removeItem\(CODE_AGENT_SESSION_STORAGE_KEY/u.test(app) &&
|
||||
/refreshRestoredCodeAgentTraces/u.test(app) &&
|
||||
/function\s+maybeReplayFullTraceForMessage\s*\(/u.test(app) &&
|
||||
/function\s+replayFullTrace\s*\(/u.test(app) &&
|
||||
/function\s+renderTraceEventList\s*\(/u.test(app) &&
|
||||
/messageTraceToolbar\(message,\s*trace,\s*events,\s*rows\)/u.test(tracePanelBody) &&
|
||||
/list\.dataset\.traceMode\s*=\s*"all"/u.test(tracePanelBody) &&
|
||||
@@ -2690,7 +2712,6 @@ function hasCodeAgentStatusSummaryContract({ html, app, styles, codeAgentStatus
|
||||
/fallback-text-chat-only/u.test(codeAgentStatus) &&
|
||||
/stateless-one-shot/u.test(codeAgentStatus) &&
|
||||
/read-only-session-tools/u.test(codeAgentStatus) &&
|
||||
/skill-cli-api-control/u.test(codeAgentStatus) &&
|
||||
/currentDeploymentRevision/u.test(codeAgentStatus) &&
|
||||
/unsafeGreenForNonReady/u.test(codeAgentStatus) &&
|
||||
/provider_config_blocked/u.test(codeAgentStatus) &&
|
||||
@@ -4407,7 +4428,7 @@ async function inspectJourneyUi(page) {
|
||||
actions,
|
||||
eventCount: events.length,
|
||||
traceMode: panel.querySelector(".message-trace-events")?.dataset.traceMode ?? "",
|
||||
allTraceVisible: /显示全部\s+\d+\s*\/\s*原始\s+\d+/u.test(countText),
|
||||
allTraceVisible: /显示全部可读事件\s+\d+\s*\/\s*已载入原始\s+\d+\s*\/\s*后端原始\s+\d+/u.test(countText),
|
||||
copyJsonVisible: actions.includes("复制 JSON"),
|
||||
downloadTraceVisible: actions.includes("下载 trace"),
|
||||
fixedAllMode: panel.querySelector(".message-trace-events")?.dataset.traceMode === "all"
|
||||
@@ -4473,8 +4494,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) ||
|
||||
(/DEGRADED:MCP|MCP\/其他 runner/u.test(runtimePathText) && !/使用 Responses wire API|wire_api=responses|真实 runner:Codex 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
|
||||
@@ -4611,24 +4630,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 runDevCloudWorkbenchExternalNetworkFixtureSmoke(options = {}) {
|
||||
return runLocalAgentFixtureSmoke({
|
||||
mode: "local-agent-external-network-fixture-browser",
|
||||
responseDelayMs: options.responseDelayMs ?? localAgentFixtureDelayMs,
|
||||
agentFixtureMode: "external-network-blocker",
|
||||
expectTimeout: false
|
||||
});
|
||||
}
|
||||
|
||||
export async function runDevCloudWorkbenchSessionContinuityFixtureSmoke(options = {}) {
|
||||
let chromium;
|
||||
try {
|
||||
@@ -4888,7 +4889,7 @@ export async function runDevCloudWorkbenchQuickPromptsFixtureSmoke() {
|
||||
{
|
||||
id: "quick-prompts-copy-boundary",
|
||||
status: promptResults.every((result) => result.copyBoundaryOk) ? "pass" : "blocked",
|
||||
summary: "Quick prompt labels and filled text say HWLAB API / Skill CLI and avoid direct gateway/box/patch-panel or PROD claims.",
|
||||
summary: "Quick prompt labels and filled text say Codex and avoid PROD claims.",
|
||||
observations: promptResults.map(({ viewport, id, label, inputValue, copyBoundaryOk, forbiddenHits }) => ({
|
||||
viewport,
|
||||
id,
|
||||
@@ -4963,7 +4964,6 @@ async function runLocalAgentFixtureSmoke({
|
||||
mode,
|
||||
responseDelayMs = 0,
|
||||
timeoutConfigMs = null,
|
||||
agentFixtureMode = "success",
|
||||
expectTimeout = false
|
||||
} = {}) {
|
||||
let chromium;
|
||||
@@ -5003,7 +5003,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 });
|
||||
@@ -5014,6 +5014,9 @@ async function runLocalAgentFixtureSmoke({
|
||||
...(globalThis.HWLAB_CLOUD_WEB_CONFIG ?? {}),
|
||||
timeouts: {
|
||||
...(globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? {}),
|
||||
codeAgentSubmitTimeoutMsMinMs: 1,
|
||||
codeAgentSubmitTimeoutMs: timeoutMs,
|
||||
codeAgentTimeoutMsMinMs: 1,
|
||||
codeAgentTimeoutMs: timeoutMs
|
||||
}
|
||||
};
|
||||
@@ -5060,24 +5063,17 @@ async function runLocalAgentFixtureSmoke({
|
||||
{ timeout: 8000 }
|
||||
);
|
||||
} else {
|
||||
const prompts = agentFixtureMode === "slow-blocker"
|
||||
? [codeAgentE2ePrompts[1]]
|
||||
: agentFixtureMode === "external-network-blocker"
|
||||
? [codeAgentExternalNetworkPrompt]
|
||||
: 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,
|
||||
@@ -5097,32 +5093,7 @@ async function runLocalAgentFixtureSmoke({
|
||||
ui.timeoutActionPanelContained &&
|
||||
!ui.completedMessageVisible
|
||||
)
|
||||
: agentFixtureMode === "slow-blocker" || agentFixtureMode === "external-network-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 &&
|
||||
(agentFixtureMode === "external-network-blocker"
|
||||
? ["能力未开放", "Runner 受阻", "服务受阻"].includes(ui.agentChatStatus)
|
||||
: 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" &&
|
||||
@@ -5182,8 +5153,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" || agentFixtureMode === "external-network-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,
|
||||
@@ -5197,7 +5166,7 @@ async function runLocalAgentFixtureSmoke({
|
||||
}
|
||||
}
|
||||
];
|
||||
if (!expectTimeout && agentFixtureMode === "success") {
|
||||
if (!expectTimeout) {
|
||||
checks.push({
|
||||
id: "local-agent-fixture-no-4500ms-permanent-failure",
|
||||
status: promptResults.every((result) =>
|
||||
@@ -5233,22 +5202,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" || agentFixtureMode === "external-network-blocker")) {
|
||||
checks.push({
|
||||
id: agentFixtureMode === "external-network-blocker"
|
||||
? "local-agent-fixture-external-network-blocker"
|
||||
: "local-agent-fixture-slow-structured-blocker",
|
||||
status: pass ? "pass" : "blocked",
|
||||
summary: agentFixtureMode === "external-network-blocker"
|
||||
? "External network blocker for a GitHub prompt displays Runner 受阻 with network trace, keeps the user prompt, and does not restore main evidence/facts noise."
|
||||
: "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")
|
||||
@@ -5274,7 +5227,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,
|
||||
@@ -5323,6 +5276,9 @@ async function inspectLocalAgentPendingViewport(browser, url, { width, height, r
|
||||
...(globalThis.HWLAB_CLOUD_WEB_CONFIG ?? {}),
|
||||
timeouts: {
|
||||
...(globalThis.HWLAB_CLOUD_WEB_CONFIG?.timeouts ?? {}),
|
||||
codeAgentSubmitTimeoutMsMinMs: 1,
|
||||
codeAgentSubmitTimeoutMs: timeoutMs,
|
||||
codeAgentTimeoutMsMinMs: 1,
|
||||
codeAgentTimeoutMs: timeoutMs
|
||||
}
|
||||
};
|
||||
@@ -5729,7 +5685,7 @@ async function inspectQuickPromptsViewport(browser, url, viewport) {
|
||||
const button = document.querySelector(`[data-agent-quick-prompt="${id}"]`);
|
||||
const input = document.querySelector("#command-input");
|
||||
const text = `${button?.textContent ?? ""}\n${button?.getAttribute("title") ?? ""}\n${button?.dataset.promptText ?? ""}`;
|
||||
const forbiddenPattern = /\b(?:PROD|gateway|box|patch-panel|真实硬件直控)\b/iu;
|
||||
const forbiddenPattern = /\bPROD\b/iu;
|
||||
const forbiddenHits = [...new Set(text.match(forbiddenPattern) ?? [])];
|
||||
return {
|
||||
label: button?.textContent?.replace(/\s+/gu, " ").trim() ?? "",
|
||||
@@ -5739,7 +5695,7 @@ async function inspectQuickPromptsViewport(browser, url, viewport) {
|
||||
focused: document.activeElement === input,
|
||||
explicitSendRequired: button?.dataset.requiresExplicitSend === "true",
|
||||
copyBoundaryOk:
|
||||
/(?:HWLAB API|Skill CLI)/u.test(text) &&
|
||||
/Codex/u.test(text) &&
|
||||
!forbiddenPattern.test(text) &&
|
||||
(button?.dataset.requiresExplicitSend !== "true" || /只填充输入框|点击发送后才确认/u.test(text)),
|
||||
forbiddenHits
|
||||
@@ -5988,7 +5944,7 @@ function sessionContinuityFixturePayload({ body, traceId, conversationId, messag
|
||||
status: "idle"
|
||||
},
|
||||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||||
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
|
||||
runnerLimitations: ["secret-values-redacted"],
|
||||
codexStdioFeasibility: {
|
||||
ready: true,
|
||||
canStartLongLivedCodexStdio: true,
|
||||
@@ -6190,8 +6146,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",
|
||||
@@ -6890,275 +6846,6 @@ async function handleLocalAgentFixtureApi({ request, response, url, options = {}
|
||||
}));
|
||||
return true;
|
||||
}
|
||||
if (options.agentFixtureMode === "external-network-blocker") {
|
||||
const blocker = {
|
||||
code: "external_network_blocked",
|
||||
category: "capability_unavailable",
|
||||
layer: "network",
|
||||
userMessage: "当前 Code Agent 运行策略禁止外部网络访问;未访问外网,也不会伪造成功。",
|
||||
retryable: false,
|
||||
traceId,
|
||||
sessionId: conversationId,
|
||||
conversationId,
|
||||
stage: "policy",
|
||||
elapsedMs: options.agentDelayMs ?? 0,
|
||||
lastEvent: { seq: 4, traceId, type: "network", stage: "policy", status: "blocked", label: "network:blocked", errorCode: "external_network_blocked" }
|
||||
};
|
||||
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-external-network-blocker",
|
||||
projectId: stringOrFallback(body?.projectId, gateSummary.topology.projectId),
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
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: "external_network_blocked",
|
||||
category: "capability_unavailable",
|
||||
layer: "network",
|
||||
message: "SOURCE external network blocker: runtime policy disabled external network checks.",
|
||||
userMessage: blocker.userMessage,
|
||||
retryable: false,
|
||||
blocker
|
||||
},
|
||||
blocker,
|
||||
blockers: [blocker],
|
||||
toolCalls: [
|
||||
{
|
||||
name: "external.network.check",
|
||||
type: "network-check",
|
||||
status: "blocked",
|
||||
exitCode: 2,
|
||||
traceId,
|
||||
stderrSummary: "external_network_blocked",
|
||||
targetUrl: "https://github.com/",
|
||||
blocker
|
||||
}
|
||||
],
|
||||
skills: {
|
||||
status: "not_requested",
|
||||
items: [],
|
||||
count: 0,
|
||||
totalCount: 0,
|
||||
blockers: []
|
||||
},
|
||||
runnerTrace: {
|
||||
traceId,
|
||||
runnerKind: "codex-mcp-stdio-runner",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
sessionId: conversationId,
|
||||
sessionStatus: "idle",
|
||||
status: "blocked",
|
||||
elapsedMs: options.agentDelayMs ?? 0,
|
||||
waitingFor: "network-policy",
|
||||
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", toolName: "external.network.check" },
|
||||
{ seq: 3, traceId, type: "network", stage: "policy", status: "started", label: "network:started", toolName: "external.network.check" },
|
||||
blocker.lastEvent,
|
||||
{ seq: 5, traceId, type: "tool_call", stage: "tool_call", status: "blocked", label: "tool:external.network.check:blocked", toolName: "external.network.check", errorCode: "external_network_blocked" }
|
||||
],
|
||||
lastEvent: { seq: 5, traceId, type: "tool_call", stage: "tool_call", status: "blocked", label: "tool:external.network.check:blocked", toolName: "external.network.check", errorCode: "external_network_blocked" }
|
||||
},
|
||||
conversationFacts: {
|
||||
conversationId,
|
||||
sessionId: conversationId,
|
||||
sessionStatus: "idle",
|
||||
sessionMode: "codex-mcp-stdio-long-lived",
|
||||
capabilityLevel: "blocked",
|
||||
runnerKind: "codex-mcp-stdio-runner",
|
||||
workspace: "/workspace/hwlab",
|
||||
sandbox: "workspace-write",
|
||||
turnCount: 1,
|
||||
latestTraceId: traceId,
|
||||
traceIds: [traceId],
|
||||
latestSkills: null,
|
||||
recentToolCalls: [{ name: "external.network.check", status: "blocked" }],
|
||||
facts: [],
|
||||
valuesRedacted: true,
|
||||
secretMaterialStored: false
|
||||
},
|
||||
providerTrace: {
|
||||
source: "SOURCE-local-browser-fixture",
|
||||
sourceKind: "SOURCE",
|
||||
transport: "stdio+controlled-network",
|
||||
responseId: "rsp_source_fixture_external_network_blocker",
|
||||
redacted: true
|
||||
}
|
||||
});
|
||||
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",
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user