4898 lines
198 KiB
JavaScript
4898 lines
198 KiB
JavaScript
import assert from "node:assert/strict";
|
||
import { createServer as createRawHttpServer } from "node:http";
|
||
import { createServer as createRawTcpServer } from "node:net";
|
||
import { chmod, mkdtemp, mkdir, rm, writeFile } from "node:fs/promises";
|
||
import os from "node:os";
|
||
import path from "node:path";
|
||
import test from "node:test";
|
||
|
||
import { createCloudApiServer as createRawCloudApiServer } from "./server.mjs";
|
||
import { validateCodeAgentChatSchema } from "./code-agent-chat.mjs";
|
||
import { createCodexStdioSessionManager } from "./codex-stdio-session.mjs";
|
||
import { createCodeAgentTraceStore } from "./code-agent-trace-store.mjs";
|
||
import { createCloudRuntimeStore } from "../db/runtime-store.mjs";
|
||
import { classifyCodexRunnerCapability } from "../../scripts/src/code-agent-response-contract.mjs";
|
||
import {
|
||
RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
|
||
RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
|
||
RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
|
||
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
|
||
RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED
|
||
} from "../db/runtime-store.mjs";
|
||
import { HWLAB_M3_IO_CAPABILITY_LEVELS } from "../../skills/hwlab-agent-runtime/scripts/src/m3-io-skill-client.mjs";
|
||
|
||
const CODEX_STDIO_FEASIBILITY_BLOCKERS = new Set([
|
||
"codex_cli_binary_missing",
|
||
"codex_stdio_supervisor_disabled",
|
||
"codex_stdio_egress_boundary"
|
||
]);
|
||
|
||
function createHttpServer(...args) {
|
||
return trackTestServer(createRawHttpServer(...args));
|
||
}
|
||
|
||
function createTcpServer(...args) {
|
||
return trackTestServer(createRawTcpServer(...args));
|
||
}
|
||
|
||
function createCloudApiServer(...args) {
|
||
return trackTestServer(createRawCloudApiServer(...args));
|
||
}
|
||
|
||
function trackTestServer(server) {
|
||
const sockets = new Set();
|
||
server.on("connection", (socket) => {
|
||
sockets.add(socket);
|
||
socket.on("close", () => sockets.delete(socket));
|
||
});
|
||
const close = server.close.bind(server);
|
||
server.close = (callback) => {
|
||
server.closeIdleConnections?.();
|
||
for (const socket of sockets) socket.destroy();
|
||
return close(callback);
|
||
};
|
||
return server;
|
||
}
|
||
|
||
function assertCodexStdioFeasibilityBlocker(value, label = "codex stdio blocker") {
|
||
assert.equal(CODEX_STDIO_FEASIBILITY_BLOCKERS.has(value), true, `${label}: ${value}`);
|
||
}
|
||
|
||
test("cloud api exposes /health, /health/live, and /live probes", async () => {
|
||
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
|
||
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
delete process.env.HWLAB_CLOUD_DB_URL;
|
||
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: "",
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex"
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
|
||
const health = await fetch(`http://127.0.0.1:${port}/health`);
|
||
assert.equal(health.status, 200);
|
||
const healthPayload = await health.json();
|
||
assert.equal(healthPayload.serviceId, "hwlab-cloud-api");
|
||
assert.equal(healthPayload.environment, "dev");
|
||
assert.equal(healthPayload.status, "degraded");
|
||
assert.equal(healthPayload.commit.id.length > 0, true);
|
||
assert.equal(healthPayload.image.reference.length > 0, true);
|
||
assert.equal(typeof healthPayload.revision, "string");
|
||
assert.equal(typeof healthPayload.build, "object");
|
||
assert.equal(Object.hasOwn(healthPayload.build, "createdAt"), true);
|
||
assert.equal(healthPayload.service.id, "hwlab-cloud-api");
|
||
assert.equal(healthPayload.db.status, "blocked");
|
||
assert.equal(healthPayload.db.connected, false);
|
||
assert.equal(healthPayload.db.liveConnected, false);
|
||
assert.equal(healthPayload.db.liveDbEvidence, false);
|
||
assert.equal(healthPayload.db.connectionAttempted, false);
|
||
assert.equal(healthPayload.db.connectionResult, "not_attempted_missing_env");
|
||
assert.equal(healthPayload.db.endpointSource, "secret-url-host");
|
||
assert.equal(healthPayload.db.endpoint.authoritative.source, "secret-url-host");
|
||
assert.equal(healthPayload.db.endpoint.authoritative.env, "HWLAB_CLOUD_DB_URL");
|
||
assert.equal(healthPayload.db.endpoint.authoritative.usedForProbe, true);
|
||
assert.equal(healthPayload.db.optionalPublicDnsAlias.source, "optional-public-dns-alias");
|
||
assert.equal(healthPayload.db.optionalPublicDnsAlias.requiredForReadiness, false);
|
||
assert.equal(healthPayload.db.optionalPublicDnsAlias.usedForProbe, false);
|
||
assert.equal(healthPayload.db.redaction.valuesRedacted, true);
|
||
assert.equal(healthPayload.db.redaction.secretMaterialRead, false);
|
||
assert.equal(healthPayload.db.safety.liveDbEvidence, false);
|
||
assert.equal(healthPayload.runtime.durable, false);
|
||
assert.equal(healthPayload.runtime.status, "degraded");
|
||
assert.equal(healthPayload.runtime.durabilityContract.dbLiveEvidenceIsDurabilityEvidence, false);
|
||
assert.equal(healthPayload.runtime.durabilityContract.blockedLayer, "adapter");
|
||
assert.equal(healthPayload.readiness.contractVersion, "v3");
|
||
assert.equal(healthPayload.readiness.durability.status, "blocked");
|
||
assert.equal(healthPayload.readiness.durability.dbLiveEvidenceObserved, false);
|
||
assert.equal(healthPayload.readiness.durability.dbLiveEvidenceIsDurabilityEvidence, false);
|
||
assert.equal(healthPayload.readiness.durability.blockedLayer, "adapter");
|
||
assert.equal(healthPayload.readiness.provider.status, "blocked");
|
||
assert.equal(healthPayload.readiness.provider.ready, false);
|
||
assert.equal(healthPayload.readiness.provider.blocker, "provider_config_blocked");
|
||
assert.equal(healthPayload.readiness.dbDurable.status, "blocked");
|
||
assert.equal(healthPayload.readiness.dbDurable.ready, false);
|
||
assert.equal(healthPayload.readiness.sessionRunner.status, "blocked");
|
||
assert.equal(healthPayload.readiness.sessionRunner.ready, false);
|
||
assert.equal(healthPayload.readiness.sessionRunner.codexStdio, true);
|
||
assert.equal(healthPayload.readiness.sessionRunner.durableSession, false);
|
||
assert.equal(healthPayload.readiness.sessionRunner.longLivedSession, false);
|
||
assert.equal(healthPayload.readiness.codexStdio.status, "blocked");
|
||
assert.equal(healthPayload.readiness.codexStdio.ready, false);
|
||
assertCodexStdioFeasibilityBlocker(healthPayload.readiness.codexStdio.blocker);
|
||
assert.equal(healthPayload.readiness.codeAgent.providerReady, false);
|
||
assert.equal(healthPayload.readiness.codeAgent.durableDbReady, false);
|
||
assert.equal(healthPayload.readiness.codeAgent.sessionRunnerReady, false);
|
||
assert.equal(healthPayload.readiness.codeAgent.codexStdioFeasible, false);
|
||
assert.deepEqual(healthPayload.readiness.codeAgent.currentBlockers.slice(0, 2), [
|
||
"provider_config_blocked",
|
||
"runtime_durable_adapter_missing"
|
||
]);
|
||
assertCodexStdioFeasibilityBlocker(healthPayload.readiness.codeAgent.currentBlockers[2], "readiness current blocker");
|
||
assert.equal(healthPayload.readiness.codeAgent.secretMaterialRead, false);
|
||
assert.equal(healthPayload.codeAgent.status, "blocked");
|
||
assert.equal(healthPayload.codeAgent.agentKind, "codex-stdio-blocked");
|
||
assert.equal(healthPayload.codeAgent.partialReady, false);
|
||
assert.match(healthPayload.codeAgent.blocker, /Codex CLI command/u);
|
||
assert.equal(healthPayload.codeAgent.reason, "codex_cli_binary_missing");
|
||
assert.equal(healthPayload.codeAgent.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(healthPayload.codeAgent.runner.ready, false);
|
||
assert.equal(healthPayload.codeAgent.capabilityLevel, "blocked");
|
||
assert.equal(healthPayload.codeAgent.sessionRegistry.status, "available");
|
||
assert.equal(healthPayload.codeAgent.longLivedSessionGate.status, "blocked");
|
||
assert.ok(healthPayload.codeAgent.longLivedSessionGate.blockers.some((blocker) => blocker.code === "codex_cli_binary_missing"));
|
||
assert.ok(healthPayload.codeAgent.longLivedSessionGate.blockers.some((blocker) => blocker.code === "provider_token_boundary"));
|
||
assert.equal(healthPayload.codeAgent.codexStdio.runtimeContract.binary.status, "missing");
|
||
assert.equal(healthPayload.codeAgent.codexStdio.runtimeContract.stdioProtocol.status, "blocked");
|
||
assert.equal(healthPayload.codeAgent.codexStdio.runtimeContract.lifecycleSupervisor.status, "present");
|
||
assert.equal(healthPayload.codeAgent.codexStdio.runtimeContract.cancelReapTraceReadiness.status, "ready");
|
||
assert.ok(healthPayload.codeAgent.codexStdio.blockerCodes.includes("codex_cli_binary_missing"));
|
||
assert.ok(healthPayload.codeAgent.codexStdio.blockerCodes.includes("stdio_protocol_not_wired"));
|
||
assert.ok(healthPayload.codeAgent.codexStdio.blockerCodes.includes("provider_token_boundary"));
|
||
assert.deepEqual(healthPayload.codeAgent.missingEnv, ["OPENAI_API_KEY"]);
|
||
assert.equal(healthPayload.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider");
|
||
assert.equal(healthPayload.codeAgent.secretRefs[0].secretKey, "openai-api-key");
|
||
assert.equal(healthPayload.codeAgent.secretRefs[0].redacted, true);
|
||
assert.equal(healthPayload.codeAgent.egress.directPublicOpenAi, false);
|
||
assert.equal(healthPayload.codeAgent.safety.secretMaterialRead, false);
|
||
assert.equal(JSON.stringify(healthPayload.codeAgent).includes("sk-"), false);
|
||
assert.deepEqual(healthPayload.db.missingEnv, ["HWLAB_CLOUD_DB_URL", "HWLAB_CLOUD_DB_SSL_MODE"]);
|
||
assert.equal(healthPayload.db.secretRefs[0].secretName, "hwlab-cloud-api-dev-db");
|
||
assert.equal(healthPayload.db.secretRefs[0].redacted, true);
|
||
|
||
const healthLive = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
assert.equal(healthLive.status, 200);
|
||
const healthLivePayload = await healthLive.json();
|
||
assert.equal(healthLivePayload.serviceId, "hwlab-cloud-api");
|
||
assert.equal(healthLivePayload.status, "degraded");
|
||
assert.equal(healthLivePayload.commit.id.length > 0, true);
|
||
assert.equal(typeof healthLivePayload.build, "object");
|
||
assert.equal(healthLivePayload.db.ready, false);
|
||
assert.equal(healthLivePayload.codeAgent.status, "blocked");
|
||
assert.equal(healthLivePayload.codeAgent.ready, false);
|
||
assert.equal(healthLivePayload.codeAgent.partialReady, false);
|
||
assert.equal(healthLivePayload.codeAgent.capabilityLevel, "blocked");
|
||
|
||
const readiness = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
assert.equal(readiness.status, 200);
|
||
assert.equal((await readiness.json()).status, "degraded");
|
||
|
||
const live = await fetch(`http://127.0.0.1:${port}/live`);
|
||
assert.equal(live.status, 200);
|
||
assert.equal((await live.json()).status, "live");
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
if (originalUrl === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_URL;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
|
||
}
|
||
if (originalSslMode === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
|
||
}
|
||
}
|
||
});
|
||
|
||
test("cloud api aggregates live HWLAB build times from health and controlled deploy/catalog metadata", async () => {
|
||
const healthByPath = new Map([
|
||
["/hwlab-cloud-web/health/live", {
|
||
serviceId: "hwlab-cloud-web",
|
||
status: "ok",
|
||
revision: "webabcdef123456",
|
||
commit: { id: "webabcdef123456", source: "test" },
|
||
image: {
|
||
reference: "127.0.0.1:5000/hwlab/hwlab-cloud-web:webabcd",
|
||
tag: "webabcd",
|
||
digest: "sha256:" + "1".repeat(64)
|
||
},
|
||
build: {
|
||
createdAt: "2026-05-23T02:10:00.000Z",
|
||
metadataSource: "runtime-env:HWLAB_BUILD_CREATED_AT"
|
||
}
|
||
}],
|
||
["/hwlab-agent-mgr/health/live", {
|
||
serviceId: "hwlab-agent-mgr",
|
||
status: "ok",
|
||
revision: "48dfbf9",
|
||
commit: { id: "48dfbf9", source: "test" },
|
||
image: {
|
||
reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
|
||
tag: "48dfbf9",
|
||
digest: "sha256:" + "2".repeat(64)
|
||
}
|
||
}],
|
||
["/hwlab-agent-skills/health/live", {
|
||
serviceId: "hwlab-agent-skills",
|
||
status: "ok",
|
||
revision: "skillsabcdef123456",
|
||
commit: { id: "skillsabcdef123456", source: "test" },
|
||
image: {
|
||
reference: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd",
|
||
tag: "skillsabcd",
|
||
digest: "sha256:" + "5".repeat(64)
|
||
}
|
||
}],
|
||
["/external/health", {
|
||
serviceId: "postgres",
|
||
status: "ok",
|
||
image: {
|
||
reference: "postgres:16",
|
||
tag: "16"
|
||
}
|
||
}]
|
||
]);
|
||
const liveBuildMetadata = {
|
||
deployManifest: {
|
||
services: [
|
||
{
|
||
serviceId: "hwlab-cloud-web",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-cloud-web:webcat",
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
replicas: 1
|
||
},
|
||
{
|
||
serviceId: "hwlab-agent-mgr",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3df89fe",
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
replicas: 1
|
||
},
|
||
{
|
||
serviceId: "hwlab-agent-skills",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd",
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
replicas: 1,
|
||
env: {
|
||
HWLAB_COMMIT_ID: "skillsabcdef123456",
|
||
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd",
|
||
HWLAB_IMAGE_TAG: "skillsabcd",
|
||
HWLAB_BUILD_CREATED_AT: "2026-05-23T05:30:00.000Z",
|
||
HWLAB_BUILD_SOURCE: "deploy-env-test"
|
||
}
|
||
},
|
||
{
|
||
serviceId: "hwlab-agent-worker",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-agent-worker:workcat",
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
replicas: 0
|
||
},
|
||
{
|
||
serviceId: "hwlab-edge-proxy",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-edge-proxy:edgecat",
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health",
|
||
replicas: 1
|
||
},
|
||
{
|
||
serviceId: "hwlab-extra-lab",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-extra-lab:extracat",
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
replicas: 1
|
||
}
|
||
]
|
||
},
|
||
artifactCatalog: {
|
||
services: [
|
||
{
|
||
serviceId: "hwlab-agent-mgr",
|
||
commitId: "3df89fe",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:3df89fe",
|
||
imageTag: "3df89fe",
|
||
digest: "sha256:" + "3".repeat(64),
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
buildCreatedAt: "2026-05-23T04:20:00.000Z",
|
||
buildSource: "artifact-catalog-test"
|
||
},
|
||
{
|
||
serviceId: "hwlab-agent-skills",
|
||
commitId: "skillsabcdef123456",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-agent-skills:skillsabcd",
|
||
imageTag: "skillsabcd",
|
||
digest: "sha256:" + "5".repeat(64),
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
buildCreatedAt: null,
|
||
buildSource: null
|
||
},
|
||
{
|
||
serviceId: "hwlab-extra-lab",
|
||
commitId: "extracatabcdef123456",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-extra-lab:extracat",
|
||
imageTag: "extracat",
|
||
digest: "sha256:" + "4".repeat(64),
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
buildCreatedAt: "2026-05-23T03:00:00.000Z",
|
||
buildSource: "artifact-catalog-test"
|
||
}
|
||
]
|
||
}
|
||
};
|
||
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: "",
|
||
HWLAB_COMMIT_ID: "apiabcdef123456",
|
||
HWLAB_IMAGE: "127.0.0.1:5000/hwlab/hwlab-cloud-api:apiabcd",
|
||
HWLAB_IMAGE_TAG: "apiabcd",
|
||
HWLAB_BUILD_CREATED_AT: "2026-05-23T01:00:00.000Z",
|
||
HWLAB_CLOUD_WEB_SERVICE_URL: "http://live.test/hwlab-cloud-web",
|
||
HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr",
|
||
HWLAB_AGENT_WORKER_URL: "http://live.test/missing-agent-worker",
|
||
HWLAB_GATEWAY_URL: "http://live.test/missing-gateway",
|
||
HWLAB_GATEWAY_SIMU_URL: "http://live.test/missing-gateway-simu",
|
||
HWLAB_BOX_SIMU_URL: "http://live.test/missing-box-simu",
|
||
HWLAB_PATCH_PANEL_URL: "http://live.test/missing-patch-panel",
|
||
HWLAB_ROUTER_URL: "http://live.test/missing-router",
|
||
HWLAB_TUNNEL_CLIENT_URL: "http://live.test/missing-tunnel-client",
|
||
HWLAB_EDGE_PROXY_URL: "http://live.test/external",
|
||
HWLAB_CLI_URL: "http://live.test/missing-cli",
|
||
HWLAB_AGENT_SKILLS_URL: "http://live.test/hwlab-agent-skills",
|
||
HWLAB_EXTRA_LAB_URL: "http://live.test/missing-extra-lab"
|
||
},
|
||
liveBuildMetadata,
|
||
fetchImpl: async (url) => {
|
||
const path = new URL(url).pathname;
|
||
const payload = healthByPath.get(path);
|
||
if (!payload) {
|
||
return {
|
||
ok: false,
|
||
status: 503,
|
||
async text() {
|
||
return JSON.stringify({ error: "offline" });
|
||
}
|
||
};
|
||
}
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
async text() {
|
||
return JSON.stringify(payload);
|
||
}
|
||
};
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/live-builds`);
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.contractVersion, "live-builds-v1");
|
||
assert.equal(payload.latest.serviceId, "hwlab-agent-skills");
|
||
assert.equal(payload.latest.build.createdAt, "2026-05-23T05:30:00.000Z");
|
||
assert.equal(payload.latest.image.tag, "skillsabcd");
|
||
assert.equal(payload.latest.commit.id, "skillsabcdef123456");
|
||
assert.equal(payload.latest.build.metadataSource, "deploy-env:HWLAB_BUILD_CREATED_AT");
|
||
assert.equal(payload.latest.build.liveMetadataMatch.metadata.imageTag, "skillsabcd");
|
||
assert.match(payload.latest.build.liveHealthMissingReason, /匹配的 deploy metadata/u);
|
||
assert.equal(payload.counts.total, 15);
|
||
assert.equal(payload.counts.external, 2);
|
||
assert.equal(payload.counts.withBuildTime, 3);
|
||
const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr");
|
||
assert.equal(manager.build.createdAt, null);
|
||
assert.equal(manager.image.tag, "48dfbf9");
|
||
assert.equal(manager.revision, "48dfbf9");
|
||
assert.equal(manager.build.metadataSource, "live-health:controlled-metadata-mismatch");
|
||
assert.equal(manager.build.unavailableReason, "构建时间不可用:live tag 与 catalog metadata 不匹配");
|
||
assert.match(manager.build.unavailableDetail, /live tag 48dfbf9/u);
|
||
assert.match(manager.build.unavailableDetail, /metadata tag 3df89fe/u);
|
||
const extra = payload.services.find((service) => service.serviceId === "hwlab-extra-lab");
|
||
assert.equal(extra.build.createdAt, null);
|
||
assert.match(extra.build.unavailableReason, /当前 live health 不可用/u);
|
||
const worker = payload.services.find((service) => service.serviceId === "hwlab-agent-worker");
|
||
assert.equal(worker.build.createdAt, null);
|
||
assert.match(worker.build.unavailableReason, /构建时间不可用:当前 live health 不可用/u);
|
||
assert.match(worker.build.unavailableReason, /hwlab-agent-worker 当前 hwlab-dev desired replicas=0/u);
|
||
assert.ok(payload.services.some((service) => service.kind === "external" && service.serviceId === "hwlab-edge-proxy" && service.healthUrl.endsWith("/health")));
|
||
assert.ok(payload.services.some((service) => service.kind === "external" && service.serviceId === "hwlab-frpc" && service.image.tag === "v0.68.1"));
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api ignores old repository reports and keeps missing buildCreatedAt unavailable", async () => {
|
||
const tempRoot = await mkdtemp(path.join(os.tmpdir(), "hwlab-live-builds-"));
|
||
const deployDir = path.join(tempRoot, "deploy");
|
||
const oldReportDir = path.join(tempRoot, "reports", "dev-gate");
|
||
await mkdir(deployDir, { recursive: true });
|
||
await mkdir(oldReportDir, { recursive: true });
|
||
await writeFile(path.join(deployDir, "deploy.json"), `${JSON.stringify({
|
||
services: [
|
||
{
|
||
serviceId: "hwlab-agent-mgr",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
replicas: 1
|
||
}
|
||
]
|
||
}, null, 2)}\n`);
|
||
await writeFile(path.join(deployDir, "artifact-catalog.dev.json"), `${JSON.stringify({
|
||
services: [
|
||
{
|
||
serviceId: "hwlab-agent-mgr",
|
||
commitId: "48dfbf9abcdef",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
|
||
imageTag: "48dfbf9",
|
||
digest: "sha256:" + "6".repeat(64),
|
||
namespace: "hwlab-dev",
|
||
profile: "dev",
|
||
healthPath: "/health/live",
|
||
buildCreatedAt: null,
|
||
buildSource: null
|
||
}
|
||
]
|
||
}, null, 2)}\n`);
|
||
await writeFile(path.join(oldReportDir, "dev-artifacts.json"), `${JSON.stringify({
|
||
artifactPublish: {
|
||
services: [
|
||
{
|
||
serviceId: "hwlab-agent-mgr",
|
||
sourceCommitId: "48dfbf9abcdef",
|
||
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
|
||
imageTag: "48dfbf9",
|
||
digest: "sha256:" + "6".repeat(64),
|
||
buildCreatedAt: "2099-01-01T00:00:00.000Z",
|
||
buildSource: "old-report-must-not-be-read"
|
||
}
|
||
]
|
||
}
|
||
}, null, 2)}\n`);
|
||
|
||
const observe = async () => {
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: "",
|
||
HWLAB_AGENT_MGR_URL: "http://live.test/hwlab-agent-mgr"
|
||
},
|
||
repoRoot: tempRoot,
|
||
fetchImpl: async (url) => {
|
||
if (new URL(url).pathname === "/hwlab-agent-mgr/health/live") {
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
async text() {
|
||
return JSON.stringify({
|
||
serviceId: "hwlab-agent-mgr",
|
||
status: "ok",
|
||
revision: "48dfbf9abcdef",
|
||
commit: { id: "48dfbf9abcdef", source: "test" },
|
||
image: {
|
||
reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:48dfbf9",
|
||
tag: "48dfbf9",
|
||
digest: "sha256:" + "6".repeat(64)
|
||
}
|
||
});
|
||
}
|
||
};
|
||
}
|
||
return {
|
||
ok: false,
|
||
status: 503,
|
||
async text() {
|
||
return JSON.stringify({ error: "offline" });
|
||
}
|
||
};
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/live-builds`);
|
||
assert.equal(response.status, 200);
|
||
return response.json();
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
};
|
||
|
||
try {
|
||
const withOldReport = await observe();
|
||
await rm(path.join(tempRoot, "reports"), { recursive: true, force: true });
|
||
const withoutOldReport = await observe();
|
||
const summarize = (payload) => {
|
||
const manager = payload.services.find((service) => service.serviceId === "hwlab-agent-mgr");
|
||
return {
|
||
latest: payload.latest,
|
||
source: payload.source,
|
||
createdAt: manager.build.createdAt,
|
||
metadataSource: manager.build.metadataSource,
|
||
unavailableReason: manager.build.unavailableReason,
|
||
unavailableDetail: manager.build.unavailableDetail,
|
||
liveMetadataMatch: manager.build.liveMetadataMatch,
|
||
counts: payload.counts
|
||
};
|
||
};
|
||
|
||
assert.deepEqual(summarize(withOldReport), summarize(withoutOldReport));
|
||
const manager = withOldReport.services.find((service) => service.serviceId === "hwlab-agent-mgr");
|
||
assert.equal(withOldReport.latest, null);
|
||
assert.equal(manager.build.createdAt, null);
|
||
assert.equal(manager.build.metadataSource, "live-health:matched-controlled-metadata-without-build-time");
|
||
assert.equal(manager.build.unavailableReason, "构建时间不可用:当前 live 镜像未提供 buildCreatedAt");
|
||
assert.match(manager.build.unavailableDetail, /catalog metadata/u);
|
||
assert.equal(Object.hasOwn(withOldReport.source, "artifactReportSource"), false);
|
||
assert.equal(JSON.stringify(withOldReport).includes("2099-01-01T00:00:00.000Z"), false);
|
||
assert.equal(JSON.stringify(withOldReport).includes("artifact-report"), false);
|
||
} finally {
|
||
await rm(tempRoot, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api health reports DB env presence and live connection classification without leaking values", async () => {
|
||
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
|
||
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
const originalAliasEnv = {
|
||
HWLAB_CLOUD_DB_SERVICE_NAME: process.env.HWLAB_CLOUD_DB_SERVICE_NAME,
|
||
HWLAB_CLOUD_DB_SERVICE_NAMESPACE: process.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE,
|
||
HWLAB_CLOUD_DB_HOST: process.env.HWLAB_CLOUD_DB_HOST,
|
||
HWLAB_CLOUD_DB_PORT: process.env.HWLAB_CLOUD_DB_PORT
|
||
};
|
||
const fakeDb = createTcpServer((socket) => socket.end());
|
||
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
|
||
const dbPort = fakeDb.address().port;
|
||
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test@127.0.0.1:${dbPort}/hwlab`;
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
|
||
delete process.env.HWLAB_CLOUD_DB_SERVICE_NAME;
|
||
delete process.env.HWLAB_CLOUD_DB_SERVICE_NAMESPACE;
|
||
delete process.env.HWLAB_CLOUD_DB_HOST;
|
||
delete process.env.HWLAB_CLOUD_DB_PORT;
|
||
|
||
const server = createCloudApiServer();
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health`);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "degraded");
|
||
assert.equal(payload.db.status, "ready");
|
||
assert.equal(payload.db.connected, true);
|
||
assert.equal(payload.db.liveConnected, true);
|
||
assert.equal(payload.db.liveDbEvidence, true);
|
||
assert.equal(payload.db.connectionChecked, true);
|
||
assert.equal(payload.db.connectionAttempted, true);
|
||
assert.equal(payload.db.connectionResult, "connected");
|
||
assert.equal(payload.db.endpointSource, "secret-url-host");
|
||
assert.equal(payload.db.connection.endpointSource, "secret-url-host");
|
||
assert.equal(payload.db.endpoint.authoritative.source, "secret-url-host");
|
||
assert.equal(payload.db.endpoint.authoritative.env, "HWLAB_CLOUD_DB_URL");
|
||
assert.equal(payload.db.optionalPublicDnsAlias.source, "optional-public-dns-alias");
|
||
assert.equal(payload.db.optionalPublicDnsAlias.requiredForReadiness, false);
|
||
assert.equal(payload.db.optionalPublicDnsAlias.usedForProbe, false);
|
||
assert.equal(payload.db.optionalPublicDnsAlias.envPresent, false);
|
||
assert.equal(payload.db.configReady, true);
|
||
assert.equal(payload.db.ready, true);
|
||
assert.equal(payload.db.evidence, "live_db_tcp_connection_ready");
|
||
assert.equal(payload.db.safety.liveDbEvidence, true);
|
||
assert.equal(payload.db.safety.liveDbConnectedEvidence, true);
|
||
assert.equal(payload.runtime.adapter, "memory");
|
||
assert.equal(payload.runtime.durable, false);
|
||
assert.equal(payload.runtime.status, "degraded");
|
||
assert.match(payload.runtime.reason, /process-local/u);
|
||
assert.equal(payload.runtime.durabilityContract.blockedLayer, "adapter");
|
||
assert.equal(payload.runtime.durabilityContract.dbLiveEvidenceIsDurabilityEvidence, false);
|
||
assert.equal(payload.readiness.components.db, "ready");
|
||
assert.equal(payload.readiness.components.runtime, "blocked");
|
||
assert.equal(payload.readiness.durability.status, "blocked");
|
||
assert.equal(payload.readiness.durability.dbLiveEvidenceObserved, true);
|
||
assert.equal(payload.readiness.durability.dbLiveEvidenceIsDurabilityEvidence, false);
|
||
assert.equal(payload.readiness.durability.blockedLayer, "adapter");
|
||
assert.equal(payload.readiness.durability.liveRuntimeEvidence, false);
|
||
assert.equal(payload.readiness.provider.status, "blocked");
|
||
assert.equal(payload.readiness.dbDurable.status, "blocked");
|
||
assert.equal(payload.readiness.dbDurable.dbLiveEvidenceObserved, true);
|
||
assert.equal(payload.readiness.dbDurable.blocker, "runtime_durable_adapter_missing");
|
||
assert.equal(payload.readiness.sessionRunner.status, "blocked");
|
||
assert.equal(payload.readiness.codexStdio.status, "blocked");
|
||
assert.equal(payload.db.redaction.valuesRedacted, true);
|
||
assert.equal(payload.db.redaction.endpointRedacted, true);
|
||
assert.equal(payload.db.redaction.secretMaterialRead, false);
|
||
assert.deepEqual(payload.db.missingEnv, []);
|
||
assert.equal(JSON.stringify(payload.db).includes("password"), false);
|
||
assert.equal(JSON.stringify(payload.db).includes("127.0.0.1"), false);
|
||
assert.equal(JSON.stringify(payload.db).includes(String(dbPort)), false);
|
||
assert.equal(JSON.stringify(payload.db).includes("cloud-api-db.hwlab-dev.svc.cluster.local"), false);
|
||
assert.equal(payload.db.fields.find((field) => field.name === "HWLAB_CLOUD_DB_URL").redacted, true);
|
||
assert.equal(payload.db.connection.endpointRedacted, true);
|
||
} finally {
|
||
if (originalUrl === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_URL;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
|
||
}
|
||
if (originalSslMode === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
|
||
}
|
||
for (const [name, value] of Object.entries(originalAliasEnv)) {
|
||
if (value === undefined) {
|
||
delete process.env[name];
|
||
} else {
|
||
process.env[name] = value;
|
||
}
|
||
}
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await new Promise((resolve, reject) => {
|
||
fakeDb.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api health separates DB connected from durable runtime schema readiness", async () => {
|
||
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
|
||
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
const fakeDb = createTcpServer((socket) => socket.end());
|
||
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
|
||
const dbPort = fakeDb.address().port;
|
||
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test@127.0.0.1:${dbPort}/hwlab`;
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
|
||
|
||
const server = createCloudApiServer({
|
||
runtimeStore: {
|
||
async readiness() {
|
||
return {
|
||
adapter: "postgres",
|
||
durable: false,
|
||
durableRequested: true,
|
||
durableCapable: false,
|
||
ready: false,
|
||
status: "blocked",
|
||
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
|
||
reason: "test runtime schema is missing",
|
||
liveRuntimeEvidence: false,
|
||
fixtureEvidence: false,
|
||
connection: {
|
||
queryAttempted: true,
|
||
queryResult: "schema_blocked",
|
||
endpointRedacted: true,
|
||
valueRedacted: true
|
||
},
|
||
schema: {
|
||
checked: true,
|
||
ready: false,
|
||
missingTables: ["gateway_sessions"],
|
||
missingColumns: ["gateway_sessions.gateway_session_json"]
|
||
},
|
||
migration: {
|
||
checked: false,
|
||
ready: false,
|
||
missing: true
|
||
},
|
||
counts: {}
|
||
};
|
||
}
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
const payload = await response.json();
|
||
assert.equal(response.status, 200);
|
||
assert.equal(payload.status, "degraded");
|
||
assert.equal(payload.db.connected, true);
|
||
assert.equal(payload.db.liveDbEvidence, true);
|
||
assert.equal(payload.runtime.durable, false);
|
||
assert.equal(payload.runtime.durableRequested, true);
|
||
assert.equal(payload.runtime.ready, false);
|
||
assert.equal(payload.runtime.blocker, RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED);
|
||
assert.equal(payload.readiness.components.db, "ready");
|
||
assert.equal(payload.readiness.components.runtime, "blocked");
|
||
assert.equal(payload.readiness.durability.status, "blocked");
|
||
assert.equal(payload.readiness.durability.dbLiveEvidenceObserved, true);
|
||
assert.equal(payload.readiness.durability.dbLiveEvidenceIsDurabilityEvidence, false);
|
||
assert.equal(payload.readiness.durability.blockedLayer, "schema");
|
||
assert.equal(payload.readiness.durability.queryAttempted, true);
|
||
assert.equal(payload.readiness.durability.queryResult, "schema_blocked");
|
||
assert.ok(payload.blockerCodes.includes(RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED));
|
||
assert.equal(JSON.stringify(payload).includes("password"), false);
|
||
} finally {
|
||
if (originalUrl === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_URL;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
|
||
}
|
||
if (originalSslMode === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
|
||
}
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await new Promise((resolve, reject) => {
|
||
fakeDb.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api health keeps DB live evidence separate when durable adapter query is blocked", async () => {
|
||
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
|
||
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
const fakeDb = createTcpServer((socket) => socket.end());
|
||
await new Promise((resolve) => fakeDb.listen(0, "127.0.0.1", resolve));
|
||
const dbPort = fakeDb.address().port;
|
||
process.env.HWLAB_CLOUD_DB_URL = `postgres://hwlab_test:password@127.0.0.1:${dbPort}/hwlab`;
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
|
||
|
||
const server = createCloudApiServer({
|
||
runtimeStore: {
|
||
async readiness() {
|
||
return {
|
||
adapter: "postgres",
|
||
durable: false,
|
||
durableRequested: true,
|
||
durableCapable: false,
|
||
ready: false,
|
||
status: "blocked",
|
||
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
|
||
reason: "test runtime durable read query is blocked",
|
||
liveRuntimeEvidence: false,
|
||
fixtureEvidence: false,
|
||
connection: {
|
||
queryAttempted: true,
|
||
queryResult: "query_blocked",
|
||
endpointRedacted: true,
|
||
valueRedacted: true,
|
||
errorCode: "57014"
|
||
},
|
||
schema: {
|
||
checked: true,
|
||
ready: true,
|
||
missingTables: [],
|
||
missingColumns: []
|
||
},
|
||
migration: {
|
||
checked: true,
|
||
ready: true,
|
||
missing: false
|
||
},
|
||
gates: {
|
||
auth: { checked: true, ready: true, status: "ready", blocker: null },
|
||
schema: { checked: true, ready: true, status: "ready", blocker: null },
|
||
migration: { checked: true, ready: true, status: "ready", blocker: null },
|
||
durability: {
|
||
checked: true,
|
||
ready: false,
|
||
status: "blocked",
|
||
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED
|
||
}
|
||
},
|
||
counts: {}
|
||
};
|
||
}
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
const payload = await response.json();
|
||
assert.equal(response.status, 200);
|
||
assert.equal(payload.status, "degraded");
|
||
assert.equal(payload.db.ready, true);
|
||
assert.equal(payload.db.connected, true);
|
||
assert.equal(payload.db.liveDbEvidence, true);
|
||
assert.equal(payload.runtime.durable, false);
|
||
assert.equal(payload.runtime.ready, false);
|
||
assert.equal(payload.runtime.blocker, RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED);
|
||
assert.equal(payload.readiness.components.db, "ready");
|
||
assert.equal(payload.readiness.components.runtime, "blocked");
|
||
assert.equal(payload.readiness.durability.status, "blocked");
|
||
assert.equal(payload.readiness.durability.dbLiveEvidenceObserved, true);
|
||
assert.equal(payload.readiness.durability.dbLiveEvidenceIsDurabilityEvidence, false);
|
||
assert.equal(payload.readiness.durability.blockedLayer, "durability_query");
|
||
assert.equal(payload.readiness.durability.queryAttempted, true);
|
||
assert.equal(payload.readiness.durability.queryResult, "query_blocked");
|
||
assert.equal(payload.readiness.durability.liveRuntimeEvidence, false);
|
||
assert.ok(payload.blockerCodes.includes(RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED));
|
||
assert.equal(payload.readiness.dbDurable.status, "blocked");
|
||
assert.equal(payload.readiness.dbDurable.blocker, RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED);
|
||
assert.equal(payload.readiness.codeAgent.currentBlockers.includes(RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED), true);
|
||
assert.equal(JSON.stringify(payload).includes("password"), false);
|
||
assert.equal(JSON.stringify(payload).includes(String(dbPort)), false);
|
||
} finally {
|
||
if (originalUrl === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_URL;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
|
||
}
|
||
if (originalSslMode === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
|
||
}
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await new Promise((resolve, reject) => {
|
||
fakeDb.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api health contract distinguishes durable SSL, auth, schema, migration, and query blockers", async () => {
|
||
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
|
||
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
process.env.HWLAB_CLOUD_DB_URL = "redacted";
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = "require";
|
||
|
||
const cases = [
|
||
{
|
||
blocker: RUNTIME_DURABLE_ADAPTER_SSL_BLOCKED,
|
||
blockedLayer: "ssl",
|
||
queryResult: "ssl_negotiation_blocked",
|
||
gates: {
|
||
ssl: "blocked",
|
||
auth: "not_checked",
|
||
schema: "not_checked",
|
||
migration: "not_checked",
|
||
durability: "blocked"
|
||
}
|
||
},
|
||
{
|
||
blocker: RUNTIME_DURABLE_ADAPTER_AUTH_BLOCKED,
|
||
blockedLayer: "auth",
|
||
queryResult: "auth_blocked",
|
||
gates: {
|
||
ssl: "ready",
|
||
auth: "blocked",
|
||
schema: "not_checked",
|
||
migration: "not_checked",
|
||
durability: "blocked"
|
||
}
|
||
},
|
||
{
|
||
blocker: RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
|
||
blockedLayer: "schema",
|
||
queryResult: "schema_blocked",
|
||
gates: {
|
||
ssl: "ready",
|
||
auth: "ready",
|
||
schema: "blocked",
|
||
migration: "not_checked",
|
||
durability: "blocked"
|
||
}
|
||
},
|
||
{
|
||
blocker: RUNTIME_DURABLE_ADAPTER_MIGRATION_BLOCKED,
|
||
blockedLayer: "migration",
|
||
queryResult: "migration_blocked",
|
||
gates: {
|
||
ssl: "ready",
|
||
auth: "ready",
|
||
schema: "ready",
|
||
migration: "blocked",
|
||
durability: "blocked"
|
||
}
|
||
},
|
||
{
|
||
blocker: RUNTIME_DURABLE_ADAPTER_QUERY_BLOCKED,
|
||
blockedLayer: "durability_query",
|
||
queryResult: "query_blocked",
|
||
gates: {
|
||
ssl: "ready",
|
||
auth: "ready",
|
||
schema: "ready",
|
||
migration: "ready",
|
||
durability: "blocked"
|
||
}
|
||
}
|
||
];
|
||
|
||
try {
|
||
for (const testCase of cases) {
|
||
const server = createCloudApiServer({
|
||
dbProbe: {
|
||
probe: async ({ endpointSource, timeoutMs }) => ({
|
||
attempted: true,
|
||
networkAttempted: true,
|
||
endpointSource,
|
||
probeType: "tcp-connect",
|
||
endpointRedacted: true,
|
||
valueRedacted: true,
|
||
timeoutMs,
|
||
durationMs: 0,
|
||
result: "connected",
|
||
classification: "tcp_connected",
|
||
errorCode: null,
|
||
missingEnv: []
|
||
})
|
||
},
|
||
runtimeStore: {
|
||
async readiness() {
|
||
return durableBlockedRuntime(testCase);
|
||
}
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
const payload = await response.json();
|
||
assert.equal(response.status, 200);
|
||
assert.equal(payload.status, "degraded");
|
||
assert.equal(payload.db.ready, true);
|
||
assert.equal(payload.db.liveDbEvidence, true);
|
||
assert.equal(payload.runtime.blocker, testCase.blocker);
|
||
assert.equal(payload.runtime.connection.queryResult, testCase.queryResult);
|
||
assert.equal(payload.runtime.gates.ssl.status, testCase.gates.ssl);
|
||
assert.equal(payload.runtime.gates.auth.status, testCase.gates.auth);
|
||
assert.equal(payload.runtime.gates.schema.status, testCase.gates.schema);
|
||
assert.equal(payload.runtime.gates.migration.status, testCase.gates.migration);
|
||
assert.equal(payload.runtime.gates.durability.status, testCase.gates.durability);
|
||
assert.equal(payload.runtime.durabilityContract.blockedLayer, testCase.blockedLayer);
|
||
assert.equal(payload.db.runtimeReadiness.blockedLayer, testCase.blockedLayer);
|
||
assert.equal(payload.db.runtimeReadiness.queryResult, testCase.queryResult);
|
||
assert.equal(payload.db.runtimeReadiness.dbLiveEvidenceIsDurabilityEvidence, false);
|
||
assert.equal(payload.db.readinessLayers.ssl.status, layerStatus(testCase.gates.ssl));
|
||
assert.equal(payload.db.readinessLayers.auth.status, layerStatus(testCase.gates.auth));
|
||
assert.equal(payload.db.readinessLayers.schema.status, layerStatus(testCase.gates.schema));
|
||
assert.equal(payload.db.readinessLayers.migration.status, layerStatus(testCase.gates.migration));
|
||
assert.equal(payload.db.readinessLayers.durability.status, layerStatus(testCase.gates.durability));
|
||
assert.equal(payload.db.readinessLayers[layerForBlockedCase(testCase.blockedLayer)].result, testCase.queryResult);
|
||
assert.equal(payload.readiness.durability.blockedLayer, testCase.blockedLayer);
|
||
assert.equal(payload.readiness.durability.queryResult, testCase.queryResult);
|
||
assert.equal(payload.readiness.durability.secretMaterialRead, false);
|
||
assert.ok(payload.blockerCodes.includes(testCase.blocker));
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
}
|
||
} finally {
|
||
if (originalUrl === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_URL;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
|
||
}
|
||
if (originalSslMode === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
|
||
}
|
||
}
|
||
});
|
||
|
||
test("cloud api health does not treat DB env presence-only as live readiness", async () => {
|
||
const originalUrl = process.env.HWLAB_CLOUD_DB_URL;
|
||
const originalSslMode = process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
const originalProbeDisabled = process.env.HWLAB_CLOUD_DB_PROBE_DISABLED;
|
||
process.env.HWLAB_CLOUD_DB_URL = "postgres://user:password@db.example.invalid:5432/hwlab";
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = "disable";
|
||
delete process.env.HWLAB_CLOUD_DB_PROBE_DISABLED;
|
||
|
||
const server = createCloudApiServer();
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
const payload = await response.json();
|
||
assert.equal(response.status, 200);
|
||
assert.equal(payload.db.configReady, true);
|
||
assert.equal(payload.db.connected, false);
|
||
assert.equal(payload.db.liveConnected, false);
|
||
assert.equal(payload.db.liveDbEvidence, false);
|
||
assert.equal(payload.db.safety.liveDbEvidence, false);
|
||
assert.equal(payload.db.ready, false);
|
||
assert.equal(payload.db.connectionAttempted, true);
|
||
assert.equal(payload.db.connectionResult, "forbidden_runtime_host");
|
||
assert.equal(payload.db.evidence, "live_db_tcp_connection_blocked");
|
||
assert.equal(payload.db.endpointSource, "secret-url-host");
|
||
assert.equal(payload.db.connection.endpointSource, "secret-url-host");
|
||
assert.equal(payload.db.connection.networkAttempted, false);
|
||
assert.equal(payload.db.connection.classification, "db_url_forbidden_invalid_host");
|
||
assert.equal(payload.db.readinessLayers.dns.status, "not_proven");
|
||
assert.equal(JSON.stringify(payload.db).includes("password"), false);
|
||
assert.equal(JSON.stringify(payload.db).includes("db.example.invalid"), false);
|
||
} finally {
|
||
if (originalUrl === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_URL;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_URL = originalUrl;
|
||
}
|
||
if (originalSslMode === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_SSL_MODE;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_SSL_MODE = originalSslMode;
|
||
}
|
||
if (originalProbeDisabled === undefined) {
|
||
delete process.env.HWLAB_CLOUD_DB_PROBE_DISABLED;
|
||
} else {
|
||
process.env.HWLAB_CLOUD_DB_PROBE_DISABLED = originalProbeDisabled;
|
||
}
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
function durableBlockedRuntime({ blocker, blockedLayer, queryResult, gates }) {
|
||
return {
|
||
adapter: "postgres",
|
||
durable: false,
|
||
durableRequested: true,
|
||
durableCapable: false,
|
||
ready: false,
|
||
status: "blocked",
|
||
blocker,
|
||
reason: `test durable runtime blocker ${blocker}`,
|
||
liveRuntimeEvidence: false,
|
||
fixtureEvidence: false,
|
||
connection: {
|
||
queryAttempted: true,
|
||
queryResult,
|
||
endpointRedacted: true,
|
||
valueRedacted: true,
|
||
errorCode: "TEST_BLOCKER"
|
||
},
|
||
schema: {
|
||
checked: gates.schema !== "not_checked",
|
||
ready: gates.schema === "ready",
|
||
missingTables: gates.schema === "blocked" ? ["gateway_sessions"] : [],
|
||
missingColumns: gates.schema === "blocked" ? ["gateway_sessions.gateway_session_json"] : []
|
||
},
|
||
migration: {
|
||
checked: gates.migration !== "not_checked",
|
||
ready: gates.migration === "ready",
|
||
missing: gates.migration !== "ready"
|
||
},
|
||
gates: Object.fromEntries(Object.entries(gates).map(([name, status]) => [
|
||
name,
|
||
{
|
||
checked: status !== "not_checked",
|
||
ready: status === "ready",
|
||
status,
|
||
blocker: status === "blocked" ? blocker : null
|
||
}
|
||
])),
|
||
durabilityContract: {
|
||
ready: false,
|
||
status: "blocked",
|
||
requiredEvidence: "runtime_adapter_schema_migration_read_query",
|
||
dbLiveEvidenceIsDurabilityEvidence: false,
|
||
liveRuntimeEvidence: false,
|
||
adapterQueryRequired: true,
|
||
blockedLayer,
|
||
blocker,
|
||
secretMaterialRead: false
|
||
},
|
||
safety: {
|
||
secretMaterialRead: false,
|
||
valuesRedacted: true,
|
||
endpointRedacted: true
|
||
},
|
||
counts: {}
|
||
};
|
||
}
|
||
|
||
function durableReadyRuntime() {
|
||
return {
|
||
adapter: "postgres",
|
||
durable: true,
|
||
durableRequested: true,
|
||
durableCapable: true,
|
||
ready: true,
|
||
status: "ready",
|
||
blocker: null,
|
||
reason: "test durable runtime ready",
|
||
liveRuntimeEvidence: true,
|
||
fixtureEvidence: false,
|
||
connection: {
|
||
queryAttempted: true,
|
||
queryResult: "durable_readiness_ready",
|
||
endpointRedacted: true,
|
||
valueRedacted: true
|
||
},
|
||
schema: {
|
||
checked: true,
|
||
ready: true,
|
||
missingTables: [],
|
||
missingColumns: []
|
||
},
|
||
migration: {
|
||
checked: true,
|
||
ready: true,
|
||
missing: false
|
||
},
|
||
gates: Object.fromEntries(["ssl", "auth", "schema", "migration", "durability"].map((name) => [
|
||
name,
|
||
{
|
||
checked: true,
|
||
ready: true,
|
||
status: "ready",
|
||
blocker: null
|
||
}
|
||
])),
|
||
durabilityContract: {
|
||
ready: true,
|
||
status: "ready",
|
||
requiredEvidence: "runtime_adapter_schema_migration_read_query",
|
||
dbLiveEvidenceIsDurabilityEvidence: false,
|
||
liveRuntimeEvidence: true,
|
||
adapterQueryRequired: true,
|
||
blockedLayer: null,
|
||
blocker: null,
|
||
secretMaterialRead: false
|
||
},
|
||
safety: {
|
||
secretMaterialRead: false,
|
||
valuesRedacted: true,
|
||
endpointRedacted: true
|
||
},
|
||
counts: {}
|
||
};
|
||
}
|
||
|
||
function layerStatus(gateStatus) {
|
||
if (gateStatus === "ready") return "pass";
|
||
if (gateStatus === "blocked") return "blocked";
|
||
return "not_proven";
|
||
}
|
||
|
||
function layerForBlockedCase(blockedLayer) {
|
||
if (blockedLayer === "durability_query") return "durability";
|
||
return blockedLayer;
|
||
}
|
||
|
||
async function m3ReadinessRequestJson(url, request = {}) {
|
||
const parsed = new URL(url);
|
||
const body = request.body ?? {};
|
||
if (url.startsWith("http://gateway-1") && parsed.pathname === "/status") {
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: m3GatewayStatus({
|
||
gatewayId: "gwsimu_1",
|
||
gatewaySessionId: "gws_gwsimu_1",
|
||
boxId: "boxsimu_1",
|
||
resourceId: "res_boxsimu_1"
|
||
})
|
||
};
|
||
}
|
||
if (url.startsWith("http://gateway-1") && parsed.pathname === "/invoke") {
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: {
|
||
accepted: true,
|
||
operationId: "op_m3_status_do1",
|
||
traceId: "trc_m3_status_do1",
|
||
boxId: "boxsimu_1",
|
||
resourceId: "res_boxsimu_1",
|
||
port: "DO1",
|
||
value: false,
|
||
state: {
|
||
port: "DO1",
|
||
direction: "output",
|
||
value: false,
|
||
source: "gateway-simu",
|
||
updatedAt: "2026-05-23T00:00:00.000Z"
|
||
},
|
||
auditId: "aud_m3_status_do1",
|
||
evidenceId: "evd_m3_status_do1"
|
||
}
|
||
};
|
||
}
|
||
if (url.startsWith("http://gateway-2") && parsed.pathname === "/status") {
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: m3GatewayStatus({
|
||
gatewayId: "gwsimu_2",
|
||
gatewaySessionId: "gws_gwsimu_2",
|
||
boxId: "boxsimu_2",
|
||
resourceId: "res_boxsimu_2"
|
||
})
|
||
};
|
||
}
|
||
if (url.startsWith("http://gateway-2") && parsed.pathname === "/invoke") {
|
||
const value = request.body?.operationId?.includes("do_write") || request.body?.operationId?.includes("do-write")
|
||
? true
|
||
: false;
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: {
|
||
accepted: true,
|
||
operationId: "op_m3_status_di1",
|
||
traceId: "trc_m3_status_di1",
|
||
boxId: "boxsimu_2",
|
||
resourceId: "res_boxsimu_2",
|
||
port: "DI1",
|
||
value,
|
||
state: {
|
||
port: "DI1",
|
||
direction: "input",
|
||
value,
|
||
source: "patch-panel",
|
||
sourceResourceId: "res_boxsimu_1",
|
||
sourcePort: "DO1",
|
||
propagatedBy: "hwlab-patch-panel",
|
||
updatedAt: "2026-05-23T00:00:00.000Z"
|
||
},
|
||
auditId: "aud_m3_status_di1",
|
||
evidenceId: "evd_m3_status_di1"
|
||
}
|
||
};
|
||
}
|
||
if (url.startsWith("http://patch-panel") && parsed.pathname === "/status") {
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: {
|
||
serviceId: "hwlab-patch-panel",
|
||
state: "active",
|
||
activeConnections: [
|
||
{
|
||
fromResourceId: "res_boxsimu_1",
|
||
fromPort: "DO1",
|
||
toResourceId: "res_boxsimu_2",
|
||
toPort: "DI1"
|
||
}
|
||
]
|
||
}
|
||
};
|
||
}
|
||
if (url.startsWith("http://patch-panel") && parsed.pathname === "/wiring") {
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: {
|
||
wiringConfigId: "wir_m3_do1_di1",
|
||
status: "active",
|
||
connections: [
|
||
{
|
||
from: {
|
||
resourceId: "res_boxsimu_1",
|
||
port: "DO1"
|
||
},
|
||
to: {
|
||
resourceId: "res_boxsimu_2",
|
||
port: "DI1"
|
||
},
|
||
mode: "exclusive"
|
||
}
|
||
]
|
||
}
|
||
};
|
||
}
|
||
if (url.startsWith("http://patch-panel") && parsed.pathname === "/sync/tick") {
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: {
|
||
accepted: true,
|
||
propagatedBy: "hwlab-patch-panel",
|
||
routeCount: 1,
|
||
deliveryCount: 1,
|
||
routes: [
|
||
{
|
||
accepted: true,
|
||
deliveryCount: 1,
|
||
deliveries: [
|
||
{
|
||
resourceId: "res_boxsimu_2",
|
||
port: "DI1",
|
||
value: body.signals?.[0]?.value ?? false,
|
||
sourceResourceId: "res_boxsimu_1",
|
||
sourcePort: "DO1",
|
||
deliveryStatus: "applied"
|
||
}
|
||
]
|
||
}
|
||
]
|
||
}
|
||
};
|
||
}
|
||
throw new Error(`unexpected M3 readiness request ${url}`);
|
||
}
|
||
|
||
function m3GatewayStatus({ gatewayId, gatewaySessionId, boxId, resourceId }) {
|
||
return {
|
||
serviceId: "hwlab-gateway-simu",
|
||
gatewayId,
|
||
gatewaySessionId,
|
||
session: {
|
||
gatewayId,
|
||
gatewaySessionId
|
||
},
|
||
registry: {
|
||
gatewayId,
|
||
gatewaySessionId,
|
||
boxes: [
|
||
{
|
||
boxId,
|
||
resourceId,
|
||
state: "registered"
|
||
}
|
||
]
|
||
}
|
||
};
|
||
}
|
||
|
||
function codexStdioReadyFixture({ workspace, codexHome }) {
|
||
return {
|
||
kind: "codex-app-server-stdio-runner",
|
||
provider: "codex-stdio",
|
||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||
status: "feasible",
|
||
ready: true,
|
||
startupReady: true,
|
||
canStartLongLivedCodexStdio: true,
|
||
command: path.join(process.cwd(), "node_modules", ".bin", "codex"),
|
||
binaryOnPath: true,
|
||
binary: {
|
||
present: true,
|
||
executable: true,
|
||
nativeDependencyPresent: true,
|
||
versionDetected: true
|
||
},
|
||
workspace,
|
||
workspaceState: { exists: true, readable: true, writable: true, writeRequired: true },
|
||
codexHome,
|
||
codexHomeState: { exists: true, readable: true, writable: true, writeRequired: true },
|
||
sandbox: "workspace-write",
|
||
enabled: true,
|
||
supervisor: { configured: true, mode: "repo-owned-node-supervisor" },
|
||
tokenBoundary: { present: true, secretMaterialRead: false, valuesRedacted: true },
|
||
egress: { configured: true, directPublicOpenAi: false, valueRedacted: true },
|
||
protocol: {
|
||
status: "wired",
|
||
wired: true,
|
||
protocolVersion: "codex-app-server-jsonrpc-stdio",
|
||
requiredMethods: ["initialize", "thread/start", "thread/resume", "turn/start"],
|
||
toolsObserved: [],
|
||
missingTools: [],
|
||
command: "codex app-server --listen stdio://"
|
||
},
|
||
commandProbe: {
|
||
status: "ready",
|
||
ready: true,
|
||
probe: "workspace.pwd",
|
||
traceId: "trc_codex_stdio_command_probe",
|
||
toolCalls: [{
|
||
name: "pwd",
|
||
status: "completed",
|
||
cwd: workspace,
|
||
command: "pwd",
|
||
exitCode: 0,
|
||
stdoutSummary: "workspace path matched",
|
||
outputTruncated: false
|
||
}],
|
||
secretMaterialRead: false,
|
||
valuesRedacted: true
|
||
},
|
||
stdioProtocol: {
|
||
status: "wired",
|
||
wired: true,
|
||
protocolVersion: "codex-app-server-jsonrpc-stdio",
|
||
requiredMethods: ["initialize", "thread/start", "thread/resume", "turn/start"],
|
||
toolsObserved: [],
|
||
missingTools: [],
|
||
command: "codex app-server --listen stdio://"
|
||
},
|
||
sessionLifecycle: {
|
||
status: "present",
|
||
present: true,
|
||
create: true,
|
||
reuse: true,
|
||
cancel: true,
|
||
reap: true,
|
||
traceCapture: true,
|
||
idleTimeoutMs: 1800000
|
||
},
|
||
lifecycleSupervisor: {
|
||
status: "present",
|
||
present: true,
|
||
create: true,
|
||
reuse: true,
|
||
cancel: true,
|
||
reap: true,
|
||
traceCapture: true,
|
||
idleTimeoutMs: 1800000
|
||
},
|
||
runtimeContract: {
|
||
status: "ready",
|
||
ready: true,
|
||
binary: {
|
||
status: "present",
|
||
present: true,
|
||
executable: true,
|
||
nativeDependencyPresent: true,
|
||
versionDetected: true
|
||
},
|
||
stdioProtocol: {
|
||
status: "wired",
|
||
wired: true,
|
||
protocolVersion: "codex-app-server-jsonrpc-stdio",
|
||
requiredMethods: ["initialize", "thread/start", "thread/resume", "turn/start"],
|
||
toolsObserved: [],
|
||
missingTools: []
|
||
},
|
||
commandProbe: {
|
||
status: "ready",
|
||
ready: true,
|
||
probe: "workspace.pwd",
|
||
traceId: "trc_codex_stdio_command_probe",
|
||
toolCalls: [{
|
||
name: "pwd",
|
||
status: "completed",
|
||
cwd: workspace,
|
||
command: "pwd",
|
||
exitCode: 0,
|
||
stdoutSummary: "workspace path matched",
|
||
outputTruncated: false
|
||
}],
|
||
secretMaterialRead: false,
|
||
valuesRedacted: true
|
||
},
|
||
lifecycleSupervisor: {
|
||
status: "present",
|
||
present: true,
|
||
create: true,
|
||
reuse: true,
|
||
cancel: true,
|
||
reap: true,
|
||
traceCapture: true,
|
||
idleTimeoutMs: 1800000
|
||
},
|
||
workspaceMount: {
|
||
path: workspace,
|
||
status: "ready",
|
||
mounted: true,
|
||
readable: true,
|
||
writable: true,
|
||
sandbox: "workspace-write"
|
||
},
|
||
codexHome: {
|
||
path: codexHome,
|
||
status: "ready",
|
||
exists: true,
|
||
readable: true,
|
||
writable: true
|
||
},
|
||
cancelReapTraceReadiness: {
|
||
status: "ready",
|
||
cancel: true,
|
||
reap: true,
|
||
traceCapture: true,
|
||
idleTimeout: true
|
||
}
|
||
},
|
||
blockers: [],
|
||
blockerCodes: [],
|
||
safety: { secretMaterialRead: false, valuesRedacted: true }
|
||
};
|
||
}
|
||
|
||
function codexStdioChatFixture({ workspace, codexHome, params }) {
|
||
const traceId = params.traceId;
|
||
const session = {
|
||
sessionId: "ses_server_stdio_pwd",
|
||
conversationId: params.conversationId,
|
||
status: "idle",
|
||
workspace,
|
||
sandbox: "workspace-write",
|
||
runnerKind: "codex-app-server-stdio-runner",
|
||
sessionMode: "codex-app-server-stdio-long-lived",
|
||
capabilityLevel: "long-lived-codex-stdio-session",
|
||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||
createdAt: "2026-05-23T00:00:00.000Z",
|
||
updatedAt: "2026-05-23T00:00:00.000Z",
|
||
idleTimeoutMs: 1800000,
|
||
expiresAt: "2026-05-23T00:30:00.000Z",
|
||
lastTraceId: traceId,
|
||
turn: 1,
|
||
threadId: "thread_server_stdio_pwd",
|
||
reused: false,
|
||
durable: true,
|
||
longLivedSession: true,
|
||
codexStdio: true,
|
||
writeCapable: true,
|
||
secretMaterialStored: false,
|
||
valuesRedacted: true
|
||
};
|
||
const feasibility = codexStdioReadyFixture({ workspace, codexHome });
|
||
return {
|
||
provider: "codex-stdio",
|
||
model: "gpt-test",
|
||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||
content: `stdio pwd\n${workspace}`,
|
||
workspace,
|
||
sandbox: "workspace-write",
|
||
session,
|
||
sessionMode: "codex-app-server-stdio-long-lived",
|
||
sessionReuse: {
|
||
conversationId: params.conversationId,
|
||
sessionId: session.sessionId,
|
||
threadId: session.threadId,
|
||
mapped: true,
|
||
reused: false,
|
||
turn: 1,
|
||
previousTurns: 0,
|
||
workspace,
|
||
status: "idle",
|
||
idleTimeoutMs: 1800000
|
||
},
|
||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
|
||
codexStdioFeasibility: feasibility,
|
||
longLivedSessionGate: {
|
||
status: "pass",
|
||
pass: true,
|
||
provider: "codex-stdio",
|
||
runnerKind: "codex-app-server-stdio-runner",
|
||
sessionMode: "codex-app-server-stdio-long-lived",
|
||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||
blockers: []
|
||
},
|
||
toolCalls: [{
|
||
id: "tool_server_stdio_pwd",
|
||
type: "workspace-read",
|
||
name: "pwd",
|
||
status: "completed",
|
||
cwd: workspace,
|
||
command: "pwd",
|
||
exitCode: 0,
|
||
stdout: workspace,
|
||
stderrSummary: "",
|
||
outputTruncated: false,
|
||
traceId
|
||
}],
|
||
skills: {
|
||
status: "not_requested",
|
||
items: [],
|
||
count: 0,
|
||
blockers: []
|
||
},
|
||
runner: {
|
||
kind: "codex-app-server-stdio-runner",
|
||
provider: "codex-stdio",
|
||
backend: "hwlab-cloud-api/codex-app-server-stdio",
|
||
workspace,
|
||
sandbox: "workspace-write",
|
||
session: "codex-app-server-stdio-long-lived",
|
||
sessionMode: "codex-app-server-stdio-long-lived",
|
||
sessionId: session.sessionId,
|
||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||
codexStdio: true,
|
||
longLivedSession: true,
|
||
durableSession: true,
|
||
writeCapable: true,
|
||
readOnly: false,
|
||
capabilityLevel: "long-lived-codex-stdio-session"
|
||
},
|
||
runnerTrace: {
|
||
traceId,
|
||
runnerKind: "codex-app-server-stdio-runner",
|
||
workspace,
|
||
sandbox: "workspace-write",
|
||
sessionMode: "codex-app-server-stdio-long-lived",
|
||
sessionId: session.sessionId,
|
||
sessionStatus: "idle",
|
||
turn: 1,
|
||
implementationType: "repo-owned-codex-app-server-stdio-session",
|
||
events: ["stdio:acquire", "session:created", "stdio:ready", "tool:pwd:completed"],
|
||
outputTruncated: false,
|
||
valuesPrinted: false
|
||
},
|
||
capabilityLevel: "long-lived-codex-stdio-session",
|
||
providerTrace: {
|
||
transport: "stdio",
|
||
protocol: "codex-app-server-jsonrpc-stdio",
|
||
command: "codex app-server --listen stdio://",
|
||
toolName: "codex-app-server.thread/start+turn/start",
|
||
threadId: "thread_server_stdio_pwd",
|
||
valuesPrinted: false
|
||
}
|
||
};
|
||
}
|
||
|
||
test("cloud api /v1 describes Code Agent provider blocker without leaking secret values", async () => {
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: "",
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex"
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1`);
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.codeAgent.endpoint, "POST /v1/agent/chat");
|
||
assert.equal(payload.codeAgent.provider, "codex-stdio");
|
||
assert.equal(payload.codeAgent.model, "gpt-test");
|
||
assert.equal(payload.codeAgent.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.codeAgent.mode, "codex-stdio");
|
||
assert.equal(payload.codeAgent.status, "blocked");
|
||
assert.match(payload.codeAgent.blocker, /Codex CLI command/u);
|
||
assert.equal(payload.codeAgent.reason, "codex_cli_binary_missing");
|
||
assert.equal(payload.codeAgent.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.codeAgent.runner.ready, false);
|
||
assert.equal(payload.codeAgent.capabilityLevel, "blocked");
|
||
assert.equal(payload.codeAgent.sessionRegistry.status, "available");
|
||
assert.equal(payload.codeAgent.longLivedSessionGate.status, "blocked");
|
||
assert.deepEqual(payload.codeAgent.missingEnv, []);
|
||
assert.equal(payload.codeAgent.secretRefs[0].env, "OPENAI_API_KEY");
|
||
assert.equal(payload.codeAgent.secretRefs[0].secretName, "hwlab-code-agent-provider");
|
||
assert.equal(payload.codeAgent.secretRefs[0].secretKey, "openai-api-key");
|
||
assert.equal(payload.codeAgent.secretRefs[0].redacted, true);
|
||
assert.equal(payload.codeAgent.ready, false);
|
||
assert.equal(payload.codeAgent.partialReady, false);
|
||
assert.equal(payload.codeAgent.egress.present, false);
|
||
assert.equal(payload.codeAgent.egress.valueRedacted, true);
|
||
assert.equal(payload.codeAgent.safety.secretMaterialRead, false);
|
||
assert.match(payload.codeAgent.summary, /will not use controlled-readonly-session-registry/u);
|
||
assert.match(payload.codeAgent.summary, /Codex stdio long-lived session is blocked/u);
|
||
assert.equal(JSON.stringify(payload).includes("sk-"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1 describes Code Agent egress blocker without leaking API key", async () => {
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "https://api.openai.com/v1/responses"
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1`);
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.codeAgent.status, "blocked");
|
||
assert.equal(payload.codeAgent.egress.directPublicOpenAi, true);
|
||
assert.match(payload.codeAgent.blocker, /Codex stdio|long-lived|Codex CLI command/u);
|
||
assert.equal(payload.codeAgent.runner.ready, false);
|
||
assert.equal(payload.codeAgent.safety.secretMaterialRead, false);
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api health reports provider, durable DB, and codex stdio ready when runtime contract passes", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-health-codex-stdio-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const manager = createCodexStdioSessionManager({
|
||
createRpcClient: async () => createFakeAppServerClient()
|
||
});
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:17680/v1/responses",
|
||
HWLAB_CLOUD_DB_URL: "postgres://hwlab_test:password@db.internal.local:5432/hwlab",
|
||
HWLAB_CLOUD_DB_SSL_MODE: "disable"
|
||
},
|
||
dbProbe: {
|
||
probe: async ({ endpointSource, timeoutMs }) => ({
|
||
attempted: true,
|
||
networkAttempted: true,
|
||
endpointSource,
|
||
probeType: "tcp-connect",
|
||
endpointRedacted: true,
|
||
valueRedacted: true,
|
||
timeoutMs,
|
||
durationMs: 1,
|
||
result: "connected",
|
||
classification: "tcp_connected",
|
||
errorCode: null,
|
||
missingEnv: []
|
||
})
|
||
},
|
||
runtimeStore: {
|
||
async readiness() {
|
||
return durableReadyRuntime();
|
||
}
|
||
},
|
||
codexStdioManager: manager
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "ok");
|
||
assert.equal(payload.db.ready, true);
|
||
assert.equal(payload.db.liveDbEvidence, true);
|
||
assert.equal(payload.runtime.ready, true);
|
||
assert.equal(payload.runtime.durable, true);
|
||
assert.equal(payload.readiness.provider.status, "ready");
|
||
assert.equal(payload.readiness.provider.ready, true);
|
||
assert.equal(payload.readiness.provider.blocker, null);
|
||
assert.equal(payload.readiness.dbDurable.status, "ready");
|
||
assert.equal(payload.readiness.dbDurable.ready, true);
|
||
assert.equal(payload.readiness.sessionRunner.status, "codex_stdio_ready");
|
||
assert.equal(payload.readiness.sessionRunner.ready, true);
|
||
assert.equal(payload.readiness.sessionRunner.codexStdio, true);
|
||
assert.equal(payload.readiness.sessionRunner.writeCapable, true);
|
||
assert.equal(payload.readiness.codexStdio.status, "ready");
|
||
assert.equal(payload.readiness.codexStdio.ready, true);
|
||
assert.deepEqual(payload.readiness.codeAgent.currentBlockers, []);
|
||
assert.equal(payload.readiness.codeAgent.providerReady, true);
|
||
assert.equal(payload.readiness.codeAgent.durableDbReady, true);
|
||
assert.equal(payload.readiness.codeAgent.sessionRunnerReady, true);
|
||
assert.equal(payload.readiness.codeAgent.codexStdioFeasible, true);
|
||
assert.equal(payload.codeAgent.ready, true);
|
||
assert.equal(payload.codeAgent.status, "codex-stdio-feasible");
|
||
assert.equal(payload.codeAgent.longLivedSessionGate.status, "pass");
|
||
assert.equal(payload.codeAgent.codexStdio.runtimeContract.binary.status, "present");
|
||
assert.deepEqual(payload.codeAgent.codexStdio.protocol.toolsObserved, []);
|
||
assert.deepEqual(payload.codeAgent.codexStdio.protocol.requiredMethods, ["initialize", "thread/start", "thread/resume", "turn/start"]);
|
||
assert.equal(payload.codeAgent.codexStdio.commandProbe.ready, true);
|
||
assert.equal(payload.codeAgent.codexStdio.runtimeContract.commandProbe.ready, true);
|
||
assert.equal(payload.readiness.codexStdio.commandProbeReady, true);
|
||
assert.equal(JSON.stringify(payload).includes("provider_unavailable"), false);
|
||
assert.equal(JSON.stringify(payload).includes("dns_resolution_failed"), false);
|
||
assert.equal(JSON.stringify(payload).includes("runtime_durable_adapter_auth_blocked"), false);
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
assert.equal(JSON.stringify(payload).includes("password"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api health blocks full Code Agent readiness when Codex stdio command probe fails", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-health-codex-stdio-probe-fail-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const workspaceFile = path.join(workspace, "workspace-file");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
await writeFile(workspaceFile, "not a directory\n", "utf8");
|
||
const manager = createCodexStdioSessionManager({
|
||
createRpcClient: async () => createFakeAppServerClient()
|
||
});
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspaceFile,
|
||
HWLAB_CODE_AGENT_WORKSPACE: workspaceFile,
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:17680/v1/responses",
|
||
HWLAB_CLOUD_DB_URL: "postgres://hwlab_test:password@db.internal.local:5432/hwlab",
|
||
HWLAB_CLOUD_DB_SSL_MODE: "disable"
|
||
},
|
||
dbProbe: {
|
||
probe: async ({ endpointSource, timeoutMs }) => ({
|
||
attempted: true,
|
||
networkAttempted: true,
|
||
endpointSource,
|
||
probeType: "tcp-connect",
|
||
endpointRedacted: true,
|
||
valueRedacted: true,
|
||
timeoutMs,
|
||
durationMs: 1,
|
||
result: "connected",
|
||
classification: "tcp_connected",
|
||
errorCode: null,
|
||
missingEnv: []
|
||
})
|
||
},
|
||
runtimeStore: {
|
||
async readiness() {
|
||
return durableReadyRuntime();
|
||
}
|
||
},
|
||
codexStdioManager: manager
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "degraded");
|
||
assert.equal(payload.ready, false);
|
||
assert.equal(payload.readiness.components.codeAgent, "blocked");
|
||
assert.equal(payload.readiness.codexStdio.status, "blocked");
|
||
assert.equal(payload.readiness.codexStdio.ready, false);
|
||
assert.equal(payload.readiness.codexStdio.commandProbeReady, false);
|
||
assert.ok(payload.readiness.codexStdio.blockerCodes.includes("codex_stdio_command_probe_failed"));
|
||
assert.ok(payload.blockerCodes.includes("codex_stdio_command_probe_failed"));
|
||
assert.equal(payload.codeAgent.ready, false);
|
||
assert.equal(payload.codeAgent.codexStdio.commandProbe.ready, false);
|
||
assert.equal(payload.codeAgent.codexStdio.commandProbe.status, "blocked");
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
assert.equal(JSON.stringify(payload).includes("password"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat refuses provider stub when Codex stdio is unavailable", async () => {
|
||
let providerCalled = false;
|
||
const server = createCloudApiServer({
|
||
callCodeAgentProvider: async () => {
|
||
providerCalled = true;
|
||
throw new Error("provider stub must not be used");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-agent-chat"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-agent-chat",
|
||
message: "请用一句话说明当前 HWLAB 工作台可以做什么。"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.conversationId, "cnv_server-test-agent-chat");
|
||
assert.equal(payload.sessionId, "cnv_server-test-agent-chat");
|
||
assert.match(payload.messageId, /^msg_/);
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.traceId, "trc_server-test-agent-chat");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.model.length > 0, true);
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(Number.isNaN(Date.parse(payload.createdAt)), false);
|
||
assert.equal(Number.isNaN(Date.parse(payload.updatedAt)), false);
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.equal(payload.error.layer, "runner");
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
assert.equal(providerCalled, false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api REST JSON-RPC bridge exposes unknown serviceId reason for Code Agent internal calls", async () => {
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio"
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/rpc/hardware.invoke.shell`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-invalid-service-id",
|
||
"x-source-service-id": "hwlab-code-agent-hotfix"
|
||
},
|
||
body: JSON.stringify({
|
||
projectId: "prj_mvp_topology",
|
||
gatewaySessionId: "gws_DESKTOP-1MHOD9I",
|
||
resourceId: "res_windows_host",
|
||
capabilityId: "cap_windows_cmd_exec",
|
||
input: {
|
||
command: "cmd /c echo should-not-dispatch"
|
||
}
|
||
})
|
||
});
|
||
assert.equal(response.status, 400);
|
||
const payload = await response.json();
|
||
assert.equal(payload.error.code, -32600);
|
||
assert.equal(payload.error.message, "Invalid JSON-RPC request");
|
||
assert.match(payload.error.data.reason, /unknown serviceId "hwlab-code-agent-hotfix"/u);
|
||
assert.equal(JSON.stringify(payload).includes("should-not-dispatch"), false);
|
||
assert.equal(JSON.stringify(payload).includes("sk-"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("OpenAI fallback text chat is not used when Codex stdio is unavailable", async () => {
|
||
let providerCalled = false;
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
callCodeAgentProvider: async () => {
|
||
providerCalled = true;
|
||
throw new Error("OpenAI fallback must not be used");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server_openai_fallback_not_runner",
|
||
traceId: "trc_server_openai_fallback_not_runner",
|
||
message: "请用一句话说明当前 HWLAB 工作台可以做什么。"
|
||
});
|
||
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.runner.codexStdio, true);
|
||
assert.equal(payload.longLivedSessionGate.status, "blocked");
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
assert.equal(providerCalled, false);
|
||
const capability = classifyCodexRunnerCapability(payload, { httpStatus: 200 });
|
||
assert.equal(capability.capabilityPass, false);
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api health does not pass long-lived session gate for explicit OpenAI fallback even if Codex stdio is feasible", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-openai-health-"));
|
||
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-openai-health-codex-home-"));
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: {
|
||
describe() {
|
||
return codexStdioReadyFixture({ workspace, codexHome });
|
||
},
|
||
async probe() {
|
||
return codexStdioReadyFixture({ workspace, codexHome });
|
||
},
|
||
async chat(params = {}) {
|
||
return codexStdioChatFixture({ workspace, codexHome, params });
|
||
},
|
||
cancel() {},
|
||
reapIdle() {}
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.codeAgent.provider, "openai-responses");
|
||
assert.equal(payload.codeAgent.mode, "openai");
|
||
assert.equal(payload.codeAgent.ready, false);
|
||
assert.equal(payload.codeAgent.capabilityLevel, "blocked");
|
||
assert.equal(payload.codeAgent.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.codeAgent.runner.codexStdio, true);
|
||
assert.equal(payload.codeAgent.longLivedSessionGate.status, "blocked");
|
||
assert.equal(payload.readiness.sessionRunner.status, "codex_stdio_ready");
|
||
assert.equal(payload.readiness.codeAgent.ready, false);
|
||
assert.equal(payload.readiness.codeAgent.provider.provider, "openai-responses");
|
||
assert.equal(payload.readiness.codeAgent.sessionRunner.codexStdio, true);
|
||
assert.equal(JSON.stringify(payload.codeAgent).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(codexHome, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat parse and params errors use structured blocker envelope", async () => {
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test"
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const parse = await postAgentRaw(port, "{", { "x-trace-id": "trc_parse_error" });
|
||
assert.equal(parse.status, 400);
|
||
assert.equal(parse.body.status, "failed");
|
||
assert.equal(parse.body.error.code, "parse_error");
|
||
assert.equal(parse.body.error.layer, "api");
|
||
assert.equal(parse.body.error.retryable, true);
|
||
assert.equal(parse.body.error.traceId, "trc_parse_error");
|
||
assert.equal(parse.body.error.route, "/v1/agent/chat");
|
||
assert.match(parse.body.error.userMessage, /JSON/u);
|
||
assert.equal(Object.hasOwn(parse.body, "reply"), false);
|
||
assert.equal(JSON.stringify(parse.body).includes("sk-"), false);
|
||
|
||
const invalid = await postAgentRaw(port, JSON.stringify([]), { "x-trace-id": "trc_invalid_params" });
|
||
assert.equal(invalid.status, 400);
|
||
assert.equal(invalid.body.error.code, "invalid_params");
|
||
assert.equal(invalid.body.error.layer, "api");
|
||
assert.equal(invalid.body.error.retryable, true);
|
||
assert.equal(invalid.body.error.blocker.code, "invalid_params");
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat runs Codex stdio pwd with session and workspace evidence", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-workspace-"));
|
||
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-codex-home-"));
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: {
|
||
describe() {
|
||
return codexStdioReadyFixture({ workspace, codexHome });
|
||
},
|
||
async probe() {
|
||
return codexStdioReadyFixture({ workspace, codexHome });
|
||
},
|
||
async chat(params = {}) {
|
||
return codexStdioChatFixture({ workspace, codexHome, params });
|
||
},
|
||
cancel() {},
|
||
reapIdle() {}
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-runner-pwd"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-runner-pwd",
|
||
message: "用pwd列出你当前的工作目录"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
|
||
assert.equal(payload.workspace, workspace);
|
||
assert.equal(payload.sandbox, "workspace-write");
|
||
assert.equal(payload.session.status, "idle");
|
||
assert.equal(payload.session.workspace, workspace);
|
||
assert.equal(payload.session.sandbox, "workspace-write");
|
||
assert.equal(payload.session.lastTraceId, "trc_server-test-runner-pwd");
|
||
assert.equal(typeof payload.session.idleTimeoutMs, "number");
|
||
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.runner.longLivedSession, true);
|
||
assert.equal(payload.runner.codexStdio, true);
|
||
assert.equal(payload.runner.writeCapable, true);
|
||
assert.equal(payload.runner.durableSession, true);
|
||
assert.equal(payload.runner.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.equal(payload.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.equal(payload.implementationType, "repo-owned-codex-app-server-stdio-session");
|
||
assert.equal(payload.sessionReuse.reused, false);
|
||
assert.equal(payload.sessionReuse.turn, 1);
|
||
assert.equal(payload.sessionReuse.status, "idle");
|
||
assert.ok(payload.runnerLimitations.includes("hardware-control-via-cloud-api-only"));
|
||
assert.equal(payload.codexStdioFeasibility.ready, true);
|
||
assert.equal(payload.codexStdioFeasibility.runtimeContract.stdioProtocol.status, "wired");
|
||
assert.equal(payload.codexStdioFeasibility.runtimeContract.lifecycleSupervisor.status, "present");
|
||
assert.equal(payload.longLivedSessionGate.status, "pass");
|
||
assert.equal(payload.longLivedSessionGate.pass, true);
|
||
assert.equal(payload.toolCalls[0].name, "pwd");
|
||
assert.equal(payload.toolCalls[0].status, "completed");
|
||
assert.equal(payload.toolCalls[0].stdout, workspace);
|
||
assert.equal(payload.skills.status, "not_requested");
|
||
assert.equal(payload.runnerTrace.runnerKind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.runnerTrace.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.match(payload.reply.content, new RegExp(workspace.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")));
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat supports short submit and result polling", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-short-"));
|
||
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-short-codex-home-"));
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "danger-full-access",
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: {
|
||
describe() {
|
||
return {
|
||
...codexStdioReadyFixture({ workspace, codexHome }),
|
||
sandbox: "danger-full-access"
|
||
};
|
||
},
|
||
async probe() {
|
||
return {
|
||
...codexStdioReadyFixture({ workspace, codexHome }),
|
||
sandbox: "danger-full-access"
|
||
};
|
||
},
|
||
async chat(params = {}) {
|
||
return {
|
||
...codexStdioChatFixture({ workspace, codexHome, params }),
|
||
sandbox: "danger-full-access",
|
||
session: {
|
||
...codexStdioChatFixture({ workspace, codexHome, params }).session,
|
||
sandbox: "danger-full-access"
|
||
}
|
||
};
|
||
},
|
||
cancel() {},
|
||
reapIdle() {}
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const traceId = "trc_server-test-short-submit";
|
||
const submit = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": traceId,
|
||
"prefer": "respond-async",
|
||
"x-hwlab-short-connection": "1"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-short-submit",
|
||
message: "用pwd列出你当前的工作目录"
|
||
})
|
||
});
|
||
assert.equal(submit.status, 202);
|
||
const accepted = await submit.json();
|
||
assert.equal(accepted.accepted, true);
|
||
assert.equal(accepted.shortConnection, true);
|
||
assert.equal(accepted.traceId, traceId);
|
||
assert.equal(accepted.resultUrl, `/v1/agent/chat/result/${traceId}`);
|
||
|
||
const payload = await pollAgentResult(port, traceId);
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.traceId, traceId);
|
||
assert.equal(payload.sandbox, "danger-full-access");
|
||
assert.match(payload.reply.content, new RegExp(workspace.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")));
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(codexHome, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api health reports Codex stdio runner facts without readonly limitations", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-health-stdio-"));
|
||
const codexHome = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-health-codex-home-"));
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: {
|
||
describe() {
|
||
return codexStdioReadyFixture({ workspace, codexHome });
|
||
},
|
||
async probe() {
|
||
return codexStdioReadyFixture({ workspace, codexHome });
|
||
},
|
||
async chat(params = {}) {
|
||
return codexStdioChatFixture({ workspace, codexHome, params });
|
||
},
|
||
cancel() {},
|
||
reapIdle() {}
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/health/live`);
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.codeAgent.status, "codex-stdio-feasible");
|
||
assert.equal(payload.codeAgent.ready, true);
|
||
assert.equal(payload.codeAgent.provider, "codex-stdio");
|
||
assert.equal(payload.codeAgent.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.codeAgent.runner.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.equal(payload.codeAgent.runner.codexStdio, true);
|
||
assert.equal(payload.codeAgent.runner.writeCapable, true);
|
||
assert.equal(payload.codeAgent.runner.durableSession, true);
|
||
assert.equal(payload.codeAgent.workspace, workspace);
|
||
assert.equal(payload.codeAgent.sandbox, "workspace-write");
|
||
assert.equal(payload.codeAgent.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.equal(payload.codeAgent.sessionRegistry.kind, "codex-stdio-session-registry");
|
||
assert.deepEqual(payload.codeAgent.sessionRegistry.statuses, ["creating", "ready", "busy", "idle", "timeout", "error", "canceled", "interrupted", "expired", "failed"]);
|
||
assert.equal(payload.codeAgent.capabilityLevel, "long-lived-codex-stdio-session");
|
||
assert.equal(payload.codeAgent.longLivedSessionGate.status, "pass");
|
||
assert.equal(payload.readiness.sessionRunner.status, "codex_stdio_ready");
|
||
assert.equal(payload.readiness.sessionRunner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.readiness.sessionRunner.codexStdio, true);
|
||
assert.equal(payload.readiness.sessionRunner.writeCapable, true);
|
||
assert.equal(payload.readiness.sessionRunner.capabilityLevel, "long-lived-codex-stdio-session");
|
||
assert.equal(payload.readiness.codeAgent.sessionRunner.codexStdio, true);
|
||
assert.equal(payload.readiness.codeAgent.sessionRunner.writeCapable, true);
|
||
assert.deepEqual(payload.codeAgent.runnerLimitations, [
|
||
"hardware-control-via-cloud-api-only",
|
||
"no-direct-gateway-link",
|
||
"no-direct-box-simu-link",
|
||
"no-direct-patch-panel-link",
|
||
"secret-values-redacted"
|
||
]);
|
||
const serialized = JSON.stringify(payload.codeAgent);
|
||
assert.equal(serialized.includes("not-codex-stdio"), false);
|
||
assert.equal(serialized.includes("not-write-capable"), false);
|
||
assert.equal(serialized.includes("process-local-session-registry"), false);
|
||
assert.equal(serialized.includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(codexHome, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat sends pwd prompt through real Codex stdio instead of local sidecar", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-pwd-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const codexAppServerCalls = [];
|
||
const manager = createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_pwd_real",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
calls: codexAppServerCalls,
|
||
text: `真实 Codex stdio 回复:当前工作目录是 ${workspace}`
|
||
})
|
||
});
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: manager,
|
||
callCodeAgentProvider: async () => {
|
||
throw new Error("OpenAI fallback must not handle Codex stdio pwd");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server_stdio_pwd_sidecar",
|
||
traceId: "trc_server_stdio_pwd_sidecar",
|
||
message: "用pwd列出你当前的工作目录"
|
||
});
|
||
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
|
||
assert.equal(payload.workspace, workspace);
|
||
assert.equal(payload.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.equal(payload.longLivedSessionGate.status, "pass");
|
||
assert.equal(payload.providerTrace.sidecarOnly, undefined);
|
||
assert.equal(payload.providerTrace.toolName, "codex-app-server.thread/start+turn/start");
|
||
assert.deepEqual(codexAppServerCalls.filter((call) => call.method !== "initialize").map((call) => call.method), ["thread/start", "turn/start"]);
|
||
assert.match(codexAppServerCalls.find((call) => call.method === "turn/start")?.args?.prompt ?? "", /用pwd列出/u);
|
||
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "codex-app-server.thread/start+turn/start" && toolCall.status === "completed"));
|
||
assert.match(payload.reply.content, /真实 Codex stdio 回复/u);
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "prompt:sent"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "tool:codex-app-server.thread/start+turn/start:started"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "assistant:completed"));
|
||
assert.equal(JSON.stringify(payload).includes("openai-responses-fallback"), false);
|
||
assert.equal(JSON.stringify(payload).includes("text-chat-only"), false);
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat answers skills prompt through real Codex stdio and retains trace", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-skills-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const skillsDir = path.join(workspace, "skills");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await mkdir(path.join(skillsDir, "hwlab-test-skill"), { recursive: true });
|
||
await prepareFakeCodexHome(codexHome);
|
||
await writeFile(path.join(skillsDir, "hwlab-test-skill", "SKILL.md"), [
|
||
"---",
|
||
"name: hwlab-test-skill",
|
||
"description: Test skill mounted for Codex stdio discovery.",
|
||
"version: v-test",
|
||
"---",
|
||
"",
|
||
"# HWLAB Test Skill"
|
||
].join("\n"));
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_SKILLS_DIRS: skillsDir,
|
||
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_skills",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
text: "模型泛化回答不应成为 skills 最终回复。"
|
||
})
|
||
})
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server_stdio_skills",
|
||
traceId: "trc_server_stdio_skills",
|
||
message: "列出你可用的skills"
|
||
});
|
||
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
|
||
assert.equal(payload.providerTrace.sidecarOnly, undefined);
|
||
assert.equal(payload.providerTrace.toolName, "codex-app-server.thread/start+turn/start");
|
||
assert.equal(payload.skills.status, "ready");
|
||
const discovered = payload.skills.items.find((skill) => skill.name === "hwlab-test-skill");
|
||
assert.ok(discovered);
|
||
assert.equal(discovered.source, path.join(skillsDir, "hwlab-test-skill", "SKILL.md"));
|
||
assert.equal(discovered.manifest.path, path.join(skillsDir, "hwlab-test-skill", "SKILL.md"));
|
||
assert.equal(discovered.version, "v-test");
|
||
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "completed"));
|
||
assert.match(payload.reply.content, /hwlab-test-skill/u);
|
||
assert.match(payload.reply.content, /真实 skill manifest 清单/u);
|
||
assert.doesNotMatch(payload.reply.content, /模型泛化回答/u);
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "prompt:sent"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "tool:codex-app-server.thread/start+turn/start:started"));
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat handles GitHub access through Codex stdio controlled network check", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-network-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const codexAppServerCalls = [];
|
||
const fetchCalls = [];
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_network",
|
||
lookupImpl: async () => [{ address: "140.82.114.4", family: 4 }],
|
||
fetchImpl: async (url, init) => {
|
||
fetchCalls.push({ url: String(url), method: init?.method });
|
||
return {
|
||
status: 200,
|
||
statusText: "OK",
|
||
url: "https://github.com/"
|
||
};
|
||
},
|
||
createRpcClient: async () => createFakeAppServerClient({ calls: codexAppServerCalls })
|
||
}),
|
||
callCodeAgentProvider: async () => {
|
||
throw new Error("OpenAI fallback must not handle external network prompts");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server_stdio_network",
|
||
traceId: "trc_server_stdio_network",
|
||
message: "访问一下github看看"
|
||
});
|
||
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
|
||
assert.equal(payload.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.providerTrace.transport, "stdio+controlled-network");
|
||
assert.equal(payload.providerTrace.toolName, "external.network.check");
|
||
assert.equal(payload.providerTrace.httpStatus, 200);
|
||
assert.equal(codexAppServerCalls.some((call) => call.method === "turn/start"), false);
|
||
assert.equal(fetchCalls.length, 1);
|
||
assert.equal(fetchCalls[0].url, "https://github.com/");
|
||
assert.equal(fetchCalls[0].method, "HEAD");
|
||
assert.ok(payload.toolCalls.some((toolCall) =>
|
||
toolCall.name === "external.network.check" &&
|
||
toolCall.status === "completed" &&
|
||
toolCall.httpStatus === 200
|
||
));
|
||
assert.match(payload.reply.content, /GitHub可以访问/u);
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "session:created"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "prompt:sent"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "network:started"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "network:completed"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "tool:external.network.check:completed"));
|
||
assert.equal(JSON.stringify(payload).includes("openai-responses-fallback"), false);
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat returns structured external network blocker before 150s", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-network-blocked-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
let fetchCalled = false;
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_EXTERNAL_NETWORK: "0",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_network_blocked",
|
||
lookupImpl: async () => [{ address: "140.82.114.4", family: 4 }],
|
||
fetchImpl: async () => {
|
||
fetchCalled = true;
|
||
throw new Error("fetch must not run when policy blocks external network");
|
||
},
|
||
createRpcClient: async () => createFakeAppServerClient()
|
||
}),
|
||
callCodeAgentProvider: async () => {
|
||
throw new Error("OpenAI fallback must not handle blocked external network prompts");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const started = Date.now();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server_stdio_network_blocked",
|
||
traceId: "trc_server_stdio_network_blocked",
|
||
message: "看看 github 是否能访问"
|
||
});
|
||
const elapsed = Date.now() - started;
|
||
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.error.code, "external_network_blocked");
|
||
assert.equal(payload.blocker.code, "external_network_blocked");
|
||
assert.equal(payload.blocker.traceId, "trc_server_stdio_network_blocked");
|
||
assert.equal(payload.blocker.sessionId, "ses_server_stdio_network_blocked");
|
||
assert.equal(payload.blocker.conversationId, "cnv_server_stdio_network_blocked");
|
||
assert.equal(payload.blocker.stage, "policy");
|
||
assert.equal(typeof payload.blocker.elapsedMs, "number");
|
||
assert.equal(typeof payload.blocker.lastEvent, "object");
|
||
assert.equal(payload.toolCalls[0].name, "external.network.check");
|
||
assert.equal(payload.toolCalls[0].status, "blocked");
|
||
assert.equal(payload.toolCalls[0].blocker.code, "external_network_blocked");
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "prompt:sent"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "network:started"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "network:blocked"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "tool:external.network.check:blocked"));
|
||
assert.equal(fetchCalled, false);
|
||
assert.equal(elapsed < 150000, true);
|
||
assert.equal(JSON.stringify(payload).includes("openai-responses-fallback"), false);
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat returns network_timeout with preserved runner trace", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-network-timeout-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "workspace-write",
|
||
HWLAB_CODE_AGENT_EXTERNAL_NETWORK_TIMEOUT_MS: "25",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_network_timeout",
|
||
lookupImpl: async () => [{ address: "140.82.114.4", family: 4 }],
|
||
fetchImpl: async (_url, init) => new Promise((resolve, reject) => {
|
||
init?.signal?.addEventListener("abort", () => {
|
||
const error = new Error("aborted");
|
||
error.name = "AbortError";
|
||
reject(error);
|
||
});
|
||
}),
|
||
createRpcClient: async () => createFakeAppServerClient()
|
||
})
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const started = Date.now();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server_stdio_network_timeout",
|
||
traceId: "trc_server_stdio_network_timeout",
|
||
message: "打开 https://github.com 看看"
|
||
});
|
||
const elapsed = Date.now() - started;
|
||
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "timeout");
|
||
assert.equal(payload.error.code, "network_timeout");
|
||
assert.equal(payload.blocker.code, "network_timeout");
|
||
assert.equal(payload.blocker.traceId, "trc_server_stdio_network_timeout");
|
||
assert.equal(payload.blocker.sessionId, "ses_server_stdio_network_timeout");
|
||
assert.equal(payload.blocker.stage, "http-timeout");
|
||
assert.equal(payload.toolCalls[0].status, "blocked");
|
||
assert.equal(payload.toolCalls[0].blocker.code, "network_timeout");
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "network:timeout"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "tool:external.network.check:blocked"));
|
||
assert.equal(elapsed < 150000, true);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat returns structured skills blocker without local skills manifest", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-skills-missing-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_SKILLS_DIRS: path.join(workspace, "missing-skills"),
|
||
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_skills_missing",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
text: "模型泛化回答:alpha-skill"
|
||
})
|
||
})
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server_stdio_skills_missing",
|
||
traceId: "trc_server_stdio_skills_missing",
|
||
message: "列出你可用的skills"
|
||
});
|
||
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.equal(payload.error.code, "skills_unavailable");
|
||
assert.equal(payload.skills.status, "blocked");
|
||
assert.equal(payload.skills.blockers[0].code, "skills_unavailable");
|
||
assert.deepEqual(payload.skills.sourceIssues, ["pikasTech/HWLAB#136", "pikasTech/HWLAB#237"]);
|
||
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "blocked"));
|
||
assert.equal(payload.toolCalls.some((toolCall) => toolCall.name === "codex-app-server.thread/start+turn/start"), false);
|
||
assert.equal(payload.session.status, "idle");
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
assert.equal(JSON.stringify(payload).includes("alpha-skill"), false);
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat routes M3 IO through Skill CLI even when Codex stdio is unavailable", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-skill-"));
|
||
const calls = [];
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||
OPENAI_API_KEY: "must-not-be-used"
|
||
},
|
||
m3IoSkillRequestJson: async (url, request) => {
|
||
calls.push({ url, request });
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: {
|
||
serviceId: "hwlab-cloud-api",
|
||
contractVersion: "m3-io-control-v1",
|
||
status: "succeeded",
|
||
accepted: true,
|
||
action: "do.write",
|
||
traceId: "trc_server-test-m3-skill",
|
||
operationId: "op_m3_do_write_server_skill",
|
||
auditId: "aud_m3_do_write_server_skill_succeeded",
|
||
evidenceId: "evd_m3_do_write_server_skill_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: {
|
||
status: "succeeded",
|
||
value: false
|
||
}
|
||
},
|
||
controlPath: {
|
||
status: "succeeded",
|
||
cloudApi: true,
|
||
gatewaySimu: true,
|
||
boxSimu: true,
|
||
patchPanel: true,
|
||
frontendBypass: false
|
||
}
|
||
}
|
||
};
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-m3-skill"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-m3-skill",
|
||
message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 false,然后读取 res_boxsimu_2 的 DI1。"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "hwlab-skill-cli");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli");
|
||
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
|
||
assert.equal(payload.runner.kind, "hwlab-m3-io-skill-cli");
|
||
assert.equal(payload.codexStdioFeasibility.skipped, true);
|
||
assert.equal(payload.responseType, "m3_io_result");
|
||
assert.equal(payload.toolCalls.length, 1);
|
||
assert.equal(payload.toolCalls[0].name, "hwlab-agent-runtime.m3-io");
|
||
assert.equal(payload.toolCalls[0].route, "/v1/m3/io");
|
||
assert.equal(payload.toolCalls[0].accepted, true);
|
||
assert.equal(Object.hasOwn(payload, "reply"), true);
|
||
assert.equal(calls.length, 1);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat recognizes M3 patch-panel topology text before Codex free chat", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-topology-skill-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const calls = [];
|
||
const codexStdioCalls = [];
|
||
const manager = createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_m3_topology_skill",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
calls: codexStdioCalls,
|
||
text: "stdio session ready for controlled topology M3 skill."
|
||
})
|
||
});
|
||
const server = createCloudApiServer({
|
||
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: workspace,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667"
|
||
},
|
||
codexStdioManager: manager,
|
||
m3IoSkillRequestJson: async (url, request) => {
|
||
calls.push({ url, request });
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: {
|
||
serviceId: "hwlab-cloud-api",
|
||
contractVersion: "m3-io-control-v1",
|
||
status: "succeeded",
|
||
accepted: true,
|
||
action: "do.write",
|
||
traceId: "trc_server-test-m3-topology",
|
||
operationId: "op_m3_do_write_topology_skill",
|
||
auditId: "aud_m3_do_write_topology_skill",
|
||
evidenceId: "evd_m3_do_write_topology_skill",
|
||
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,
|
||
resourceId: "res_boxsimu_2",
|
||
port: "DI1"
|
||
}
|
||
},
|
||
controlPath: {
|
||
status: "succeeded",
|
||
cloudApi: true,
|
||
gatewaySimu: true,
|
||
boxSimu: true,
|
||
patchPanel: true,
|
||
frontendBypass: false
|
||
}
|
||
}
|
||
};
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server-test-m3-topology",
|
||
traceId: "trc_server-test-m3-topology",
|
||
message: "执行 res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 基本闭环,把 DO1 置为 true。"
|
||
});
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "hwlab-skill-cli");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli");
|
||
assert.equal(payload.runner.kind, "hwlab-m3-io-skill-cli");
|
||
assert.equal(payload.sessionMode, "controlled-m3-io-skill-cli");
|
||
assert.equal(payload.codexStdioFeasibility.skipped, true);
|
||
assert.equal(payload.responseType, "m3_io_result");
|
||
assert.equal(payload.m3Io.type, "m3_io_result");
|
||
assert.equal(payload.m3Io.action, "do.write");
|
||
assert.equal(payload.m3Io.do1.targetValue, true);
|
||
assert.equal(payload.m3Io.di1.observedValue, true);
|
||
assert.equal(payload.m3Io.wiring.label, "res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1");
|
||
assert.equal(payload.m3Io.path.summary, "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io");
|
||
assert.equal(payload.m3Io.operation.operationId, "op_m3_do_write_topology_skill");
|
||
assert.equal(payload.m3Io.trace.traceId, "trc_server-test-m3-topology");
|
||
assert.equal(payload.m3Io.session.sessionId, payload.session.sessionId);
|
||
assert.equal(payload.m3Io.trust.trusted, false);
|
||
assert.equal(payload.m3Io.trust.durable, false);
|
||
const skillTool = payload.toolCalls.find((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io");
|
||
assert.ok(skillTool);
|
||
assert.equal(skillTool.route, "/v1/m3/io");
|
||
assert.equal(skillTool.method, "POST");
|
||
assert.equal(skillTool.accepted, true);
|
||
assert.equal(skillTool.operationId, "op_m3_do_write_topology_skill");
|
||
assert.equal(skillTool.traceId, "trc_server-test-m3-topology");
|
||
assert.equal(skillTool.readback.value, true);
|
||
assert.equal(payload.providerTrace.fallbackUsed, false);
|
||
assert.equal(payload.providerTrace.transport, "skill-cli");
|
||
assert.ok(payload.runnerTrace.events.includes("tool:skill-cli:started"));
|
||
assert.ok(payload.runnerTrace.events.includes("route:/v1/m3/io"));
|
||
assert.match(payload.reply.content, /Code Agent -> Skill CLI -> HWLAB API \/v1\/m3\/io/u);
|
||
assert.match(payload.reply.content, /res_boxsimu_1:DO1=true/u);
|
||
assert.match(payload.reply.content, /res_boxsimu_2:DI1=true/u);
|
||
assert.equal(calls.length, 1);
|
||
assert.equal(calls[0].url, "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667/v1/m3/io");
|
||
assert.equal(calls[0].request.body.action, "do.write");
|
||
assert.equal(calls[0].request.body.value, true);
|
||
assert.deepEqual(codexStdioCalls, []);
|
||
assert.equal(/https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel)|:7101\b|:7201\b|:7301\b|\/invoke\b|\/sync\/tick\b/iu.test(JSON.stringify(payload.toolCalls)), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat returns Skill CLI blocker for M3 topology without DO1 value", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-topology-blocker-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const calls = [];
|
||
const manager = createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_m3_topology_blocker",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
text: "stdio session ready for blocked topology M3 skill."
|
||
})
|
||
});
|
||
const server = createCloudApiServer({
|
||
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: workspace,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667"
|
||
},
|
||
codexStdioManager: manager,
|
||
m3IoSkillRequestJson: async (url, request) => {
|
||
calls.push({ url, request });
|
||
throw new Error("missing DO1 value must block before HWLAB API request");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server-test-m3-topology-blocker",
|
||
traceId: "trc_server-test-m3-topology-blocker",
|
||
message: "执行 res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1 基本闭环。"
|
||
});
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "hwlab-skill-cli");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli");
|
||
assert.equal(payload.runner.kind, "hwlab-m3-io-skill-cli");
|
||
assert.equal(payload.sessionMode, "controlled-m3-io-skill-cli");
|
||
assert.equal(payload.codexStdioFeasibility.skipped, true);
|
||
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
||
assert.equal(payload.responseType, "m3_io_blocker");
|
||
assert.equal(payload.m3Io.type, "m3_io_blocker");
|
||
assert.equal(payload.m3Io.action, "do.write");
|
||
assert.equal(payload.m3Io.do1.targetValue, null);
|
||
assert.equal(payload.m3Io.di1.observedValue, null);
|
||
assert.equal(payload.m3Io.path.summary, "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io");
|
||
assert.equal(payload.m3Io.blocker.code, "invalid_boolean_value");
|
||
const skillTool = payload.toolCalls.find((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io");
|
||
assert.ok(skillTool);
|
||
assert.equal(skillTool.status, "blocked");
|
||
assert.equal(skillTool.route, "/v1/m3/io");
|
||
assert.equal(skillTool.blocker.code, "invalid_boolean_value");
|
||
assert.equal(skillTool.accepted, false);
|
||
assert.equal(skillTool.operationId, null);
|
||
assert.equal(payload.blocker.code, "invalid_boolean_value");
|
||
assert.match(payload.reply.content, /^M3 IO 阻塞:/u);
|
||
assert.match(payload.reply.content, /M3 DO1 写入需要 --value true 或 --value false/u);
|
||
assert.match(payload.reply.content, /Code Agent -> Skill CLI -> HWLAB API \/v1\/m3\/io/u);
|
||
assert.equal(payload.providerTrace.fallbackUsed, false);
|
||
assert.equal(payload.providerTrace.transport, "skill-cli");
|
||
assert.equal(calls.length, 0);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat promotes /v1/m3/io blocker into Chinese M3 IO reply", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-api-blocker-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const calls = [];
|
||
const manager = createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_m3_api_blocker",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
text: "stdio session ready for M3 API blocker fixture."
|
||
})
|
||
});
|
||
const server = createCloudApiServer({
|
||
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: workspace,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667"
|
||
},
|
||
codexStdioManager: manager,
|
||
m3IoSkillRequestJson: async (url, request) => {
|
||
calls.push({ url, request });
|
||
return {
|
||
ok: true,
|
||
status: 200,
|
||
body: {
|
||
serviceId: "hwlab-cloud-api",
|
||
contractVersion: "m3-io-control-v1",
|
||
route: "/v1/m3/io",
|
||
method: "POST",
|
||
status: "blocked",
|
||
accepted: false,
|
||
action: "do.write",
|
||
traceId: "trc_server-test-m3-api-blocker",
|
||
operationId: "op_m3_do_write_api_blocker",
|
||
auditId: "aud_m3_do_write_api_blocker_failed",
|
||
evidenceId: "evd_m3_do_write_api_blocker_failed",
|
||
blocker: {
|
||
code: "m3_wiring_missing",
|
||
layer: "hwlab-patch-panel",
|
||
zh: "hwlab-patch-panel 未确认 active res_boxsimu_1:DO1 -> res_boxsimu_2:DI1 接线"
|
||
},
|
||
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: null
|
||
},
|
||
controlPath: {
|
||
status: "blocked",
|
||
cloudApi: true,
|
||
gatewaySimu: true,
|
||
boxSimu: true,
|
||
patchPanel: false,
|
||
frontendBypass: false
|
||
}
|
||
}
|
||
};
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server-test-m3-api-blocker",
|
||
traceId: "trc_server-test-m3-api-blocker",
|
||
message: "把 M3 DO1 打开,然后读取 DI1。"
|
||
});
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.responseType, "m3_io_blocker");
|
||
assert.equal(payload.m3Io.type, "m3_io_blocker");
|
||
assert.equal(payload.m3Io.action, "do.write");
|
||
assert.equal(payload.m3Io.do1.targetValue, true);
|
||
assert.equal(payload.m3Io.di1.observedValue, null);
|
||
assert.equal(payload.m3Io.blocker.code, "m3_wiring_missing");
|
||
assert.equal(payload.blocker.code, "m3_wiring_missing");
|
||
assert.match(payload.reply.content, /^M3 IO 阻塞:/u);
|
||
assert.match(payload.reply.content, /hwlab-patch-panel 未确认 active/u);
|
||
assert.match(payload.reply.content, /res_boxsimu_1:DO1 -> hwlab-patch-panel -> res_boxsimu_2:DI1/u);
|
||
assert.match(payload.reply.content, /trusted=false;durable=false/u);
|
||
assert.equal(calls.length, 1);
|
||
assert.equal(new URL(calls[0].url).pathname, "/v1/m3/io");
|
||
assert.equal(calls[0].request.body.action, "do.write");
|
||
assert.equal(calls[0].request.body.value, true);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat routes M3 Skill CLI through in-process HWLAB API handler", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-stdio-m3-skill-"));
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
const gatewayCalls = [];
|
||
const codexStdioCalls = [];
|
||
const manager = createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_m3_skill",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
calls: codexStdioCalls,
|
||
text: "stdio session ready for M3 skill."
|
||
})
|
||
});
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: workspace,
|
||
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: workspace,
|
||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-cloud-api.hwlab-dev.svc.cluster.local:6667",
|
||
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
|
||
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
|
||
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
|
||
},
|
||
codexStdioManager: manager,
|
||
m3IoRequestJson: async (url, request) => {
|
||
gatewayCalls.push({ url, request });
|
||
return m3ReadinessRequestJson(url, request);
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server_stdio_m3_skill",
|
||
traceId: "trc_server_stdio_m3_skill",
|
||
message: "通过 HWLAB API 把 res_boxsimu_1 的 DO1 写成 true,然后读取 res_boxsimu_2 的 DI1。"
|
||
});
|
||
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "hwlab-skill-cli");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/hwlab-agent-runtime-skill-cli");
|
||
assert.equal(payload.runner.kind, "hwlab-m3-io-skill-cli");
|
||
assert.equal(payload.sessionMode, "controlled-m3-io-skill-cli");
|
||
assert.equal(payload.runner.codexStdio, false);
|
||
assert.equal(payload.codexStdioFeasibility.skipped, true);
|
||
assert.equal(payload.codexStdioFeasibility.reason, "m3_io_skill_cli_deterministic_route");
|
||
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
|
||
assert.equal(payload.responseType, "m3_io_result");
|
||
assert.equal(payload.m3Io.type, "m3_io_result");
|
||
assert.equal(payload.m3Io.action, "do.write");
|
||
assert.equal(payload.m3Io.do1.targetValue, true);
|
||
assert.equal(payload.m3Io.di1.observedValue, true);
|
||
assert.equal(payload.m3Io.operation.operationId.startsWith("op_m3_do_write_"), true);
|
||
assert.equal(payload.m3Io.session.sessionId, payload.session.sessionId);
|
||
assert.equal(payload.m3Io.path.summary, "Code Agent -> Skill CLI -> HWLAB API /v1/m3/io");
|
||
assert.equal(payload.m3Io.trust.trusted, false);
|
||
assert.equal(payload.m3Io.trust.durable, false);
|
||
const skillTool = payload.toolCalls.find((toolCall) => toolCall.name === "hwlab-agent-runtime.m3-io");
|
||
assert.ok(skillTool);
|
||
assert.equal(skillTool.type, "skill-cli");
|
||
assert.equal(skillTool.route, "/v1/m3/io");
|
||
assert.equal(skillTool.method, "POST");
|
||
assert.equal(skillTool.accepted, true);
|
||
assert.equal(skillTool.readback.value, true);
|
||
assert.equal(skillTool.controlReady, true);
|
||
assert.equal(skillTool.operationId.startsWith("op_m3_do_write_"), true);
|
||
assert.match(skillTool.auditId, /^aud_m3_do_write_/u);
|
||
assert.match(skillTool.evidenceId, /^evd_m3_do_write_/u);
|
||
assert.equal(payload.runnerTrace.route, "/v1/m3/io");
|
||
assert.ok(payload.runnerTrace.events.includes("tool:skill-cli:started"));
|
||
assert.ok(payload.runnerTrace.events.includes("route:/v1/m3/io"));
|
||
assert.equal(payload.providerTrace.fallbackUsed, false);
|
||
assert.equal(payload.providerTrace.codexStdio, false);
|
||
assert.equal(payload.providerTrace.transport, "skill-cli");
|
||
assert.match(payload.reply.content, /HWLAB API \/v1\/m3\/io/u);
|
||
assert.match(payload.reply.content, /res_boxsimu_1:DO1=true/u);
|
||
assert.match(payload.reply.content, /res_boxsimu_2:DI1=true/u);
|
||
assert.ok(payload.runner.toolPolicy.allowed.includes("POST /v1/m3/io"));
|
||
assert.deepEqual(gatewayCalls.map((call) => new URL(call.url).pathname), [
|
||
"/status",
|
||
"/invoke",
|
||
"/sync/tick",
|
||
"/status",
|
||
"/invoke"
|
||
]);
|
||
assert.deepEqual(codexStdioCalls, []);
|
||
assert.equal(/https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel)|:7101\b|:7201\b|:7301\b|\/invoke\b|\/sync\/tick\b/iu.test(JSON.stringify(payload.toolCalls)), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat blocks direct M3 API base targets before Codex stdio", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-direct-target-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const calls = [];
|
||
const manager = createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_m3_direct_block",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
text: "stdio session ready for blocked M3 direct target."
|
||
})
|
||
});
|
||
const server = createCloudApiServer({
|
||
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: workspace,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101"
|
||
},
|
||
codexStdioManager: manager,
|
||
m3IoSkillRequestJson: async (url, request) => {
|
||
calls.push({ url, request });
|
||
throw new Error("direct gateway target must be blocked before requestJson");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server-test-m3-direct-target",
|
||
traceId: "trc_server-test-m3-direct-target",
|
||
message: "读取 M3 DI1"
|
||
});
|
||
validateCodeAgentChatSchema(payload);
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "hwlab-skill-cli");
|
||
assert.equal(payload.runner.kind, "hwlab-m3-io-skill-cli");
|
||
assert.equal(payload.sessionMode, "controlled-m3-io-skill-cli");
|
||
assert.equal(payload.codexStdioFeasibility.skipped, true);
|
||
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
||
assert.equal(payload.responseType, "m3_io_blocker");
|
||
assert.equal(payload.m3Io.type, "m3_io_blocker");
|
||
assert.equal(payload.m3Io.blocker.code, "direct_hardware_target_blocked");
|
||
assert.equal(payload.toolCalls.length, 1);
|
||
const skillTool = payload.toolCalls[0];
|
||
assert.equal(skillTool.name, "hwlab-agent-runtime.m3-io");
|
||
assert.equal(skillTool.status, "blocked");
|
||
assert.equal(skillTool.route, "/v1/m3/io");
|
||
assert.equal(skillTool.method, "POST");
|
||
assert.equal(skillTool.blocker.code, "direct_hardware_target_blocked");
|
||
assert.equal(skillTool.hwlabApi.redactedUrl, null);
|
||
assert.equal(payload.skills.blockers[0].code, "direct_hardware_target_blocked");
|
||
assert.equal(calls.length, 0);
|
||
assert.equal(/https?:\/\/[^\s"']*(?:gateway(?:-simu)?|box(?:-simu)?|patch-panel)|:7101\b|:7201\b|:7301\b|\/invoke\b|\/sync\/tick\b/iu.test(JSON.stringify(payload.toolCalls)), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat reports M3 Skill CLI missing API base before Codex stdio", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-api-base-missing-"));
|
||
const codexHome = path.join(workspace, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const manager = createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_stdio_m3_api_base_missing",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
text: "stdio session ready for missing M3 API base."
|
||
})
|
||
});
|
||
const server = createCloudApiServer({
|
||
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: workspace,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_REQUIRE_HWLAB_API_BASE_URL: "1"
|
||
},
|
||
codexStdioManager: manager,
|
||
m3IoSkillRequestJson: async () => {
|
||
throw new Error("missing API base must not call requestJson");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server-test-m3-api-base-missing",
|
||
traceId: "trc_server-test-m3-api-base-missing",
|
||
message: "读取 M3 DI1"
|
||
});
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "hwlab-skill-cli");
|
||
assert.equal(payload.runner.kind, "hwlab-m3-io-skill-cli");
|
||
assert.equal(payload.sessionMode, "controlled-m3-io-skill-cli");
|
||
assert.equal(payload.codexStdioFeasibility.skipped, true);
|
||
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
||
assert.equal(payload.responseType, "m3_io_blocker");
|
||
assert.equal(payload.m3Io.type, "m3_io_blocker");
|
||
assert.equal(payload.m3Io.blocker.code, "skill_cli_api_base_missing");
|
||
assert.equal(payload.skills.blockers[0].code, "skill_cli_api_base_missing");
|
||
assert.equal(payload.toolCalls.length, 1);
|
||
const skillTool = payload.toolCalls[0];
|
||
assert.equal(skillTool.name, "hwlab-agent-runtime.m3-io");
|
||
assert.equal(skillTool.blocker.code, "skill_cli_api_base_missing");
|
||
assert.deepEqual(skillTool.blocker.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(payload).includes("test-openai-key-material"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(workspace, { recursive: true, force: true });
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat refuses local file-tool shortcuts when Codex stdio is unavailable", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-file-tools-"));
|
||
await writeFile(path.join(workspace, "alpha.txt"), "alpha\nbeta\n", "utf8");
|
||
await writeFile(path.join(workspace, "package.json"), "{\"name\":\"sample\"}\n", "utf8");
|
||
await mkdir(path.join(workspace, "src"), { recursive: true });
|
||
await writeFile(path.join(workspace, "src", "main.mjs"), "export const value = 1;\n", "utf8");
|
||
const env = {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_WORKSPACE: workspace
|
||
};
|
||
const server = createCloudApiServer({ env });
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const first = await postAgent(port, {
|
||
conversationId: "cnv_server-test-session-reuse",
|
||
traceId: "trc_server-test-session-reuse-ls",
|
||
message: "ls ."
|
||
});
|
||
assert.equal(first.status, "failed");
|
||
assert.equal(first.provider, "codex-stdio");
|
||
assert.equal(first.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(first.capabilityLevel, "blocked");
|
||
assert.equal(first.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.deepEqual(first.toolCalls, []);
|
||
assert.equal(Object.hasOwn(first, "reply"), false);
|
||
assert.ok(first.runnerTrace.events.some((event) => event.label === "request:accepted"));
|
||
assert.ok(first.runnerLimitations.includes("no-controlled-readonly-fallback"));
|
||
|
||
const second = await postAgent(port, {
|
||
conversationId: "cnv_server-test-session-reuse",
|
||
traceId: "trc_server-test-session-reuse-cat",
|
||
message: "cat alpha.txt"
|
||
});
|
||
assert.equal(second.status, "failed");
|
||
assert.equal(second.provider, "codex-stdio");
|
||
assert.deepEqual(second.toolCalls, []);
|
||
assert.equal(Object.hasOwn(second, "reply"), false);
|
||
|
||
const third = await postAgent(port, {
|
||
conversationId: "cnv_server-test-session-reuse",
|
||
traceId: "trc_server-test-session-reuse-rg",
|
||
message: "rg --files ."
|
||
});
|
||
assert.equal(third.status, "failed");
|
||
assert.equal(third.provider, "codex-stdio");
|
||
assert.deepEqual(third.toolCalls, []);
|
||
assert.equal(Object.hasOwn(third, "reply"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat does not use read-only conversation facts as fallback", async () => {
|
||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-session-continuity-"));
|
||
const workspace = path.join(root, "workspace");
|
||
const skillsDir = path.join(root, "skills");
|
||
await mkdir(workspace, { recursive: true });
|
||
await mkdir(path.join(skillsDir, "alpha"), { recursive: true });
|
||
await mkdir(path.join(skillsDir, "beta"), { recursive: true });
|
||
await writeFile(path.join(skillsDir, "alpha", "SKILL.md"), [
|
||
"---",
|
||
"name: alpha-skill",
|
||
"description: Alpha skill summary.",
|
||
"---",
|
||
"",
|
||
"# Alpha"
|
||
].join("\n"));
|
||
await writeFile(path.join(skillsDir, "beta", "SKILL.md"), [
|
||
"---",
|
||
"name: beta-skill",
|
||
"description: Beta skill summary.",
|
||
"---",
|
||
"",
|
||
"# Beta"
|
||
].join("\n"));
|
||
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "0",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex"
|
||
},
|
||
skillsDirs: [skillsDir],
|
||
skillsDirsExact: true
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const first = await postAgent(port, {
|
||
conversationId: "cnv_server-session-continuity",
|
||
traceId: "trc_server-session-continuity-pwd",
|
||
message: "用pwd列出你当前的工作目录"
|
||
});
|
||
assert.equal(first.status, "failed");
|
||
assert.equal(first.provider, "codex-stdio");
|
||
assert.equal(first.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(first.capabilityLevel, "blocked");
|
||
assert.equal(first.conversationFacts.turnCount, 0);
|
||
|
||
const second = await postAgent(port, {
|
||
conversationId: "cnv_server-session-continuity",
|
||
traceId: "trc_server-session-continuity-skills",
|
||
message: "列出你能使用的所有skill"
|
||
});
|
||
assert.equal(second.status, "failed");
|
||
assert.equal(second.provider, "codex-stdio");
|
||
assert.equal(second.skills.status, "not_requested");
|
||
assert.equal(second.conversationFacts.turnCount, 0);
|
||
|
||
const third = await postAgent(port, {
|
||
conversationId: "cnv_server-session-continuity",
|
||
traceId: "trc_server-session-continuity-context",
|
||
message: "根据前两轮结果说明你现在在哪个工作目录、能使用哪些skill"
|
||
});
|
||
assert.equal(third.status, "failed");
|
||
assert.equal(third.provider, "codex-stdio");
|
||
assert.equal(third.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(third.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(third.capabilityLevel, "blocked");
|
||
assert.deepEqual(third.toolCalls, []);
|
||
assert.equal(third.conversationFacts.turnCount, 0);
|
||
assert.notEqual(third.provider, "openai-responses");
|
||
assert.notEqual(third.runner.kind, "openai-responses-fallback");
|
||
assert.notEqual(third.runner.kind, "codex-cli-one-shot-ephemeral");
|
||
assert.notEqual(third.sessionMode, "ephemeral-one-shot");
|
||
assert.equal(Object.hasOwn(third, "reply"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat blocks forbidden file paths without leaking values", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-file-block-"));
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_WORKSPACE: workspace
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const payload = await postAgent(port, {
|
||
conversationId: "cnv_server-test-security-block",
|
||
traceId: "trc_server-test-security-block",
|
||
message: "cat ../outside.txt"
|
||
});
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.error.code, "codex_cli_binary_missing");
|
||
assert.deepEqual(payload.toolCalls, []);
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "codex-stdio:blocked"));
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
assert.equal(JSON.stringify(payload).includes("sk-"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat does not answer skills from local manifest when Codex stdio is blocked", async () => {
|
||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-skills-"));
|
||
const skillsDir = path.join(root, "skills");
|
||
await mkdir(path.join(skillsDir, "alpha"), { recursive: true });
|
||
await writeFile(path.join(skillsDir, "alpha", "SKILL.md"), [
|
||
"---",
|
||
"name: alpha-skill",
|
||
"description: Alpha skill summary.",
|
||
"version: 1.2.3",
|
||
"commit: abc123def456",
|
||
"---",
|
||
"",
|
||
"# Alpha"
|
||
].join("\n"));
|
||
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_WORKSPACE: root
|
||
},
|
||
skillsDirs: [skillsDir],
|
||
skillsDirsExact: true
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-skills",
|
||
message: "列出你可用的skills"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.equal(payload.skills.status, "not_requested");
|
||
assert.deepEqual(payload.toolCalls, []);
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
assert.equal(JSON.stringify(payload).includes("alpha-skill"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat exposes prompt trace immediately while Codex stdio model call is slow", async () => {
|
||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-slow-skills-"));
|
||
const workspace = path.join(root, "workspace");
|
||
const skillsDir = path.join(root, "skills");
|
||
await mkdir(workspace, { recursive: true });
|
||
await mkdir(path.join(skillsDir, "slow-skill"), { recursive: true });
|
||
await writeFile(path.join(skillsDir, "slow-skill", "SKILL.md"), [
|
||
"---",
|
||
"name: slow-skill",
|
||
"description: Slow skill discovery summary.",
|
||
"version: 2026.05.23",
|
||
"commit: slow12345678",
|
||
"---",
|
||
"",
|
||
"# Slow"
|
||
].join("\n"));
|
||
|
||
const codexHome = path.join(root, "codex-home");
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
await prepareFakeCodexHome(codexHome);
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
CODEX_HOME: codexHome,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_SKILLS_DIRS: skillsDir,
|
||
HWLAB_CODE_AGENT_SKILLS_STRICT: "1",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1:65535/v1/responses"
|
||
},
|
||
codexStdioManager: createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_test_slow_skills",
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
text: "真实 Codex stdio 回复:slow-skill",
|
||
delayMs: 120
|
||
})
|
||
})
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const chatPromise = fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-slow-skills"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-slow-skills",
|
||
message: "请列出你可用的所有 skills"
|
||
})
|
||
});
|
||
await delay(40);
|
||
const traceResponse = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/trace/trc_server-test-slow-skills`);
|
||
assert.equal(traceResponse.status, 200);
|
||
const tracePayload = await traceResponse.json();
|
||
assert.equal(tracePayload.traceId, "trc_server-test-slow-skills");
|
||
assert.equal(tracePayload.eventCount > 0, true);
|
||
assert.ok(tracePayload.events.some((event) => event.label === "request:accepted" || event.label === "prompt:sent"));
|
||
|
||
const response = await chatPromise;
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "completed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.traceId, "trc_server-test-slow-skills");
|
||
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
|
||
assert.equal(payload.sessionMode, "codex-app-server-stdio-long-lived");
|
||
assert.equal(payload.skills.status, "ready");
|
||
assert.equal(payload.skills.items[0].name, "slow-skill");
|
||
assert.equal(payload.skills.items[0].traceId, "trc_server-test-slow-skills");
|
||
assert.equal(payload.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(payload.runnerTrace.traceId, "trc_server-test-slow-skills");
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "tool:codex-app-server.thread/start+turn/start:started"));
|
||
assert.ok(payload.runnerTrace.events.some((event) => event.label === "assistant:completed"));
|
||
assert.equal(Object.hasOwn(payload, "error"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat/cancel cancels in-flight Codex stdio and keeps retry trace", async () => {
|
||
const fakeCodex = await createFakeCodexCommand();
|
||
const workspace = path.join(fakeCodex.root, "workspace");
|
||
const codexHome = path.join(fakeCodex.root, "codex-home");
|
||
await mkdir(workspace, { recursive: true });
|
||
await prepareFakeCodexHome(codexHome);
|
||
const traceStore = createCodeAgentTraceStore();
|
||
let toolStartedResolve = null;
|
||
const toolStarted = new Promise((resolve) => {
|
||
toolStartedResolve = resolve;
|
||
});
|
||
const manager = createCodexStdioSessionManager({
|
||
idFactory: () => "ses_server_test_cancel",
|
||
traceStore,
|
||
createRpcClient: async () => createFakeAppServerClient({
|
||
complete: false,
|
||
onTurnStarted: () => toolStartedResolve()
|
||
})
|
||
});
|
||
const server = createCloudApiServer({
|
||
traceStore,
|
||
codexStdioManager: manager,
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: fakeCodex.command,
|
||
HWLAB_CODE_AGENT_CODEX_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_WORKSPACE: workspace,
|
||
HWLAB_CODE_AGENT_CODEX_SANDBOX: "workspace-write",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_ENABLED: "1",
|
||
HWLAB_CODE_AGENT_CODEX_STDIO_SUPERVISOR: "repo-owned",
|
||
CODEX_HOME: codexHome
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const chatPromise = fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-cancel"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-cancel",
|
||
message: "请执行一个需要等待的 Codex stdio 请求"
|
||
})
|
||
});
|
||
await toolStarted;
|
||
const cancelResponse = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/cancel`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-cancel"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-cancel",
|
||
sessionId: "ses_server_test_cancel",
|
||
traceId: "trc_server-test-cancel"
|
||
})
|
||
});
|
||
assert.equal(cancelResponse.status, 200);
|
||
const cancelPayload = await cancelResponse.json();
|
||
assert.equal(cancelPayload.status, "canceled");
|
||
assert.equal(cancelPayload.canceled, true);
|
||
assert.equal(cancelPayload.sessionId, "ses_server_test_cancel");
|
||
assert.equal(cancelPayload.session.status, "canceled");
|
||
assert.equal(cancelPayload.session.lifecycleStatus, "interrupted");
|
||
assert.equal(cancelPayload.sessionLifecycleStatus, "interrupted");
|
||
assert.equal(cancelPayload.sessionSummary.requiresNewSession, true);
|
||
assert.equal(cancelPayload.lastTraceEvent.label, "cancel:canceled");
|
||
|
||
const chatResponse = await chatPromise;
|
||
assert.equal(chatResponse.status, 200);
|
||
const chatPayload = await chatResponse.json();
|
||
assert.equal(chatPayload.status, "canceled");
|
||
assert.equal(chatPayload.error.code, "codex_stdio_canceled");
|
||
assert.equal(chatPayload.session.sessionId, "ses_server_test_cancel");
|
||
assert.equal(chatPayload.session.status, "canceled");
|
||
assert.equal(chatPayload.session.lifecycleStatus, "interrupted");
|
||
assert.equal(chatPayload.sessionLifecycleStatus, "interrupted");
|
||
assert.equal(chatPayload.sessionSummary.requiresNewSession, true);
|
||
assert.equal(chatPayload.runnerTrace.lastEvent.label, "cancel:canceled");
|
||
|
||
const traceResponse = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/trace/trc_server-test-cancel`);
|
||
assert.equal(traceResponse.status, 200);
|
||
const tracePayload = await traceResponse.json();
|
||
assert.equal(tracePayload.status, "canceled");
|
||
assert.equal(tracePayload.lastEvent.label, "cancel:canceled");
|
||
assert.ok(tracePayload.events.some((event) => event.label === "tool:codex-app-server.thread/start+turn/start:started"));
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await rm(fakeCodex.root, { recursive: true, force: true });
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat/cancel reports unsupported lifecycle degradation", async () => {
|
||
const traceStore = createCodeAgentTraceStore();
|
||
traceStore.append("trc_cancel_unsupported", {
|
||
type: "request",
|
||
status: "running",
|
||
label: "request:running",
|
||
sessionId: "ses_cancel_unsupported",
|
||
sessionStatus: "busy",
|
||
sessionLifecycleStatus: "busy"
|
||
});
|
||
const server = createCloudApiServer({
|
||
traceStore,
|
||
codexStdioManager: {
|
||
get() {
|
||
return null;
|
||
}
|
||
},
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-stdio"
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/cancel`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_cancel_unsupported"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_cancel_unsupported",
|
||
sessionId: "ses_cancel_unsupported",
|
||
traceId: "trc_cancel_unsupported"
|
||
})
|
||
});
|
||
assert.equal(response.status, 501);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "degraded");
|
||
assert.equal(payload.unsupported, true);
|
||
assert.equal(payload.degraded, true);
|
||
assert.equal(payload.error.code, "cancel_unsupported");
|
||
assert.equal(payload.sessionLifecycleStatus, "failed");
|
||
assert.equal(payload.sessionSummary.unsupported, true);
|
||
assert.match(payload.sessionSummary.userMessage, /unsupported\/degraded/u);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat reports Codex stdio blocker instead of structured skills fallback", async () => {
|
||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-no-skills-"));
|
||
const skillsDir = path.join(root, "missing-skills");
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_WORKSPACE: root
|
||
},
|
||
skillsDirs: [skillsDir],
|
||
skillsDirsExact: true
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-skills-missing",
|
||
message: "列出你可用的skills"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.notEqual(payload.error.code, "skills_unavailable");
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.equal(payload.skills.status, "not_requested");
|
||
assert.deepEqual(payload.toolCalls, []);
|
||
assert.ok(payload.runnerLimitations.includes("no-controlled-readonly-fallback"));
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat does not run unsupported local grep fallback", async () => {
|
||
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-tool-blocked-"));
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: process.env.PATH,
|
||
HWLAB_CODE_AGENT_WORKSPACE: workspace
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-tool-unavailable",
|
||
message: "请用grep搜索 package.json"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "failed");
|
||
assert.notEqual(payload.error.code, "tool_unavailable");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.deepEqual(payload.toolCalls, []);
|
||
assert.ok(payload.runnerLimitations.includes("no-controlled-readonly-fallback"));
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat ignores OpenAI fallback and blocks when Codex stdio is unavailable", async () => {
|
||
const providerRequests = [];
|
||
const providerServer = createHttpServer((request, response) => {
|
||
const chunks = [];
|
||
request.on("data", (chunk) => chunks.push(chunk));
|
||
request.on("end", () => {
|
||
const bodyText = Buffer.concat(chunks).toString("utf8");
|
||
const body = JSON.parse(bodyText);
|
||
providerRequests.push({
|
||
method: request.method,
|
||
url: request.url,
|
||
authorizationPresent: Boolean(request.headers.authorization),
|
||
accept: request.headers.accept,
|
||
contentType: request.headers["content-type"],
|
||
body
|
||
});
|
||
response.writeHead(200, {
|
||
"content-type": "text/event-stream"
|
||
});
|
||
response.end([
|
||
`data: ${JSON.stringify({
|
||
type: "response.created",
|
||
response: {
|
||
id: "resp_server_test_stream",
|
||
model: body.model,
|
||
usage: null
|
||
}
|
||
})}`,
|
||
`data: ${JSON.stringify({
|
||
type: "response.output_text.delta",
|
||
response_id: "resp_server_test_stream",
|
||
delta: "HWLAB Code Agent "
|
||
})}`,
|
||
`data: ${JSON.stringify({
|
||
type: "response.output_text.delta",
|
||
response_id: "resp_server_test_stream",
|
||
delta: "streaming ready."
|
||
})}`,
|
||
`data: ${JSON.stringify({
|
||
type: "response.completed",
|
||
response: {
|
||
id: "resp_server_test_stream",
|
||
model: body.model,
|
||
usage: null
|
||
}
|
||
})}`,
|
||
"data: [DONE]",
|
||
""
|
||
].join("\n\n"));
|
||
});
|
||
});
|
||
await new Promise((resolve) => providerServer.listen(0, "127.0.0.1", resolve));
|
||
const providerPort = providerServer.address().port;
|
||
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "true",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: `http://127.0.0.1:${providerPort}/v1/responses`
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-agent-chat-stream"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-agent-chat-stream",
|
||
message: "请用一句话说明当前 HWLAB 工作台可以做什么。"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.conversationId, "cnv_server-test-agent-chat-stream");
|
||
assert.equal(payload.traceId, "trc_server-test-agent-chat-stream");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.model, "gpt-test");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.equal(payload.error.code, "codex_cli_binary_missing");
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
||
|
||
assert.equal(providerRequests.length, 0);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await new Promise((resolve, reject) => {
|
||
providerServer.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat does not call delayed provider fallback beyond legacy 4500ms UI timeout", async () => {
|
||
let providerCalled = false;
|
||
const server = createCloudApiServer({
|
||
callCodeAgentProvider: async ({ providerPlan, message, traceId }) => {
|
||
providerCalled = true;
|
||
await delay(4700);
|
||
return {
|
||
provider: providerPlan.provider,
|
||
model: providerPlan.model,
|
||
backend: providerPlan.backend,
|
||
content: `延迟真实 provider stub: ${message} / ${traceId}`,
|
||
usage: null,
|
||
providerTrace: {
|
||
source: "delayed-test-provider"
|
||
}
|
||
};
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const startedAt = Date.now();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-agent-chat-delayed"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-agent-chat-delayed",
|
||
message: "请用一句话说明当前 HWLAB 工作台可以做什么,稍后回答"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(Date.now() - startedAt < 4500, true);
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.conversationId, "cnv_server-test-agent-chat-delayed");
|
||
assert.equal(payload.traceId, "trc_server-test-agent-chat-delayed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.capabilityLevel, "blocked");
|
||
assert.equal(providerCalled, false);
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat skips delayed provider fallback when Codex stdio is unavailable", async () => {
|
||
let providerCalled = false;
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "true",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "http://127.0.0.1/provider-fixture"
|
||
},
|
||
callCodeAgentProvider: async ({ providerPlan }) => {
|
||
providerCalled = true;
|
||
await delay(4600);
|
||
const error = new Error("OpenAI Responses returned HTTP 503: slow upstream rejected");
|
||
error.code = "provider_unavailable";
|
||
error.provider = providerPlan.provider;
|
||
error.model = providerPlan.model;
|
||
error.backend = providerPlan.backend;
|
||
error.providerStatus = 503;
|
||
throw error;
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const startedAt = Date.now();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-agent-chat-delayed-failure"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-agent-chat-delayed-failure",
|
||
message: "请等待后返回 provider 失败分类"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(Date.now() - startedAt < 4500, true);
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.traceId, "trc_server-test-agent-chat-delayed-failure");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.error.code, "codex_cli_binary_missing");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.availability.fallback.backend, "hwlab-cloud-api/openai-responses");
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
assert.equal(providerCalled, false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat reports Codex stdio blocker instead of provider timeout fallback", async () => {
|
||
const providerServer = createHttpServer((request, response) => {
|
||
request.resume();
|
||
setTimeout(() => {
|
||
response.writeHead(200, { "content-type": "application/json" });
|
||
response.end(JSON.stringify({ output_text: "too late" }));
|
||
}, 250);
|
||
});
|
||
await new Promise((resolve) => providerServer.listen(0, "127.0.0.1", resolve));
|
||
const providerPort = providerServer.address().port;
|
||
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "true",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: `http://127.0.0.1:${providerPort}/v1/responses`
|
||
},
|
||
codeAgentTimeoutMs: 50
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_server-test-agent-chat-provider-timeout"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_server-test-agent-chat-provider-timeout",
|
||
message: "请等待后回答"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.traceId, "trc_server-test-agent-chat-provider-timeout");
|
||
assert.equal(payload.error.code, "codex_cli_binary_missing");
|
||
assert.equal(payload.error.layer, "runner");
|
||
assert.equal(payload.error.retryable, false);
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(payload.availability.endpoint, "POST /v1/agent/chat");
|
||
assert.equal(payload.availability.runner.kind, "codex-app-server-stdio-runner");
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await new Promise((resolve, reject) => {
|
||
providerServer.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat does not call OpenAI provider 502/503 fallback", async () => {
|
||
for (const status of [502, 503]) {
|
||
const providerServer = createHttpServer((request, response) => {
|
||
request.resume();
|
||
response.writeHead(status, { "content-type": "application/json" });
|
||
response.end(JSON.stringify({
|
||
error: {
|
||
message: `upstream ${status}`
|
||
}
|
||
}));
|
||
});
|
||
await new Promise((resolve) => providerServer.listen(0, "127.0.0.1", resolve));
|
||
const providerPort = providerServer.address().port;
|
||
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
OPENAI_API_KEY: "test-openai-key-material",
|
||
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
||
HWLAB_CODE_AGENT_ALLOW_TEXT_FALLBACK: "true",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_OPENAI_BASE_URL: `http://127.0.0.1:${providerPort}/v1/responses`
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": `trc_server-test-agent-chat-provider-${status}`
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: `cnv_server-test-agent-chat-provider-${status}`,
|
||
message: `provider ${status}`
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.traceId, `trc_server-test-agent-chat-provider-${status}`);
|
||
assert.equal(payload.error.code, "codex_cli_binary_missing");
|
||
assert.equal(payload.error.layer, "runner");
|
||
assert.equal(payload.error.retryable, false);
|
||
assert.equal(payload.error.blocker.traceId, `trc_server-test-agent-chat-provider-${status}`);
|
||
assert.equal(payload.error.blocker.retryable, false);
|
||
assert.equal(payload.error.blocker.capabilityLevel, "blocked");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
await new Promise((resolve, reject) => {
|
||
providerServer.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/agent/chat reports provider gaps without faking a reply", async () => {
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
PATH: "",
|
||
HWLAB_CODE_AGENT_PROVIDER: "codex-cli",
|
||
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
||
HWLAB_CODE_AGENT_CODEX_COMMAND: "/tmp/hwlab-missing-codex"
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json"
|
||
},
|
||
body: JSON.stringify({
|
||
message: "你好"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.match(payload.conversationId, /^cnv_/);
|
||
assert.match(payload.messageId, /^msg_/);
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(payload.model, "gpt-test");
|
||
assert.equal(payload.backend, "hwlab-cloud-api/codex-app-server-stdio");
|
||
assert.equal(Number.isNaN(Date.parse(payload.createdAt)), false);
|
||
assert.equal(Number.isNaN(Date.parse(payload.updatedAt)), false);
|
||
assert.equal(payload.error.code, "codex_cli_binary_missing");
|
||
assert.equal(payload.error.layer, "runner");
|
||
assert.equal(payload.error.retryable, false);
|
||
assert.match(payload.error.userMessage, /Codex CLI binary/u);
|
||
assert.match(payload.error.message, /Codex stdio long-lived session is unavailable/u);
|
||
assert.equal(payload.availability.status, "blocked");
|
||
assert.match(payload.availability.blocker, /Codex CLI command/u);
|
||
assert.equal(payload.availability.reason, "codex_cli_binary_missing");
|
||
assert.equal(payload.availability.runner.ready, false);
|
||
assert.equal(payload.availability.codexStdio.runtimeContract.binary.status, "missing");
|
||
assert.equal(payload.availability.codexStdio.runtimeContract.stdioProtocol.status, "blocked");
|
||
assert.ok(payload.availability.codexStdio.blockerCodes.includes("codex_cli_binary_missing"));
|
||
assert.equal(payload.availability.secretRefs[0].secretName, "hwlab-code-agent-provider");
|
||
assert.equal(payload.availability.secretRefs[0].secretKey, "openai-api-key");
|
||
assert.equal(payload.availability.secretRefs[0].redacted, true);
|
||
assert.match(payload.availability.summary, /will not use controlled-readonly-session-registry/u);
|
||
assert.equal(JSON.stringify(payload).includes("sk-"), false);
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
function delay(ms) {
|
||
return new Promise((resolve) => setTimeout(resolve, ms));
|
||
}
|
||
|
||
test("cloud api /v1/agent/chat does not call empty provider text fallback", async () => {
|
||
let providerCalled = false;
|
||
const server = createCloudApiServer({
|
||
callCodeAgentProvider: async () => {
|
||
providerCalled = true;
|
||
throw new Error("empty provider fallback must not be used");
|
||
}
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json"
|
||
},
|
||
body: JSON.stringify({
|
||
conversationId: "cnv_empty-provider-text",
|
||
message: "你好"
|
||
})
|
||
});
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.conversationId, "cnv_empty-provider-text");
|
||
assert.equal(payload.status, "failed");
|
||
assert.equal(payload.error.code, "codex_cli_binary_missing");
|
||
assert.equal(payload.provider, "codex-stdio");
|
||
assert.equal(Object.hasOwn(payload, "reply"), false);
|
||
assert.equal(providerCalled, false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1 exposes M3 IO control contract without generic frontend hardware RPC", async () => {
|
||
const server = createCloudApiServer({
|
||
env: {
|
||
HWLAB_M3_IO_CONTROL_ENABLED: "true",
|
||
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
|
||
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
|
||
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
|
||
},
|
||
m3IoRequestJson: m3ReadinessRequestJson
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const index = await fetch(`http://127.0.0.1:${port}/v1`);
|
||
assert.equal(index.status, 200);
|
||
const indexPayload = await index.json();
|
||
assert.equal(indexPayload.m3IoControl.route, "/v1/m3/io");
|
||
assert.equal(indexPayload.m3IoControl.boundaries.directFrontendGatewayOrBoxAccess, false);
|
||
assert.ok(indexPayload.methods.includes("m3.io.do.write"));
|
||
assert.ok(indexPayload.methods.includes("m3.io.di.read"));
|
||
|
||
const contract = await fetch(`http://127.0.0.1:${port}/v1/m3/io`);
|
||
assert.equal(contract.status, 200);
|
||
const payload = await contract.json();
|
||
assert.equal(payload.status, "available");
|
||
assert.equal(payload.controlReady, true);
|
||
assert.equal(payload.readiness.status, "ready");
|
||
assert.equal(payload.chain.sourceResourceId, "res_boxsimu_1");
|
||
assert.equal(payload.chain.targetResourceId, "res_boxsimu_2");
|
||
assert.equal(payload.boundaries.frontendCallsOnly, "/v1/m3/io");
|
||
assert.equal(payload.boundaries.patchPanelOwnsPropagation, true);
|
||
assert.equal(payload.boundaries.genericHardwareRpcExposedToFrontend, false);
|
||
|
||
const statusResponse = await fetch(`http://127.0.0.1:${port}/v1/m3/status`);
|
||
assert.equal(statusResponse.status, 200);
|
||
const statusPayload = await statusResponse.json();
|
||
assert.equal(statusPayload.route, "/v1/m3/status");
|
||
assert.equal(statusPayload.chain.sourceResourceId, "res_boxsimu_1");
|
||
assert.equal(statusPayload.chain.targetResourceId, "res_boxsimu_2");
|
||
assert.equal(statusPayload.gateways.length, 2);
|
||
assert.equal(statusPayload.gateways.every((gateway) => gateway.online === true), true);
|
||
assert.equal(statusPayload.boxes.find((box) => box.resourceId === "res_boxsimu_1").ports.DO1.value, false);
|
||
assert.equal(statusPayload.boxes.find((box) => box.resourceId === "res_boxsimu_2").ports.DI1.value, false);
|
||
assert.equal(statusPayload.patchPanel.connectionActive, true);
|
||
assert.equal(statusPayload.io.status, "live");
|
||
assert.equal(statusPayload.io.do1.resourceId, "res_boxsimu_1");
|
||
assert.equal(statusPayload.io.do1.port, "DO1");
|
||
assert.equal(statusPayload.io.do1.value, false);
|
||
assert.equal(statusPayload.io.do1.sourceKind, "DEV-LIVE");
|
||
assert.equal(statusPayload.io.di1.resourceId, "res_boxsimu_2");
|
||
assert.equal(statusPayload.io.di1.port, "DI1");
|
||
assert.equal(statusPayload.io.di1.value, false);
|
||
assert.equal(statusPayload.io.di1.sourceKind, "DEV-LIVE");
|
||
assert.equal(statusPayload.io.patchPanel.serviceId, "hwlab-patch-panel");
|
||
assert.equal(statusPayload.io.patchPanel.connectionActive, true);
|
||
assert.equal(statusPayload.operation.status, "blocked");
|
||
assert.equal(statusPayload.operation.operationId, null);
|
||
assert.equal(statusPayload.operation.blocker, "runtime_durable_adapter_missing");
|
||
assert.equal(statusPayload.trace.status, "blocked");
|
||
assert.equal(statusPayload.trace.traceId, null);
|
||
assert.equal(statusPayload.trace.blocker, "runtime_durable_adapter_missing");
|
||
assert.equal(statusPayload.audit.status, "blocked");
|
||
assert.equal(statusPayload.audit.auditId, null);
|
||
assert.equal(statusPayload.audit.blocker, "runtime_durable_adapter_missing");
|
||
assert.equal(statusPayload.runtimeDurable.status, "blocked");
|
||
assert.equal(statusPayload.runtimeDurable.green, false);
|
||
assert.equal(statusPayload.runtimeDurable.blocker, "runtime_durable_adapter_missing");
|
||
assert.equal(statusPayload.runtimeDurable.readStatus.audit, "read");
|
||
assert.equal(statusPayload.runtimeDurable.readStatus.evidence, "read");
|
||
assert.equal(statusPayload.runtimeDurable.auditReady, true);
|
||
assert.equal(statusPayload.runtimeDurable.evidenceReady, true);
|
||
assert.equal(statusPayload.trust.durableStatus, "blocked");
|
||
assert.notEqual(statusPayload.trust.durableStatus, "green");
|
||
assert.equal(statusPayload.trust.runtime.blocker, "runtime_durable_adapter_missing");
|
||
assert.equal(statusPayload.boundaries.frontendCallsOnly, "/v1/m3/status");
|
||
assert.equal(statusPayload.boundaries.directFrontendGatewayOrBoxAccess, false);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/m3/status clears nested runtime blocker when trusted durable evidence is green", async () => {
|
||
const server = createCloudApiServer({
|
||
runtimeStore: createGreenM3StatusRuntimeStore({ blocker: "runtime_durable_not_green" }),
|
||
env: {
|
||
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
|
||
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
|
||
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
|
||
},
|
||
m3IoRequestJson: m3ReadinessRequestJson
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const statusResponse = await fetch(`http://127.0.0.1:${port}/v1/m3/status`);
|
||
assert.equal(statusResponse.status, 200);
|
||
const statusPayload = await statusResponse.json();
|
||
|
||
assert.equal(statusPayload.contractVersion, "m3-status-v1");
|
||
assert.equal(statusPayload.status, "live");
|
||
assert.equal(statusPayload.sourceKind, "DEV-LIVE");
|
||
assert.equal(statusPayload.patchPanel.connectionActive, true);
|
||
assert.equal(statusPayload.trust.durableStatus, "green");
|
||
assert.equal(statusPayload.trust.blocker, null);
|
||
assert.equal(statusPayload.trust.runtime.status, "ready");
|
||
assert.equal(statusPayload.trust.runtime.blocker, null);
|
||
assert.equal(statusPayload.trust.operationId, "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f");
|
||
assert.equal(statusPayload.trust.traceId, "trc_hotfix_307_verify_1779527747043_read_false");
|
||
assert.equal(statusPayload.trust.auditId, "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded");
|
||
assert.equal(statusPayload.trust.evidenceId, "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded");
|
||
assert.equal(statusPayload.operation.status, "read");
|
||
assert.equal(statusPayload.operation.operationId, "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f");
|
||
assert.equal(statusPayload.operation.blocker, null);
|
||
assert.equal(statusPayload.trace.status, "read");
|
||
assert.equal(statusPayload.trace.traceId, "trc_hotfix_307_verify_1779527747043_read_false");
|
||
assert.equal(statusPayload.trace.blocker, null);
|
||
assert.equal(statusPayload.audit.status, "read");
|
||
assert.equal(statusPayload.audit.auditId, "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded");
|
||
assert.equal(statusPayload.audit.blocker, null);
|
||
assert.equal(statusPayload.runtimeDurable.status, "green");
|
||
assert.equal(statusPayload.runtimeDurable.green, true);
|
||
assert.equal(statusPayload.runtimeDurable.blocker, null);
|
||
assert.equal(statusPayload.runtimeDurable.auditReady, true);
|
||
assert.equal(statusPayload.runtimeDurable.evidenceReady, true);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/diagnostics/gate exposes Chinese single-table rows from live backend aggregation", async () => {
|
||
const runtimeStore = createCloudRuntimeStore({
|
||
now: () => "2026-05-23T00:00:00.000Z"
|
||
});
|
||
const server = createCloudApiServer({
|
||
runtimeStore,
|
||
env: {
|
||
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
|
||
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
|
||
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel",
|
||
HWLAB_COMMIT_ID: "abc123456789",
|
||
HWLAB_IMAGE_TAG: "abc1234"
|
||
},
|
||
m3IoRequestJson: async () => ({
|
||
ok: false,
|
||
status: 503,
|
||
body: {
|
||
error: {
|
||
message: "gateway offline"
|
||
}
|
||
}
|
||
})
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/diagnostics/gate`);
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.route, "/v1/diagnostics/gate");
|
||
assert.equal(payload.contractVersion, "gate-diagnostics-table-v1");
|
||
assert.equal(payload.sourceKind, "LIVE-BACKEND");
|
||
assert.equal(payload.liveBackend, true);
|
||
assert.deepEqual(payload.columns, ["类别", "检查项", "状态", "归因对象", "关键细节", "证据/trace", "更新时间", "下一步"]);
|
||
assert.equal(payload.safety.frontendHardcodedRows, false);
|
||
assert.equal(payload.safety.sourceFixturePromoted, false);
|
||
assert.equal(payload.safety.blockedIsGreen, false);
|
||
assert.equal(payload.safety.hardwareControlSurface, false);
|
||
assert.equal(payload.safety.codeAgentConversationSurface, false);
|
||
assert.ok(payload.rows.length >= 6);
|
||
assert.ok(payload.rows.every((row) => row.sourceKind === "LIVE-BACKEND"));
|
||
assert.ok(payload.rows.every((row) => ["通过", "阻塞", "失败", "待验证", "信息"].includes(row.status)));
|
||
assert.ok(payload.rows.some((row) => row.category === "M3" && row.status === "阻塞"));
|
||
assert.ok(payload.rows.some((row) => row.check === "/health/live readiness"));
|
||
assert.ok(payload.rows.some((row) => row.check === "audit.event.query"));
|
||
assert.ok(payload.rows.some((row) => row.check === "evidence.record.query"));
|
||
assert.ok(payload.rows.some((row) => row.check === "Cloud API 实况身份" && row.status === "通过"));
|
||
assert.equal(
|
||
payload.rows.some((row) => row.status === "阻塞" && row.statusKey === "pass"),
|
||
false,
|
||
"blocked rows must not be mapped to pass"
|
||
);
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
test("cloud api /v1/m3/io returns structured blocked payload when gateway is unavailable", async () => {
|
||
const runtimeStore = createCloudRuntimeStore({
|
||
now: () => "2026-05-23T00:00:00.000Z"
|
||
});
|
||
const server = createCloudApiServer({
|
||
runtimeStore,
|
||
env: {
|
||
HWLAB_M3_GATEWAY_SIMU_1_URL: "http://gateway-1",
|
||
HWLAB_M3_GATEWAY_SIMU_2_URL: "http://gateway-2",
|
||
HWLAB_M3_PATCH_PANEL_URL: "http://patch-panel"
|
||
},
|
||
m3IoRequestJson: async () => ({
|
||
ok: false,
|
||
status: 503,
|
||
body: {
|
||
error: {
|
||
message: "gateway offline"
|
||
}
|
||
}
|
||
})
|
||
});
|
||
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
||
|
||
try {
|
||
const { port } = server.address();
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/m3/io`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
"x-trace-id": "trc_http_m3_gateway_missing",
|
||
"x-actor-id": "usr_http_m3"
|
||
},
|
||
body: JSON.stringify({
|
||
action: "do.write",
|
||
value: true
|
||
})
|
||
});
|
||
|
||
assert.equal(response.status, 200);
|
||
const payload = await response.json();
|
||
assert.equal(payload.status, "blocked");
|
||
assert.equal(payload.accepted, false);
|
||
assert.equal(payload.route, "/v1/m3/io");
|
||
assert.equal(payload.method, "POST");
|
||
assert.equal(payload.traceId, "trc_http_m3_gateway_missing");
|
||
assert.equal(payload.operationId, null);
|
||
assert.equal(payload.auditId, null);
|
||
assert.equal(payload.evidenceId, null);
|
||
assert.equal(payload.audit.status, "not_written");
|
||
assert.equal(payload.evidence.status, "blocked");
|
||
assert.equal(payload.readback, null);
|
||
assert.match(payload.blocker.zh, /gateway 未注册\/不可用/u);
|
||
assert.equal(payload.controlPath.cloudApi, true);
|
||
assert.equal(payload.controlPath.frontendBypass, false);
|
||
assert.equal(payload.evidenceState.sourceKind, "BLOCKED");
|
||
} finally {
|
||
await new Promise((resolve, reject) => {
|
||
server.close((error) => (error ? reject(error) : resolve()));
|
||
});
|
||
}
|
||
});
|
||
|
||
async function postAgent(port, body) {
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
...(body.traceId ? { "x-trace-id": body.traceId } : {})
|
||
},
|
||
body: JSON.stringify(body)
|
||
});
|
||
assert.equal(response.status, 200);
|
||
return response.json();
|
||
}
|
||
|
||
async function pollAgentResult(port, traceId) {
|
||
let last = null;
|
||
for (let attempt = 0; attempt < 20; attempt += 1) {
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat/result/${encodeURIComponent(traceId)}`);
|
||
last = {
|
||
status: response.status,
|
||
body: await response.json()
|
||
};
|
||
if (response.status === 200) return last.body;
|
||
await delay(10);
|
||
}
|
||
throw new Error(`Code Agent result did not complete: ${JSON.stringify(last)}`);
|
||
}
|
||
|
||
async function createFakeCodexCommand() {
|
||
const root = await mkdtemp(path.join(os.tmpdir(), "hwlab-fake-codex-"));
|
||
const packageRoot = path.join(root, "node_modules", "@openai", "codex");
|
||
const command = path.join(packageRoot, "bin", "codex.js");
|
||
const native = path.join(root, "node_modules", "@openai", "codex-linux-x64", "vendor", "x86_64-unknown-linux-musl", "codex", "codex");
|
||
await mkdir(path.dirname(command), { recursive: true });
|
||
await mkdir(path.dirname(native), { recursive: true });
|
||
await writeFile(path.join(packageRoot, "package.json"), JSON.stringify({ name: "@openai/codex", version: "0.128.0" }), "utf8");
|
||
await writeFile(command, "#!/usr/bin/env node\nconsole.log('codex 0.128.0');\n", "utf8");
|
||
await writeFile(native, "#!/bin/sh\nexit 0\n", "utf8");
|
||
await chmod(command, 0o755);
|
||
await chmod(native, 0o755);
|
||
return { root, command };
|
||
}
|
||
|
||
async function prepareFakeCodexHome(codexHome) {
|
||
await mkdir(codexHome, { recursive: true });
|
||
await writeFile(path.join(codexHome, "config.toml"), "model = \"gpt-test\"\n", "utf8");
|
||
await writeFile(path.join(codexHome, "auth.json"), "{\"auth\":\"redacted-test-placeholder\"}\n", "utf8");
|
||
return codexHome;
|
||
}
|
||
|
||
function createFakeAppServerClient({ text = "真实 Codex stdio 回复", delayMs = 0, calls = null, complete = true, onTurnStarted = null } = {}) {
|
||
let notificationHandler = null;
|
||
let turn = 0;
|
||
let closed = false;
|
||
let closeResolve;
|
||
const closedPromise = new Promise((resolve) => {
|
||
closeResolve = resolve;
|
||
});
|
||
return {
|
||
closedPromise,
|
||
async initialize() {
|
||
calls?.push({ method: "initialize" });
|
||
return { initialized: true };
|
||
},
|
||
setNotificationHandler(handler) {
|
||
notificationHandler = typeof handler === "function" ? handler : null;
|
||
},
|
||
async startThread(args) {
|
||
calls?.push({ method: "thread/start", args });
|
||
const threadId = "thread_server_test_stdio";
|
||
notificationHandler?.({ method: "thread/started", params: { thread: { id: threadId } } });
|
||
return { threadId };
|
||
},
|
||
async resumeThread(args) {
|
||
calls?.push({ method: "thread/resume", args });
|
||
notificationHandler?.({ method: "thread/started", params: { thread: { id: args.threadId } } });
|
||
return { threadId: args.threadId };
|
||
},
|
||
async startTurn(args) {
|
||
calls?.push({ method: "turn/start", args });
|
||
turn += 1;
|
||
const turnId = `turn_server_test_stdio_${turn}`;
|
||
notificationHandler?.({ method: "turn/started", params: { turn: { id: turnId } } });
|
||
onTurnStarted?.({ args, turnId });
|
||
if (!complete) return { turnId };
|
||
if (delayMs > 0) await delay(delayMs);
|
||
notificationHandler?.({ method: "item/agentMessage/delta", params: { itemId: `item_server_test_stdio_${turn}`, delta: text } });
|
||
notificationHandler?.({ method: "item/completed", params: { item: { id: `item_server_test_stdio_${turn}`, type: "agentMessage", text } } });
|
||
notificationHandler?.({ method: "turn/completed", params: { turn: { id: turnId, status: "completed" } } });
|
||
return { turnId };
|
||
},
|
||
close() {
|
||
if (closed) return;
|
||
closed = true;
|
||
closeResolve?.({ code: null, signal: "SIGTERM" });
|
||
}
|
||
};
|
||
}
|
||
|
||
async function postAgentRaw(port, body, headers = {}) {
|
||
const response = await fetch(`http://127.0.0.1:${port}/v1/agent/chat`, {
|
||
method: "POST",
|
||
headers: {
|
||
"content-type": "application/json",
|
||
...headers
|
||
},
|
||
body
|
||
});
|
||
return {
|
||
status: response.status,
|
||
body: await response.json()
|
||
};
|
||
}
|
||
|
||
function escapeRegExp(value) {
|
||
return String(value).replace(/[.*+?^${}()|[\]\\]/gu, "\\$&");
|
||
}
|
||
|
||
function createGreenM3StatusRuntimeStore({ blocker = null } = {}) {
|
||
return {
|
||
async readiness() {
|
||
return {
|
||
adapter: "postgres",
|
||
durable: true,
|
||
durableRequested: true,
|
||
durableCapable: true,
|
||
ready: true,
|
||
status: "ready",
|
||
blocker,
|
||
reason: "Postgres durable runtime adapter completed live schema, migration, and read readiness queries",
|
||
liveRuntimeEvidence: true,
|
||
fixtureEvidence: false
|
||
};
|
||
},
|
||
async queryAuditEvents() {
|
||
return {
|
||
events: [
|
||
{
|
||
auditId: "aud_m3_di_read_d5afd84b-3a9d-475f-894f-dff8ea5f49ff_succeeded",
|
||
traceId: "trc_hotfix_307_verify_1779527747043_read_false",
|
||
actorType: "user",
|
||
actorId: "usr_m3_operator",
|
||
action: "m3.io.di.read",
|
||
targetType: "hardware_operation",
|
||
targetId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f",
|
||
projectId: "prj_mvp_topology",
|
||
gatewaySessionId: "gws_gwsimu_2",
|
||
operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f",
|
||
serviceId: "hwlab-cloud-api",
|
||
outcome: "succeeded",
|
||
metadata: {
|
||
route: "res_boxsimu_1:DO1->hwlab-patch-panel->res_boxsimu_2:DI1"
|
||
},
|
||
environment: "dev",
|
||
occurredAt: "2026-05-23T00:00:00.000Z"
|
||
}
|
||
],
|
||
count: 1
|
||
};
|
||
},
|
||
async queryEvidenceRecords() {
|
||
return {
|
||
records: [
|
||
{
|
||
evidenceId: "evd_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f_succeeded",
|
||
projectId: "prj_mvp_topology",
|
||
operationId: "op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f",
|
||
kind: "trace",
|
||
uri: "memory://hwlab-cloud-api/op_m3_di_read_75fc664f-e94f-41e8-816e-aff9fdb3666f/m3-io-control.json",
|
||
sha256: "0".repeat(64),
|
||
serviceId: "hwlab-cloud-api",
|
||
environment: "dev",
|
||
metadata: {
|
||
traceId: "trc_hotfix_307_verify_1779527747043_read_false",
|
||
route: "res_boxsimu_1:DO1->hwlab-patch-panel->res_boxsimu_2:DI1"
|
||
},
|
||
createdAt: "2026-05-23T00:00:00.000Z"
|
||
}
|
||
],
|
||
count: 1
|
||
};
|
||
}
|
||
};
|
||
}
|