225 lines
8.2 KiB
JavaScript
225 lines
8.2 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import test from "node:test";
|
|
|
|
import { handleJsonRpcRequest } from "../cloud/json-rpc.mjs";
|
|
import { validateResponse } from "../protocol/index.mjs";
|
|
import {
|
|
RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED,
|
|
RUNTIME_STORE_KIND_POSTGRES,
|
|
createCloudRuntimeStore,
|
|
createConfiguredCloudRuntimeStore
|
|
} from "./runtime-store.mjs";
|
|
import { CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS } from "./schema.mjs";
|
|
|
|
test("memory runtime store is never marked durable", () => {
|
|
const store = createCloudRuntimeStore();
|
|
const summary = store.summary();
|
|
|
|
assert.equal(summary.adapter, "memory");
|
|
assert.equal(summary.durable, false);
|
|
assert.equal(summary.status, "degraded");
|
|
assert.equal(summary.ready, undefined);
|
|
assert.equal(summary.adapterContract.secretMaterialRequiredInSource, false);
|
|
});
|
|
|
|
test("configured postgres runtime reports schema blocker without green readiness", async () => {
|
|
const store = createConfiguredCloudRuntimeStore({
|
|
env: {
|
|
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
|
|
HWLAB_CLOUD_RUNTIME_DURABLE: "true"
|
|
},
|
|
dbUrl: "postgres://hwlab_redacted@db.example.invalid:5432/hwlab",
|
|
queryClient: {
|
|
async query(sql) {
|
|
assert.match(sql, /information_schema\.columns/u);
|
|
return { rows: [] };
|
|
}
|
|
}
|
|
});
|
|
|
|
const readiness = await store.readiness();
|
|
assert.equal(readiness.adapter, RUNTIME_STORE_KIND_POSTGRES);
|
|
assert.equal(readiness.durable, true);
|
|
assert.equal(readiness.ready, false);
|
|
assert.equal(readiness.status, "blocked");
|
|
assert.equal(readiness.blocker, RUNTIME_DURABLE_ADAPTER_SCHEMA_BLOCKED);
|
|
assert.equal(readiness.liveRuntimeEvidence, false);
|
|
assert.equal(readiness.fixtureEvidence, false);
|
|
assert.equal(readiness.schema.checked, true);
|
|
assert.ok(readiness.schema.missingTables.includes("gateway_sessions"));
|
|
assert.equal(JSON.stringify(readiness).includes("redacted@db.example"), false);
|
|
});
|
|
|
|
test("configured postgres runtime persists and queries records through query client", async () => {
|
|
const queryClient = createFakePostgresClient();
|
|
const store = createConfiguredCloudRuntimeStore({
|
|
env: {
|
|
HWLAB_CLOUD_RUNTIME_ADAPTER: "postgres",
|
|
HWLAB_CLOUD_RUNTIME_DURABLE: "true"
|
|
},
|
|
dbUrl: "postgres://hwlab_redacted@db.example.invalid:5432/hwlab",
|
|
queryClient,
|
|
now: () => "2026-05-22T00:00:00.000Z"
|
|
});
|
|
const context = { runtimeStore: store, dbProbe: { probe: false } };
|
|
const meta = {
|
|
traceId: "trc_01J00000000000000000001000",
|
|
actorId: "usr_01J00000000000000000001000",
|
|
serviceId: "hwlab-cloud-web",
|
|
environment: "dev"
|
|
};
|
|
|
|
async function rpc(id, method, params = {}) {
|
|
const response = await handleJsonRpcRequest(
|
|
{
|
|
jsonrpc: "2.0",
|
|
id,
|
|
method,
|
|
params,
|
|
meta
|
|
},
|
|
context
|
|
);
|
|
validateResponse(response);
|
|
assert.equal(Object.hasOwn(response, "error"), false, response.error?.message);
|
|
return response.result;
|
|
}
|
|
|
|
const health = await rpc("req_01J00000000000000000001000", "system.health");
|
|
assert.equal(health.runtime.adapter, "postgres");
|
|
assert.equal(health.runtime.durable, true);
|
|
assert.equal(health.runtime.ready, true);
|
|
assert.equal(health.runtime.liveRuntimeEvidence, true);
|
|
assert.equal(health.runtime.fixtureEvidence, false);
|
|
assert.equal(health.readiness.components.runtime, "ready");
|
|
|
|
await rpc("req_01J00000000000000000001001", "gateway.session.register", {
|
|
projectId: "prj_01J00000000000000000001000",
|
|
gatewaySessionId: "gws_01J00000000000000000001000",
|
|
gatewayId: "gtw_01J00000000000000000001000",
|
|
serviceId: "hwlab-gateway-simu",
|
|
endpoint: "http://127.0.0.1:7101"
|
|
});
|
|
await rpc("req_01J00000000000000000001002", "box.resource.register", {
|
|
projectId: "prj_01J00000000000000000001000",
|
|
gatewaySessionId: "gws_01J00000000000000000001000",
|
|
resourceId: "res_01J00000000000000000001000",
|
|
boxId: "box_01J00000000000000000001000",
|
|
resourceType: "board",
|
|
state: "available"
|
|
});
|
|
await rpc("req_01J00000000000000000001003", "box.capability.report", {
|
|
capabilityId: "cap_01J00000000000000000001000",
|
|
resourceId: "res_01J00000000000000000001000",
|
|
projectId: "prj_01J00000000000000000001000",
|
|
name: "shell.exec",
|
|
direction: "bidirectional",
|
|
valueType: "object",
|
|
mutatesState: true
|
|
});
|
|
const invoke = await rpc("req_01J00000000000000000001004", "hardware.invoke.shell", {
|
|
projectId: "prj_01J00000000000000000001000",
|
|
gatewaySessionId: "gws_01J00000000000000000001000",
|
|
resourceId: "res_01J00000000000000000001000",
|
|
capabilityId: "cap_01J00000000000000000001000",
|
|
input: {
|
|
command: "echo durable"
|
|
}
|
|
});
|
|
|
|
const audit = await rpc("req_01J00000000000000000001005", "audit.event.query", {
|
|
projectId: "prj_01J00000000000000000001000"
|
|
});
|
|
const evidence = await rpc("req_01J00000000000000000001006", "evidence.record.query", {
|
|
projectId: "prj_01J00000000000000000001000"
|
|
});
|
|
|
|
assert.ok(queryClient.calls.some((call) => call.sql.startsWith("INSERT INTO gateway_sessions")));
|
|
assert.ok(queryClient.calls.some((call) => call.sql.startsWith("INSERT INTO evidence_records")));
|
|
assert.ok(audit.events.some((event) => event.action === "hardware.invoke.shell"));
|
|
assert.equal(evidence.count, 1);
|
|
assert.equal(evidence.records[0].operationId, invoke.operationId);
|
|
});
|
|
|
|
function createFakePostgresClient() {
|
|
const state = {
|
|
gateway_sessions: new Map(),
|
|
box_resources: new Map(),
|
|
box_capabilities: new Map(),
|
|
hardware_operations: new Map(),
|
|
audit_events: new Map(),
|
|
evidence_records: new Map()
|
|
};
|
|
const calls = [];
|
|
|
|
return {
|
|
calls,
|
|
async query(sql, params = []) {
|
|
calls.push({ sql, params });
|
|
if (sql.includes("information_schema.columns")) {
|
|
return { rows: schemaRows() };
|
|
}
|
|
if (sql.startsWith("SELECT COUNT(*)::int AS count FROM ")) {
|
|
const table = sql.match(/FROM ([a-z_]+)/u)?.[1];
|
|
return { rows: [{ count: state[table]?.size ?? 0 }] };
|
|
}
|
|
if (sql.startsWith("SELECT gateway_session_json FROM gateway_sessions")) {
|
|
return jsonSelect(state.gateway_sessions, params[0], "gateway_session_json");
|
|
}
|
|
if (sql.startsWith("SELECT resource_json FROM box_resources")) {
|
|
return jsonSelect(state.box_resources, params[0], "resource_json");
|
|
}
|
|
if (sql.startsWith("SELECT capability_json FROM box_capabilities")) {
|
|
return jsonSelect(state.box_capabilities, params[0], "capability_json");
|
|
}
|
|
if (sql.startsWith("SELECT event_json FROM audit_events")) {
|
|
return { rows: [...state.audit_events.values()].map((record) => ({ event_json: record.event_json })) };
|
|
}
|
|
if (sql.startsWith("SELECT metadata_json FROM evidence_records")) {
|
|
return { rows: [...state.evidence_records.values()].map((record) => ({ metadata_json: record.metadata_json })) };
|
|
}
|
|
if (sql.startsWith("INSERT INTO gateway_sessions")) {
|
|
state.gateway_sessions.set(params[0], { gateway_session_json: params[6] });
|
|
return { rows: [] };
|
|
}
|
|
if (sql.startsWith("INSERT INTO box_resources")) {
|
|
state.box_resources.set(params[0], { resource_json: params[5] });
|
|
return { rows: [] };
|
|
}
|
|
if (sql.startsWith("INSERT INTO box_capabilities")) {
|
|
state.box_capabilities.set(params[0], { capability_json: params[3] });
|
|
return { rows: [] };
|
|
}
|
|
if (sql.startsWith("INSERT INTO hardware_operations")) {
|
|
state.hardware_operations.set(params[0], { operation_json: params[4] });
|
|
return { rows: [] };
|
|
}
|
|
if (sql.startsWith("INSERT INTO audit_events")) {
|
|
state.audit_events.set(params[0], { event_json: params[8] });
|
|
return { rows: [] };
|
|
}
|
|
if (sql.startsWith("INSERT INTO evidence_records")) {
|
|
state.evidence_records.set(params[0], { metadata_json: params[5] });
|
|
return { rows: [] };
|
|
}
|
|
throw new Error(`unexpected sql: ${sql}`);
|
|
}
|
|
};
|
|
}
|
|
|
|
function schemaRows() {
|
|
return Object.entries(CLOUD_RUNTIME_DURABLE_TABLE_COLUMNS).flatMap(([table, columns]) =>
|
|
columns.map((column) => ({
|
|
table_name: table,
|
|
column_name: column
|
|
}))
|
|
);
|
|
}
|
|
|
|
function jsonSelect(map, id, column) {
|
|
const row = map.get(id);
|
|
return {
|
|
rows: row ? [{ [column]: row[column] }] : []
|
|
};
|
|
}
|