1241 lines
49 KiB
JavaScript
1241 lines
49 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { createServer as createHttpServer } from "node:http";
|
|
import { createServer as createTcpServer } from "node:net";
|
|
import test from "node:test";
|
|
|
|
import { createCloudApiServer } from "./server.mjs";
|
|
import { createCloudRuntimeStore } from "../db/runtime-store.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";
|
|
|
|
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"
|
|
}
|
|
});
|
|
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(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.codeAgent.status, "blocked");
|
|
assert.equal(healthPayload.codeAgent.blocker, "凭证缺口");
|
|
assert.equal(healthPayload.codeAgent.reason, "provider_unavailable");
|
|
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(healthLivePayload.db.ready, false);
|
|
assert.equal(healthLivePayload.codeAgent.status, "blocked");
|
|
assert.equal(healthLivePayload.codeAgent.ready, false);
|
|
|
|
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 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.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(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 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;
|
|
}
|
|
|
|
test("cloud api /v1 describes Code Agent provider blocker without leaking secret values", async () => {
|
|
const server = createCloudApiServer({
|
|
env: {
|
|
PATH: "",
|
|
HWLAB_CODE_AGENT_PROVIDER: "openai",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test"
|
|
}
|
|
});
|
|
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, "openai-responses");
|
|
assert.equal(payload.codeAgent.model, "gpt-test");
|
|
assert.equal(payload.codeAgent.backend, "hwlab-cloud-api/openai-responses");
|
|
assert.equal(payload.codeAgent.mode, "openai");
|
|
assert.equal(payload.codeAgent.status, "blocked");
|
|
assert.match(payload.codeAgent.blocker, /DEV Code Agent OpenAI base URL is missing/u);
|
|
assert.equal(payload.codeAgent.reason, "provider_unavailable");
|
|
assert.deepEqual(payload.codeAgent.missingEnv, ["OPENAI_API_KEY", "HWLAB_CODE_AGENT_OPENAI_BASE_URL"]);
|
|
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.egress.present, false);
|
|
assert.equal(payload.codeAgent.egress.valueRedacted, true);
|
|
assert.equal(payload.codeAgent.safety.secretMaterialRead, false);
|
|
assert.match(payload.codeAgent.summary, /真实后端已接入/u);
|
|
assert.match(payload.codeAgent.summary, /hwlab-code-agent-provider\/openai-api-key/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: "openai",
|
|
HWLAB_CODE_AGENT_MODEL: "gpt-test",
|
|
HWLAB_CODE_AGENT_OPENAI_BASE_URL: "https://api.openai.com/v1/responses"
|
|
}
|
|
});
|
|
await new Promise((resolve) => server.listen(0, "127.0.0.1", resolve));
|
|
|
|
try {
|
|
const { port } = server.address();
|
|
const response = await fetch(`http://127.0.0.1:${port}/v1`);
|
|
assert.equal(response.status, 200);
|
|
const payload = await response.json();
|
|
assert.equal(payload.codeAgent.status, "blocked");
|
|
assert.equal(payload.codeAgent.egress.directPublicOpenAi, true);
|
|
assert.match(payload.codeAgent.blocker, /public api\.openai\.com/u);
|
|
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 /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("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_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: "列出你能使用的所有skill"
|
|
})
|
|
});
|
|
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, /列出你能使用的所有skill/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 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_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_unavailable");
|
|
assert.match(payload.error.message, /timed out after 50ms/u);
|
|
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_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.providerStatus, status);
|
|
assert.match(payload.error.message, new RegExp(`HTTP ${status}`));
|
|
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"
|
|
}
|
|
});
|
|
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, "provider_unavailable");
|
|
assert.match(payload.error.message, /Codex CLI command is not available/);
|
|
assert.deepEqual(payload.error.missingCommands, ["codex"]);
|
|
assert.ok(payload.error.missingEnv.includes("OPENAI_API_KEY"));
|
|
assert.equal(payload.availability.status, "blocked");
|
|
assert.equal(payload.availability.blocker, "凭证缺口");
|
|
assert.equal(payload.availability.reason, "provider_unavailable");
|
|
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, /真实后端已接入/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"
|
|
}
|
|
});
|
|
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);
|
|
|
|
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.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);
|
|
} 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.traceId, "trc_http_m3_gateway_missing");
|
|
assert.equal(payload.operationId, null);
|
|
assert.equal(payload.auditId, null);
|
|
assert.equal(payload.evidenceId, 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()));
|
|
});
|
|
}
|
|
});
|