034c90763c
Align Code Agent chat, live health, and workbench UI session facts for Codex stdio vs fallback paths.
3637 lines
143 KiB
JavaScript
3637 lines
143 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createServer as createHttpServer } from "node:http";
|
|
import { createServer as createTcpServer } 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 } from "./server.mjs";
|
|
import { validateCodeAgentChatSchema } from "./code-agent-chat.mjs";
|
|
import { createCodexStdioSessionManager } from "./codex-stdio-session.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 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, "read_only_long_lived_ready");
|
|
assert.equal(healthPayload.readiness.sessionRunner.ready, true);
|
|
assert.equal(healthPayload.readiness.sessionRunner.codexStdio, false);
|
|
assert.equal(healthPayload.readiness.sessionRunner.durableSession, false);
|
|
assert.equal(healthPayload.readiness.sessionRunner.longLivedSession, true);
|
|
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, true);
|
|
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, "partial");
|
|
assert.equal(healthPayload.codeAgent.agentKind, "controlled-readonly-session-registry");
|
|
assert.equal(healthPayload.codeAgent.partialReady, true);
|
|
assert.match(healthPayload.codeAgent.blocker, /Codex CLI command/u);
|
|
assert.equal(healthPayload.codeAgent.reason, "codex_cli_binary_missing");
|
|
assert.equal(healthPayload.codeAgent.runner.kind, "hwlab-readonly-runner");
|
|
assert.equal(healthPayload.codeAgent.runner.ready, true);
|
|
assert.equal(healthPayload.codeAgent.capabilityLevel, "read-only-session-tools");
|
|
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, "partial");
|
|
assert.equal(healthLivePayload.codeAgent.ready, false);
|
|
assert.equal(healthLivePayload.codeAgent.partialReady, true);
|
|
assert.equal(healthLivePayload.codeAgent.capabilityLevel, "read-only-session-tools");
|
|
|
|
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 repo-owned artifact 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: "mgrabcdef123456",
|
|
commit: { id: "mgrabcdef123456", source: "test" },
|
|
image: {
|
|
reference: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:mgrabcd",
|
|
tag: "mgrabcd",
|
|
digest: "sha256:" + "2".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:mgrcat",
|
|
namespace: "hwlab-dev",
|
|
profile: "dev",
|
|
healthPath: "/health/live",
|
|
replicas: 1
|
|
},
|
|
{
|
|
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: "mgrcatabcdef123456",
|
|
image: "127.0.0.1:5000/hwlab/hwlab-agent-mgr:mgrcat",
|
|
imageTag: "mgrcat",
|
|
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-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"
|
|
}
|
|
]
|
|
},
|
|
artifactReport: {
|
|
artifactPublish: {
|
|
services: []
|
|
}
|
|
}
|
|
};
|
|
|
|
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/missing-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-mgr");
|
|
assert.equal(payload.latest.build.createdAt, "2026-05-23T04:20:00.000Z");
|
|
assert.equal(payload.latest.image.tag, "mgrabcd");
|
|
assert.equal(payload.latest.commit.id, "mgrabcdef123456");
|
|
assert.equal(payload.latest.build.metadataSource, "artifact-catalog:buildCreatedAt");
|
|
assert.match(payload.latest.build.liveHealthMissingReason, /health payload 缺少 build\.createdAt/u);
|
|
assert.equal(payload.counts.total, 15);
|
|
assert.equal(payload.counts.external, 2);
|
|
assert.equal(payload.counts.withBuildTime, 4);
|
|
const extra = payload.services.find((service) => service.serviceId === "hwlab-extra-lab");
|
|
assert.equal(extra.build.createdAt, "2026-05-23T03:00:00.000Z");
|
|
assert.match(extra.build.liveHealthMissingReason, /live health 不可用/u);
|
|
assert.ok(payload.services.some((service) => service.serviceId === "hwlab-agent-worker" && /构建时间不可用:hwlab-agent-worker 当前 hwlab-dev desired replicas=0/u.test(service.build.unavailableReason)));
|
|
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 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, "read_only_long_lived_ready");
|
|
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("127.0.0.1"), 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-mcp-stdio-runner",
|
|
provider: "codex-stdio",
|
|
backend: "hwlab-cloud-api/codex-mcp-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,
|
|
requiredTools: ["codex", "codex-reply"],
|
|
toolsObserved: ["codex", "codex-reply"],
|
|
missingTools: [],
|
|
command: "codex mcp-server"
|
|
},
|
|
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,
|
|
requiredTools: ["codex", "codex-reply"],
|
|
toolsObserved: ["codex", "codex-reply"],
|
|
missingTools: [],
|
|
command: "codex mcp-server"
|
|
},
|
|
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,
|
|
requiredTools: ["codex", "codex-reply"],
|
|
toolsObserved: ["codex", "codex-reply"],
|
|
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-mcp-stdio-runner",
|
|
sessionMode: "codex-mcp-stdio-long-lived",
|
|
capabilityLevel: "long-lived-codex-stdio-session",
|
|
implementationType: "repo-owned-codex-mcp-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-mcp-stdio",
|
|
content: `stdio pwd\nrunner tool evidence: pwd:completed\n${workspace}`,
|
|
workspace,
|
|
sandbox: "workspace-write",
|
|
session,
|
|
sessionMode: "codex-mcp-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-mcp-stdio-session",
|
|
runnerLimitations: ["hardware-control-via-cloud-api-only", "secret-values-redacted"],
|
|
codexStdioFeasibility: feasibility,
|
|
longLivedSessionGate: {
|
|
status: "pass",
|
|
pass: true,
|
|
provider: "codex-stdio",
|
|
runnerKind: "codex-mcp-stdio-runner",
|
|
sessionMode: "codex-mcp-stdio-long-lived",
|
|
implementationType: "repo-owned-codex-mcp-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-mcp-stdio-runner",
|
|
provider: "codex-stdio",
|
|
backend: "hwlab-cloud-api/codex-mcp-stdio",
|
|
workspace,
|
|
sandbox: "workspace-write",
|
|
session: "codex-mcp-stdio-long-lived",
|
|
sessionMode: "codex-mcp-stdio-long-lived",
|
|
sessionId: session.sessionId,
|
|
implementationType: "repo-owned-codex-mcp-stdio-session",
|
|
codexStdio: true,
|
|
longLivedSession: true,
|
|
durableSession: true,
|
|
writeCapable: true,
|
|
readOnly: false,
|
|
capabilityLevel: "long-lived-codex-stdio-session"
|
|
},
|
|
runnerTrace: {
|
|
traceId,
|
|
runnerKind: "codex-mcp-stdio-runner",
|
|
workspace,
|
|
sandbox: "workspace-write",
|
|
sessionMode: "codex-mcp-stdio-long-lived",
|
|
sessionId: session.sessionId,
|
|
sessionStatus: "idle",
|
|
turn: 1,
|
|
implementationType: "repo-owned-codex-mcp-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: "mcp",
|
|
command: "codex mcp-server",
|
|
toolName: "codex",
|
|
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-mcp-stdio");
|
|
assert.equal(payload.codeAgent.mode, "codex-stdio");
|
|
assert.equal(payload.codeAgent.status, "partial");
|
|
assert.match(payload.codeAgent.blocker, /Codex CLI command/u);
|
|
assert.equal(payload.codeAgent.reason, "codex_cli_binary_missing");
|
|
assert.equal(payload.codeAgent.runner.kind, "hwlab-readonly-runner");
|
|
assert.equal(payload.codeAgent.runner.ready, true);
|
|
assert.equal(payload.codeAgent.capabilityLevel, "read-only-session-tools");
|
|
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, true);
|
|
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, /受控只读 runner/u);
|
|
assert.match(payload.codeAgent.summary, /long-lived Codex stdio/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, "partial");
|
|
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, true);
|
|
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 mkdir(codexHome, { recursive: true });
|
|
const manager = createCodexStdioSessionManager({
|
|
createRpcClient: async () => ({
|
|
async initialize() {
|
|
return { tools: ["codex", "codex-reply"] };
|
|
},
|
|
async listTools() {
|
|
return ["codex", "codex-reply"];
|
|
},
|
|
close() {}
|
|
})
|
|
});
|
|
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, ["codex", "codex-reply"]);
|
|
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 mkdir(codexHome, { recursive: true });
|
|
await writeFile(workspaceFile, "not a directory\n", "utf8");
|
|
const manager = createCodexStdioSessionManager({
|
|
createRpcClient: async () => ({
|
|
async initialize() {
|
|
return { tools: ["codex", "codex-reply"] };
|
|
},
|
|
async listTools() {
|
|
return ["codex", "codex-reply"];
|
|
},
|
|
close() {}
|
|
})
|
|
});
|
|
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 returns structured completed Code Agent payload", async () => {
|
|
const server = createCloudApiServer({
|
|
callCodeAgentProvider: async ({ providerPlan, message, traceId }) => ({
|
|
provider: providerPlan.provider,
|
|
model: providerPlan.model,
|
|
backend: providerPlan.backend,
|
|
content: `真实 provider stub: ${message} / ${traceId}`,
|
|
usage: null,
|
|
providerTrace: {
|
|
source: "test-provider"
|
|
}
|
|
})
|
|
});
|
|
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, "completed");
|
|
assert.equal(payload.traceId, "trc_server-test-agent-chat");
|
|
assert.equal(payload.provider.length > 0, true);
|
|
assert.equal(payload.model.length > 0, true);
|
|
assert.equal(payload.backend.length > 0, true);
|
|
assert.equal(Number.isNaN(Date.parse(payload.createdAt)), false);
|
|
assert.equal(Number.isNaN(Date.parse(payload.updatedAt)), false);
|
|
assert.match(payload.reply.content, /HWLAB 工作台/);
|
|
assert.equal(Object.hasOwn(payload, "error"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("OpenAI fallback text chat does not satisfy Codex stdio capability gate", async () => {
|
|
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 ({ providerPlan }) => ({
|
|
provider: providerPlan.provider,
|
|
model: providerPlan.model,
|
|
backend: providerPlan.backend,
|
|
content: "普通文本回复,不包含 runner/tool/session 能力。",
|
|
usage: null,
|
|
providerTrace: {
|
|
source: "test-provider"
|
|
}
|
|
})
|
|
});
|
|
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, "completed");
|
|
assert.equal(payload.provider, "openai-responses");
|
|
assert.equal(payload.backend, "hwlab-cloud-api/openai-responses");
|
|
assert.equal(payload.capabilityLevel, "text-chat-only");
|
|
assert.equal(payload.runner.kind, "openai-responses-fallback");
|
|
assert.equal(payload.runner.codexStdio, false);
|
|
assert.equal(payload.longLivedSessionGate.status, "blocked");
|
|
assert.equal(payload.blocker.code, "text_chat_only_fallback");
|
|
const capability = classifyCodexRunnerCapability(payload, { httpStatus: 200 });
|
|
assert.equal(capability.capabilityPass, false);
|
|
assert.equal(capability.blocker, "openai-fallback-not-runner");
|
|
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, "read-only-session-tools");
|
|
assert.equal(payload.codeAgent.runner.kind, "hwlab-readonly-runner");
|
|
assert.equal(payload.codeAgent.runner.codexStdio, false);
|
|
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-mcp-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-mcp-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-mcp-stdio-long-lived");
|
|
assert.equal(payload.sessionMode, "codex-mcp-stdio-long-lived");
|
|
assert.equal(payload.implementationType, "repo-owned-codex-mcp-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-mcp-stdio-runner");
|
|
assert.equal(payload.runnerTrace.sessionMode, "codex-mcp-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 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-mcp-stdio-runner");
|
|
assert.equal(payload.codeAgent.runner.sessionMode, "codex-mcp-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-mcp-stdio-long-lived");
|
|
assert.equal(payload.codeAgent.sessionRegistry.kind, "codex-stdio-session-registry");
|
|
assert.deepEqual(payload.codeAgent.sessionRegistry.statuses, ["creating", "ready", "busy", "idle", "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-mcp-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 runs Codex stdio pwd through controlled command path without OpenAI fallback", 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 mkdir(codexHome, { recursive: true });
|
|
const codexToolCalls = [];
|
|
const manager = createCodexStdioSessionManager({
|
|
idFactory: () => "ses_server_stdio_pwd_sidecar",
|
|
createRpcClient: async () => ({
|
|
async initialize() {
|
|
return { tools: ["codex", "codex-reply"] };
|
|
},
|
|
async listTools() {
|
|
return ["codex", "codex-reply"];
|
|
},
|
|
async callTool(name, args) {
|
|
codexToolCalls.push({ name, args });
|
|
throw new Error("model turn must not be needed for pwd");
|
|
},
|
|
close() {}
|
|
})
|
|
});
|
|
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-mcp-stdio");
|
|
assert.equal(payload.runner.kind, "codex-mcp-stdio-runner");
|
|
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
|
|
assert.equal(payload.workspace, workspace);
|
|
assert.equal(payload.sessionMode, "codex-mcp-stdio-long-lived");
|
|
assert.equal(payload.longLivedSessionGate.status, "pass");
|
|
assert.equal(payload.providerTrace.sidecarOnly, true);
|
|
assert.equal(payload.providerTrace.toolName, "workspace-sidecar");
|
|
assert.deepEqual(codexToolCalls, []);
|
|
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "pwd" && toolCall.status === "completed" && toolCall.stdout === workspace));
|
|
assert.match(payload.reply.content, new RegExp(workspace.replace(/[.*+?^${}()|[\]\\]/gu, "\\$&")));
|
|
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 discovers skills through Codex stdio command path", 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 mkdir(codexHome, { recursive: true });
|
|
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 () => ({
|
|
async initialize() {
|
|
return { tools: ["codex", "codex-reply"] };
|
|
},
|
|
async listTools() {
|
|
return ["codex", "codex-reply"];
|
|
},
|
|
async callTool() {
|
|
throw new Error("model turn must not be needed for skills discovery");
|
|
},
|
|
close() {}
|
|
})
|
|
})
|
|
});
|
|
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-mcp-stdio");
|
|
assert.equal(payload.capabilityLevel, "long-lived-codex-stdio-session");
|
|
assert.equal(payload.providerTrace.sidecarOnly, true);
|
|
assert.equal(payload.skills.status, "ready");
|
|
assert.ok(payload.skills.items.some((skill) => skill.name === "hwlab-test-skill"));
|
|
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "completed"));
|
|
assert.match(payload.reply.content, /hwlab-test-skill/u);
|
|
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 reports Codex stdio skills_unavailable as structured blocker", 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 mkdir(codexHome, { recursive: true });
|
|
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 () => ({
|
|
async initialize() {
|
|
return { tools: ["codex", "codex-reply"] };
|
|
},
|
|
async listTools() {
|
|
return ["codex", "codex-reply"];
|
|
},
|
|
async callTool() {
|
|
throw new Error("model turn must not be needed for skills discovery");
|
|
},
|
|
close() {}
|
|
})
|
|
})
|
|
});
|
|
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-mcp-stdio");
|
|
assert.equal(payload.error.code, "skills_unavailable");
|
|
assert.equal(payload.error.userMessage, "Code Agent Skill 清单未挂载或不可读,需要补齐运行时配置。");
|
|
assert.equal(payload.capabilityLevel, "blocked");
|
|
assert.equal(payload.skills.status, "blocked");
|
|
assert.equal(payload.skills.blockers[0].code, "skills_unavailable");
|
|
assert.ok(payload.toolCalls.some((toolCall) => toolCall.name === "skills.discover" && toolCall.status === "blocked"));
|
|
assert.equal(payload.session.status, "failed");
|
|
assert.equal(payload.session.statusReason, "skills_unavailable");
|
|
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 to /v1/m3/io only", 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"
|
|
},
|
|
callCodeAgentProvider: async () => {
|
|
throw new Error("OpenAI fallback must not handle M3 IO");
|
|
},
|
|
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: "请通过 M3 DO1 写入 false,并回读 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.toolCalls[0].name, "hwlab-agent-runtime.m3-io");
|
|
assert.equal(payload.toolCalls[0].route, "/v1/m3/io");
|
|
assert.equal(payload.toolCalls[0].method, "POST");
|
|
assert.equal(payload.toolCalls[0].accepted, true);
|
|
assert.equal(payload.toolCalls[0].operationId, "op_m3_do_write_server_skill");
|
|
assert.equal(payload.toolCalls[0].traceId, "trc_server-test-m3-skill");
|
|
assert.equal(payload.toolCalls[0].auditId, "aud_m3_do_write_server_skill_succeeded");
|
|
assert.equal(payload.toolCalls[0].evidenceId, "evd_m3_do_write_server_skill_succeeded");
|
|
assert.equal(payload.toolCalls[0].readback.value, false);
|
|
assert.equal(payload.providerTrace.fallbackUsed, false);
|
|
assert.equal(payload.providerTrace.method, "POST");
|
|
assert.equal(payload.providerTrace.readback.value, false);
|
|
assert.equal(payload.runner.safety.directGatewayCallsAllowed, false);
|
|
assert.equal(payload.runner.safety.directBoxSimuCallsAllowed, false);
|
|
assert.equal(payload.runner.safety.directPatchPanelCallsAllowed, false);
|
|
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);
|
|
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, false);
|
|
assert.equal(calls[0].url.includes("gateway-simu"), false);
|
|
assert.equal(calls[0].url.includes("box-simu"), false);
|
|
assert.equal(calls[0].url.includes("patch-panel"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat routes Codex stdio M3 request 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 manager = createCodexStdioSessionManager({
|
|
idFactory: () => "ses_server_stdio_m3_skill",
|
|
createRpcClient: async () => ({
|
|
async initialize() {
|
|
return { tools: ["codex", "codex-reply"] };
|
|
},
|
|
async listTools() {
|
|
return ["codex", "codex-reply"];
|
|
},
|
|
async callTool() {
|
|
return {
|
|
structuredContent: {
|
|
threadId: "thread_server_stdio_m3_skill",
|
|
content: "stdio session ready for M3 skill."
|
|
}
|
|
};
|
|
},
|
|
close() {}
|
|
})
|
|
});
|
|
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, "codex-stdio");
|
|
assert.equal(payload.runner.kind, "codex-mcp-stdio-runner");
|
|
assert.equal(payload.sessionMode, "codex-mcp-stdio-long-lived");
|
|
assert.equal(payload.longLivedSessionGate.status, "pass");
|
|
assert.equal(payload.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.ready);
|
|
assert.equal(payload.toolCalls.length, 2);
|
|
assert.equal(payload.toolCalls[0].type, "codex-stdio");
|
|
const skillTool = payload.toolCalls[1];
|
|
assert.equal(skillTool.type, "skill-cli");
|
|
assert.equal(skillTool.name, "hwlab-agent-runtime.m3-io");
|
|
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.equal(payload.providerTrace.codexStdio, true);
|
|
assert.equal(payload.providerTrace.fallbackUsed, false);
|
|
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.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 request and without toolCall URL leakage", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-direct-target-"));
|
|
const calls = [];
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HWLAB_CODE_AGENT_WORKSPACE: workspace,
|
|
HWLAB_CODE_AGENT_HWLAB_API_BASE_URL: "http://hwlab-gateway-simu-1.hwlab-dev.svc.cluster.local:7101"
|
|
},
|
|
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.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
|
assert.equal(payload.toolCalls[0].name, "hwlab-agent-runtime.m3-io");
|
|
assert.equal(payload.toolCalls[0].status, "blocked");
|
|
assert.equal(payload.toolCalls[0].route, "/v1/m3/io");
|
|
assert.equal(payload.toolCalls[0].method, "POST");
|
|
assert.equal(payload.toolCalls[0].blocker.code, "direct_hardware_target_blocked");
|
|
assert.equal(payload.toolCalls[0].hwlabApi.redactedUrl, null);
|
|
assert.equal(payload.blocker.code, "m3_readiness_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()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat reports M3 Skill CLI missing API base as structured blocker", async () => {
|
|
const workspace = await mkdtemp(path.join(os.tmpdir(), "hwlab-agent-m3-api-base-missing-"));
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HWLAB_CODE_AGENT_WORKSPACE: workspace,
|
|
HWLAB_CODE_AGENT_REQUIRE_HWLAB_API_BASE_URL: "1"
|
|
},
|
|
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.capabilityLevel, HWLAB_M3_IO_CAPABILITY_LEVELS.blocked);
|
|
assert.equal(payload.blocker.code, "m3_readiness_blocked");
|
|
assert.equal(payload.toolCalls[0].blocker.code, "skill_cli_api_base_missing");
|
|
assert.deepEqual(payload.toolCalls[0].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()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat reuses controlled read-only session and exposes bounded file tools", 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, "completed");
|
|
assert.equal(first.sessionReuse.reused, false);
|
|
assert.equal(first.sessionReuse.turn, 1);
|
|
assert.equal(first.toolCalls[0].name, "ls");
|
|
assert.match(first.toolCalls[0].stdout, /alpha\.txt/u);
|
|
|
|
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, "completed");
|
|
assert.equal(second.sessionId, first.sessionId);
|
|
assert.equal(second.sessionReuse.reused, true);
|
|
assert.equal(second.sessionReuse.turn, 2);
|
|
assert.equal(second.toolCalls[0].name, "cat");
|
|
assert.equal(second.toolCalls[0].stdout, "alpha\nbeta\n");
|
|
|
|
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, "completed");
|
|
assert.equal(third.sessionReuse.reused, true);
|
|
assert.equal(third.sessionReuse.turn, 3);
|
|
assert.equal(third.toolCalls[0].name, "rg --files");
|
|
assert.match(third.toolCalls[0].stdout, /src\/main\.mjs/u);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
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, "security_blocked");
|
|
assert.equal(payload.toolCalls[0].name, "cat");
|
|
assert.equal(payload.toolCalls[0].status, "blocked");
|
|
assert.equal(payload.capabilityLevel, "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 discovers skills manifest with source and version evidence", 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, "completed");
|
|
assert.equal(payload.provider, "codex-readonly-runner");
|
|
assert.equal(payload.capabilityLevel, "read-only-session-tools");
|
|
assert.equal(payload.skills.status, "ready");
|
|
assert.equal(payload.skills.items[0].name, "alpha-skill");
|
|
assert.equal(payload.skills.items[0].summary, "Alpha skill summary.");
|
|
assert.equal(payload.skills.items[0].version, "1.2.3");
|
|
assert.equal(payload.skills.items[0].commit, "abc123def456");
|
|
assert.equal(payload.toolCalls[0].name, "skills.discover");
|
|
assert.match(payload.reply.content, /alpha-skill/u);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat keeps slow skills discovery running past legacy 4500ms timeout", 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 server = createCloudApiServer({
|
|
env: {
|
|
PATH: process.env.PATH,
|
|
HWLAB_CODE_AGENT_WORKSPACE: workspace
|
|
},
|
|
skillsDirs: [skillsDir],
|
|
skillsDirsExact: true,
|
|
skillsDiscoveryDelayMs: 4600
|
|
});
|
|
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-slow-skills"
|
|
},
|
|
body: JSON.stringify({
|
|
conversationId: "cnv_server-test-slow-skills",
|
|
message: "请列出你可用的所有 skills"
|
|
})
|
|
});
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(Date.now() - startedAt >= 4500, true);
|
|
assert.equal(payload.status, "completed");
|
|
assert.equal(payload.provider, "codex-readonly-runner");
|
|
assert.equal(payload.backend, "hwlab-cloud-api/codex-readonly-runner");
|
|
assert.equal(payload.traceId, "trc_server-test-slow-skills");
|
|
assert.equal(payload.capabilityLevel, "read-only-session-tools");
|
|
assert.equal(payload.sessionMode, "controlled-readonly-session-registry");
|
|
assert.equal(payload.toolCalls[0].name, "skills.discover");
|
|
assert.equal(payload.toolCalls[0].status, "completed");
|
|
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, "hwlab-readonly-runner");
|
|
assert.equal(payload.runnerTrace.traceId, "trc_server-test-slow-skills");
|
|
assert.ok(payload.runnerTrace.events.includes("tool:skills.discover:completed"));
|
|
assert.equal(Object.hasOwn(payload, "error"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat reports structured skills_unavailable blocker", 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-readonly-runner");
|
|
assert.equal(payload.error.code, "skills_unavailable");
|
|
assert.equal(payload.capabilityLevel, "blocked");
|
|
assert.equal(payload.skills.status, "blocked");
|
|
assert.equal(payload.skills.blockers[0].sourceIssue, "pikasTech/HWLAB#136");
|
|
assert.ok(payload.skills.blockers[0].linkedIssues.includes("pikasTech/HWLAB#237"));
|
|
assert.equal(payload.toolCalls[0].status, "blocked");
|
|
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 reports unsupported read-only tool as blocked", 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.equal(payload.error.code, "tool_unavailable");
|
|
assert.equal(payload.capabilityLevel, "blocked");
|
|
assert.equal(payload.toolCalls[0].name, "grep");
|
|
assert.equal(payload.toolCalls[0].status, "blocked");
|
|
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 uses streaming OpenAI Responses and parses SSE text", 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, "completed");
|
|
assert.equal(payload.conversationId, "cnv_server-test-agent-chat-stream");
|
|
assert.equal(payload.traceId, "trc_server-test-agent-chat-stream");
|
|
assert.equal(payload.provider, "openai-responses");
|
|
assert.equal(payload.model, "gpt-test");
|
|
assert.equal(payload.providerTrace.responseId, "resp_server_test_stream");
|
|
assert.equal(payload.reply.content, "HWLAB Code Agent streaming ready.");
|
|
assert.equal(JSON.stringify(payload).includes("test-openai-key-material"), false);
|
|
|
|
assert.equal(providerRequests.length, 1);
|
|
assert.equal(providerRequests[0].method, "POST");
|
|
assert.equal(providerRequests[0].url, "/v1/responses");
|
|
assert.equal(providerRequests[0].authorizationPresent, true);
|
|
assert.match(providerRequests[0].accept, /text\/event-stream/u);
|
|
assert.equal(providerRequests[0].contentType, "application/json");
|
|
assert.equal(providerRequests[0].body.model, "gpt-test");
|
|
assert.equal(providerRequests[0].body.store, false);
|
|
assert.equal(providerRequests[0].body.stream, true);
|
|
} 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 accepts provider success delayed beyond legacy 4500ms UI timeout", async () => {
|
|
const server = createCloudApiServer({
|
|
callCodeAgentProvider: async ({ providerPlan, message, traceId }) => {
|
|
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, "completed");
|
|
assert.equal(payload.conversationId, "cnv_server-test-agent-chat-delayed");
|
|
assert.equal(payload.traceId, "trc_server-test-agent-chat-delayed");
|
|
assert.match(payload.reply.content, /稍后回答/u);
|
|
assert.equal(Object.hasOwn(payload, "error"), false);
|
|
} finally {
|
|
await new Promise((resolve, reject) => {
|
|
server.close((error) => (error ? reject(error) : resolve()));
|
|
});
|
|
}
|
|
});
|
|
|
|
test("cloud api /v1/agent/chat keeps delayed provider failure structured beyond legacy 4500ms UI timeout", async () => {
|
|
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 }) => {
|
|
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, "openai-responses");
|
|
assert.equal(payload.error.code, "provider_unavailable");
|
|
assert.equal(payload.error.providerStatus, 503);
|
|
assert.match(payload.error.message, /HTTP 503/u);
|
|
assert.equal(payload.backend, "hwlab-cloud-api/openai-responses");
|
|
assert.equal(payload.availability.fallback.backend, "hwlab-cloud-api/openai-responses");
|
|
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 reports provider timeout as failed without a reply", 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, "provider_timeout");
|
|
assert.equal(payload.error.layer, "provider");
|
|
assert.equal(payload.error.retryable, true);
|
|
assert.match(payload.error.userMessage, /超时/u);
|
|
assert.match(payload.error.message, /timed out after 50ms/u);
|
|
assert.equal(payload.provider, "openai-responses");
|
|
assert.equal(payload.backend, "hwlab-cloud-api/openai-responses");
|
|
assert.equal(payload.availability.endpoint, "POST /v1/agent/chat");
|
|
assert.equal(payload.availability.runner.kind, "hwlab-readonly-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 reports OpenAI provider 502 and 503 as failed blocked payloads", 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, "provider_unavailable");
|
|
assert.equal(payload.error.layer, "provider");
|
|
assert.equal(payload.error.retryable, true);
|
|
assert.match(payload.error.userMessage, /provider/u);
|
|
assert.equal(payload.error.providerStatus, status);
|
|
assert.match(payload.error.message, new RegExp(`HTTP ${status}`));
|
|
assert.equal(payload.error.blocker.traceId, `trc_server-test-agent-chat-provider-${status}`);
|
|
assert.equal(payload.error.blocker.retryable, true);
|
|
assert.equal(payload.error.blocker.capabilityLevel, "blocked");
|
|
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-cli");
|
|
assert.equal(payload.model, "gpt-test");
|
|
assert.equal(payload.backend, "hwlab-cloud-api/codex-cli");
|
|
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 CLI command is not available/);
|
|
assert.match(payload.error.nextEvidence, /HWLAB_CODE_AGENT_CODEX_COMMAND|PATH/u);
|
|
assert.deepEqual(payload.error.missingCommands, ["/tmp/hwlab-missing-codex"]);
|
|
assert.ok(payload.error.missingEnv.includes("OPENAI_API_KEY"));
|
|
assert.ok(payload.error.missingConfig.includes("OPENAI_API_KEY"));
|
|
assert.ok(payload.error.missingConfig.includes("command:/tmp/hwlab-missing-codex"));
|
|
assert.equal(payload.availability.status, "partial");
|
|
assert.match(payload.availability.blocker, /Codex CLI command/u);
|
|
assert.equal(payload.availability.reason, "codex_cli_binary_missing");
|
|
assert.equal(payload.availability.runner.ready, true);
|
|
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, /受控只读 runner/u);
|
|
assert.match(payload.availability.summary, /hwlab-code-agent-provider\/openai-api-key/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 complete on empty provider text", async () => {
|
|
const server = createCloudApiServer({
|
|
callCodeAgentProvider: async ({ providerPlan }) => ({
|
|
provider: providerPlan.provider,
|
|
model: providerPlan.model,
|
|
backend: providerPlan.backend,
|
|
content: " ",
|
|
usage: null,
|
|
providerTrace: {
|
|
source: "empty-test-provider"
|
|
}
|
|
})
|
|
});
|
|
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, "provider_unavailable");
|
|
assert.match(payload.error.message, /no assistant text/);
|
|
assert.equal(Object.hasOwn(payload, "reply"), 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.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");
|
|
} 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 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 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 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
|
|
};
|
|
}
|
|
};
|
|
}
|