#!/usr/bin/env node import assert from "node:assert/strict"; import http from "node:http"; import https from "node:https"; import { handleCodeAgentChat, validateCodeAgentChatSchema } from "../internal/cloud/code-agent-chat.mjs"; import { classifyCodexRunnerCapability, classifyCodeAgentChatReadiness, isHttpNon2xxStatus, summarizeCodeAgentPayload } from "./src/code-agent-response-contract.mjs"; const defaultLiveUrl = "http://74.48.78.17:16666/"; const defaultLiveMessage = "请用一句话回复 HWLAB Code Agent readiness。"; const args = parseArgs(process.argv.slice(2)); if (args.mode === "help") { process.stdout.write(`${JSON.stringify(usage(), null, 2)}\n`); } else if (args.mode === "live") { const report = await runLiveReadinessSmoke(args); process.stdout.write(`${JSON.stringify(report, null, 2)}\n`); process.exitCode = report.status === "pass" ? 0 : 2; } else { await runLocalContractSmoke(); } function logOk(name) { process.stdout.write(`[code-agent-chat-smoke] ok ${name}\n`); } async function runLocalContractSmoke() { const completed = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-smoke", traceId: "trc_code-agent-chat-smoke", projectId: "prj_code-agent-chat-smoke", message: "请用一句话说明当前 HWLAB 工作台可以做什么。" }, { now: () => "2026-05-22T00:00:00.000Z", callProvider: async ({ providerPlan }) => ({ provider: providerPlan.provider, model: providerPlan.model, backend: providerPlan.backend, content: "HWLAB 工作台可以用 Code Agent 对话来整理任务、查看资源并保持硬件变更受控。", usage: null, providerTrace: { source: "schema-smoke-provider" } }) } ); validateCodeAgentChatSchema(completed); assert.equal(completed.conversationId, "cnv_code-agent-chat-smoke"); assert.equal(completed.sessionId, "cnv_code-agent-chat-smoke"); assert.match(completed.messageId, /^msg_/); assert.equal(completed.status, "completed"); assert.equal(completed.createdAt, "2026-05-22T00:00:00.000Z"); assert.equal(completed.updatedAt, "2026-05-22T00:00:00.000Z"); assert.equal(completed.traceId, "trc_code-agent-chat-smoke"); assert.equal(completed.error, undefined); assert.match(completed.reply.content, /Code Agent/); logOk("completed schema"); const sourceReadiness = classifyCodeAgentChatReadiness(completed, { realDevLive: false }); assert.equal(sourceReadiness.status, "blocked"); assert.equal(sourceReadiness.level, "SOURCE"); assert.equal(sourceReadiness.devLiveReplyPass, false); logOk("local stub completion is not #143 DEV-LIVE pass"); const echoReadiness = classifyCodeAgentChatReadiness({ ...completed, provider: "echo-mock", model: "mock-model", backend: "hwlab-cloud-api/echo-mock" }, { realDevLive: true, httpStatus: 200 }); assert.equal(echoReadiness.status, "blocked"); assert.equal(echoReadiness.level, "BLOCKED/untrusted-completion"); assert.equal(echoReadiness.blocker, "untrusted-completion"); assert.equal(echoReadiness.devLiveReplyPass, false); assert.match(echoReadiness.reason, /echo\/mock\/stub/u); logOk("echo/mock completion is not real DEV-LIVE pass"); const failed = await handleCodeAgentChat( { traceId: "trc_code-agent-chat-smoke-failed", message: "你好" }, { now: () => "2026-05-22T00:01:00.000Z", env: { PATH: "", HWLAB_CODE_AGENT_PROVIDER: "codex-cli", HWLAB_CODE_AGENT_MODEL: "gpt-test" } } ); validateCodeAgentChatSchema(failed); assert.match(failed.conversationId, /^cnv_/); assert.match(failed.sessionId, /^cnv_/); assert.match(failed.messageId, /^msg_/); assert.equal(failed.status, "failed"); assert.equal(failed.createdAt, "2026-05-22T00:01:00.000Z"); assert.equal(failed.updatedAt, "2026-05-22T00:01:00.000Z"); assert.equal(failed.traceId, "trc_code-agent-chat-smoke-failed"); assert.equal(failed.provider, "codex-cli"); assert.equal(failed.model, "gpt-test"); assert.equal(failed.backend, "hwlab-cloud-api/codex-cli"); assert.equal(failed.error.code, "provider_unavailable"); assert.match(failed.error.message, /Codex CLI command is not available/); assert.deepEqual(failed.error.missingCommands, ["codex"]); assert.ok(failed.error.missingEnv.includes("OPENAI_API_KEY")); assert.equal(failed.availability.status, "partial"); assert.match(failed.availability.blocker, /Codex stdio|long-lived/u); assert.equal(failed.availability.reason, "codex_stdio_supervisor_disabled"); assert.equal(failed.availability.runner.ready, true); assert.match(failed.availability.summary, /受控只读 runner/u); assert.match(failed.availability.summary, /hwlab-code-agent-provider\/openai-api-key/u); assert.equal(JSON.stringify(failed).includes("sk-"), false); assert.equal(Object.hasOwn(failed, "reply"), false); logOk("failed provider-gap schema"); const credentialReadiness = classifyCodeAgentChatReadiness(failed, { realDevLive: true }); assert.equal(credentialReadiness.status, "blocked"); assert.equal(credentialReadiness.level, "BLOCKED/credential"); assert.equal(credentialReadiness.blocker, "credential"); assert.equal(credentialReadiness.devLiveReplyPass, false); logOk("provider credential blocker readiness"); const upstreamBlocked = classifyCodeAgentChatReadiness({ ...failed, error: { code: "provider_unavailable", message: "OpenAI Responses returned HTTP 502: request rejected", providerStatus: 502, missingEnv: [] }, availability: { status: "available" } }, { realDevLive: true, httpStatus: 200 }); assert.equal(upstreamBlocked.status, "blocked"); assert.equal(upstreamBlocked.level, "BLOCKED/provider"); assert.equal(upstreamBlocked.blocker, "provider-upstream"); assert.equal(upstreamBlocked.providerStatus, 502); assert.equal(upstreamBlocked.devLiveReplyPass, false); logOk("provider 502 blocker readiness"); const completedLivePayload = { ...completed, provider: "openai-responses", model: "gpt-5.5", backend: "hwlab-cloud-api/openai-responses" }; const runnerPwd = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-runner-pwd", traceId: "trc_code-agent-chat-runner-pwd", message: "用pwd列出你当前的工作目录" }, { now: () => "2026-05-22T00:02:00.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); validateCodeAgentChatSchema(runnerPwd); assert.equal(runnerPwd.status, "completed"); assert.equal(runnerPwd.provider, "codex-readonly-runner"); assert.equal(runnerPwd.workspace, process.cwd()); assert.equal(runnerPwd.sandbox, "read-only"); assert.equal(runnerPwd.capabilityLevel, "read-only-session-tools"); assert.equal(runnerPwd.session.status, "idle"); assert.equal(runnerPwd.session.workspace, process.cwd()); assert.equal(runnerPwd.session.sandbox, "read-only"); assert.equal(runnerPwd.session.idleTimeoutMs, 1800000); assert.equal(runnerPwd.session.lastTraceId, "trc_code-agent-chat-runner-pwd"); assert.equal(runnerPwd.sessionMode, "controlled-readonly-session-registry"); assert.equal(runnerPwd.implementationType, "controlled-readonly-session-registry"); assert.equal(runnerPwd.runner.session, "controlled-readonly-session-registry"); assert.equal(runnerPwd.runner.codexStdio, false); assert.equal(runnerPwd.runner.writeCapable, false); assert.equal(runnerPwd.runner.durableSession, false); assert.equal(runnerPwd.sessionReuse.reused, false); assert.equal(runnerPwd.sessionReuse.turn, 1); assert.equal(runnerPwd.sessionReuse.status, "idle"); assert.ok(runnerPwd.runnerLimitations.includes("not-codex-stdio")); assert.equal(runnerPwd.codexStdioFeasibility.status, "blocked"); assert.equal(runnerPwd.longLivedSessionGate.status, "blocked"); assert.equal(runnerPwd.longLivedSessionGate.pass, false); assert.ok(runnerPwd.longLivedSessionGate.blockers.some((blocker) => blocker.code === "controlled_readonly_not_long_lived_stdio")); assert.equal(runnerPwd.toolCalls[0].name, "pwd"); assert.equal(runnerPwd.toolCalls[0].status, "completed"); const runnerCapability = classifyCodexRunnerCapability(runnerPwd, { httpStatus: 200 }); assert.equal(runnerCapability.status, "blocked"); assert.equal(runnerCapability.capabilityPass, false); assert.equal(runnerCapability.blocker, "controlled-readonly-not-long-lived"); logOk("read-only runner pwd remains partial capability"); const runnerSecondTurn = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-runner-pwd", traceId: "trc_code-agent-chat-runner-session-reuse", message: "ls ." }, { now: () => "2026-05-22T00:02:01.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); validateCodeAgentChatSchema(runnerSecondTurn); assert.equal(runnerSecondTurn.status, "completed"); assert.equal(runnerSecondTurn.sessionId, runnerPwd.sessionId); assert.equal(runnerSecondTurn.session.sessionId, runnerPwd.session.sessionId); assert.equal(runnerSecondTurn.sessionReuse.reused, true); assert.equal(runnerSecondTurn.sessionReuse.turn, 2); assert.equal(runnerSecondTurn.session.lastTraceId, "trc_code-agent-chat-runner-session-reuse"); assert.equal(runnerSecondTurn.toolCalls[0].name, "ls"); assert.equal(runnerSecondTurn.toolCalls[0].status, "completed"); assert.match(runnerSecondTurn.reply.content, /package\.json/u); logOk("same conversation reuses controlled read-only session registry"); const fallbackCapability = classifyCodexRunnerCapability(completedLivePayload, { httpStatus: 200 }); assert.equal(fallbackCapability.status, "blocked"); assert.equal(fallbackCapability.blocker, "openai-fallback-not-runner"); assert.equal(fallbackCapability.capabilityPass, false); logOk("OpenAI fallback does not satisfy Codex runner capability"); const missingSkills = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-missing-skills", traceId: "trc_code-agent-chat-missing-skills", message: "列出你可用的skills" }, { now: () => "2026-05-22T00:03:00.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), HWLAB_CODE_AGENT_SKILLS_DIRS: "/tmp/hwlab-code-agent-smoke-missing-skills" }, skillsDirs: ["/tmp/hwlab-code-agent-smoke-missing-skills"], skillsDirsExact: true } ); validateCodeAgentChatSchema(missingSkills); assert.equal(missingSkills.status, "failed"); assert.equal(missingSkills.error.code, "skills_unavailable"); assert.equal(missingSkills.skills.status, "blocked"); assert.equal(missingSkills.skills.blockers[0].sourceIssue, "pikasTech/HWLAB#136"); assert.equal(classifyCodexRunnerCapability(missingSkills, { httpStatus: 200 }).capabilityPass, false); logOk("missing skills is structured blocker"); const boundedCat = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-cat", traceId: "trc_code-agent-chat-cat", message: "请用cat读取 package.json" }, { now: () => "2026-05-22T00:04:00.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); validateCodeAgentChatSchema(boundedCat); assert.equal(boundedCat.status, "completed"); assert.equal(boundedCat.toolCalls[0].name, "cat"); assert.equal(boundedCat.toolCalls[0].status, "completed"); assert.ok(boundedCat.toolCalls[0].stdout.length <= 4100); assert.match(boundedCat.reply.content, /"name": "hwlab"/u); logOk("bounded cat read-only tool capability"); const boundedRgFiles = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-rg-files", traceId: "trc_code-agent-chat-rg-files", message: "rg --files ." }, { now: () => "2026-05-22T00:04:30.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); validateCodeAgentChatSchema(boundedRgFiles); assert.equal(boundedRgFiles.status, "completed"); assert.equal(boundedRgFiles.toolCalls[0].name, "rg --files"); assert.equal(boundedRgFiles.toolCalls[0].status, "completed"); assert.ok(boundedRgFiles.toolCalls[0].stdout.length <= 4100); assert.match(boundedRgFiles.reply.content, /受控只读 rg --files 结果/u); logOk("bounded rg --files read-only tool capability"); const toolUnavailable = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-tool-unavailable", traceId: "trc_code-agent-chat-tool-unavailable", message: "请用grep搜索 package.json" }, { now: () => "2026-05-22T00:04:40.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); validateCodeAgentChatSchema(toolUnavailable); assert.equal(toolUnavailable.status, "failed"); assert.equal(toolUnavailable.error.code, "tool_unavailable"); assert.equal(toolUnavailable.toolCalls[0].status, "blocked"); assert.equal(classifyCodexRunnerCapability(toolUnavailable, { httpStatus: 200 }).capabilityPass, false); logOk("tool unavailable is blocked, not completed runner capability"); const securityBlocked = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-security-blocked", traceId: "trc_code-agent-chat-security-blocked", message: "请cat ../secret" }, { now: () => "2026-05-22T00:04:50.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd() } } ); validateCodeAgentChatSchema(securityBlocked); assert.equal(securityBlocked.status, "failed"); assert.equal(securityBlocked.error.code, "security_blocked"); assert.equal(securityBlocked.toolCalls[0].status, "blocked"); assert.equal(classifyCodexRunnerCapability(securityBlocked, { httpStatus: 200 }).capabilityPass, false); logOk("security blocked path is structured"); const m3SkillCalls = []; const m3SkillWrite = await handleCodeAgentChat( { conversationId: "cnv_code-agent-chat-m3-skill", traceId: "trc_code-agent-chat-m3-skill", message: "请通过 M3 DO1 写入 true,并回读 DI1" }, { now: () => "2026-05-22T00:04:55.000Z", env: { PATH: process.env.PATH, HWLAB_CODE_AGENT_WORKSPACE: process.cwd(), HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667", OPENAI_API_KEY: "must-not-be-used" }, callProvider: async () => { throw new Error("OpenAI fallback must not be used for M3 IO"); }, m3IoSkillRequestJson: async (url, request) => { m3SkillCalls.push({ url, request }); return { ok: true, status: 200, body: { status: "succeeded", accepted: true, action: "do.write", traceId: "trc_code-agent-chat-m3-skill", operationId: "op_m3_do_write_smoke", auditId: "aud_m3_do_write_smoke_succeeded", evidenceId: "evd_m3_do_write_smoke_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: true, targetReadback: { status: "succeeded", value: true } }, controlPath: { cloudApi: true, gatewaySimu: true, boxSimu: true, patchPanel: true, frontendBypass: false } } }; } } ); validateCodeAgentChatSchema(m3SkillWrite); assert.equal(m3SkillWrite.status, "completed"); assert.equal(m3SkillWrite.provider, "hwlab-skill-cli"); assert.equal(m3SkillWrite.toolCalls[0].name, "hwlab-agent-runtime.m3-io"); assert.equal(m3SkillWrite.toolCalls[0].route, "/v1/m3/io"); assert.equal(m3SkillWrite.providerTrace.fallbackUsed, false); assert.equal(m3SkillWrite.runner.safety.directGatewayCallsAllowed, false); assert.equal(m3SkillCalls.length, 1); assert.equal(m3SkillCalls[0].url, "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/m3/io"); assert.equal(m3SkillCalls[0].url.includes("gateway-simu"), false); assert.equal(m3SkillCalls[0].url.includes("box-simu"), false); assert.equal(m3SkillCalls[0].url.includes("patch-panel"), false); logOk("M3 Skill CLI routes through HWLAB API only"); const completedHttp200Readiness = classifyCodeAgentChatReadiness(completedLivePayload, { realDevLive: true, httpStatus: 200 }); assert.equal(completedHttp200Readiness.status, "blocked"); assert.equal(completedHttp200Readiness.devLiveReplyPass, false); assert.equal(completedHttp200Readiness.blocker, "untrusted-completion"); logOk("HTTP 200 OpenAI fallback completion is not full Code Agent pass"); for (const status of [400, 401, 403, 429, 500, 502, 503, 504]) { const readiness = classifyCodeAgentChatReadiness(completedLivePayload, { realDevLive: true, httpStatus: status }); assert.equal(readiness.status, "blocked", `HTTP ${status} must block`); assert.equal(readiness.blocker, "provider-upstream", `HTTP ${status} blocker`); assert.equal(readiness.providerStatus, status, `HTTP ${status} providerStatus`); assert.equal(readiness.devLiveReplyPass, false, `HTTP ${status} devLiveReplyPass`); assert.match(readiness.reason, /HTTP non-2xx|upstream response/u, `HTTP ${status} reason`); } logOk("non-2xx completion-looking payloads are blocked"); process.stdout.write("[code-agent-chat-smoke] passed\n"); } async function runLiveReadinessSmoke(args) { const endpoint = agentChatEndpoint(args.url); const traceId = protocolId("trc_code-agent-chat-live"); const conversationId = protocolId("cnv_code-agent-chat-live"); let httpStatus = null; let payload = null; let transportError = null; try { const response = await postJson(endpoint, { headers: { "x-trace-id": traceId }, body: { conversationId, traceId, projectId: "prj_hwlab-cloud-workbench", message: args.message } }); httpStatus = response.status; payload = response.json; } catch (error) { transportError = error instanceof Error ? error.message : String(error); } const readiness = payload || isHttpNon2xxStatus(httpStatus) ? classifyCodeAgentChatReadiness(payload, { realDevLive: true, httpStatus }) : { status: "blocked", level: "BLOCKED", blocker: "transport", devLiveReplyPass: false, reason: "real DEV /v1/agent/chat did not return a JSON Code Agent payload" }; if (payload && readiness.status === "pass") { try { validateCodeAgentChatSchema(payload); } catch (error) { readiness.status = "blocked"; readiness.level = "BLOCKED"; readiness.blocker = "schema"; readiness.devLiveReplyPass = false; readiness.reason = error instanceof Error ? error.message : String(error); } } return { status: readiness.status, mode: "live-readiness", url: endpoint.href, httpStatus, readiness, response: summarizePayload(payload, { httpStatus, traceId }), ...(transportError ? { transportError } : {}), safety: { prodTouched: false, servicesRestarted: false, readsSecretValues: false, printsAssistantReply: false, statement: "This smoke posts only to the configured DEV Code Agent chat endpoint and prints reply presence, not reply content or secret values." } }; } function summarizePayload(payload, options = {}) { const responseSummary = summarizeCodeAgentPayload(payload, options); const assistantReply = responseSummary?.hasReply === true && typeof payload?.reply?.content === "string" ? payload.reply.content.trim() : ""; return { status: responseSummary?.status ?? null, provider: responseSummary?.provider ?? null, model: responseSummary?.model ?? null, backend: responseSummary?.backend ?? null, runner: responseSummary?.runner ?? null, workspace: responseSummary?.workspace ?? null, sandbox: responseSummary?.sandbox ?? null, capabilityLevel: responseSummary?.capabilityLevel ?? null, sessionMode: responseSummary?.sessionMode ?? null, sessionReuse: responseSummary?.sessionReuse ?? null, implementationType: responseSummary?.implementationType ?? null, runnerLimitations: responseSummary?.runnerLimitations ?? [], toolCalls: responseSummary?.toolCalls ?? [], skills: responseSummary?.skills ?? null, runnerTrace: responseSummary?.runnerTrace ?? null, codexStdioFeasibility: responseSummary?.codexStdioFeasibility ?? null, session: responseSummary?.session ?? null, longLivedSessionGate: responseSummary?.longLivedSessionGate ?? null, assistantReplyNonEmpty: responseSummary?.hasReply === true, assistantReplyLength: assistantReply.length, error: responseSummary?.error ?? null }; } function parseArgs(argv) { const parsed = { mode: "local", url: defaultLiveUrl, message: defaultLiveMessage }; for (let index = 0; index < argv.length; index += 1) { const arg = argv[index]; if (arg === "--live") { parsed.mode = "live"; } else if (arg === "--url") { index += 1; if (!argv[index]) throw new Error("--url requires a value"); parsed.url = argv[index]; } else if (arg === "--message") { index += 1; if (!argv[index]) throw new Error("--message requires a value"); parsed.message = argv[index]; } else if (arg === "--help") { return { mode: "help" }; } else { throw new Error(`unknown argument ${arg}`); } } return parsed; } function usage() { return { status: "usage", command: "node scripts/code-agent-chat-smoke.mjs [--live --url http://74.48.78.17:16666/]", notes: [ "Default mode is local schema/readiness contract only and cannot pass #143 DEV-LIVE.", "--live posts a minimal chat prompt to real DEV and passes only on completed plus non-empty assistant reply.", "provider_unavailable with missing OPENAI_API_KEY or missing/forbidden HWLAB_CODE_AGENT_OPENAI_BASE_URL is reported as BLOCKED/provider-config." ] }; } function agentChatEndpoint(value) { const url = new URL(value); if (url.pathname.endsWith("/v1/agent/chat")) return url; return new URL("/v1/agent/chat", url); } function postJson(url, { headers = {}, body, timeoutMs = 10000 }) { const client = url.protocol === "https:" ? https : http; const payload = JSON.stringify(body); return new Promise((resolve, reject) => { const request = client.request( url, { method: "POST", headers: { "content-type": "application/json", "content-length": Buffer.byteLength(payload), ...headers }, insecureHTTPParser: true, timeout: timeoutMs }, (response) => { const chunks = []; response.on("data", (chunk) => chunks.push(chunk)); response.on("end", () => { const text = Buffer.concat(chunks).toString("utf8"); resolve({ status: response.statusCode ?? null, body: text, json: parseJsonOrNull(text) }); }); } ); request.on("timeout", () => request.destroy(new Error(`request timed out after ${timeoutMs}ms`))); request.on("error", reject); request.end(payload); }); } function parseJsonOrNull(value) { try { return JSON.parse(value); } catch { return null; } } function protocolId(prefix) { return `${prefix}_${Date.now().toString(36)}`; }