fix: close Release A monitor migration blockers

This commit is contained in:
AgentRun Codex
2026-07-12 23:24:41 +00:00
parent e3d21ed4e1
commit c101b2543e
9 changed files with 162 additions and 64 deletions
+1 -1
View File
@@ -163,7 +163,7 @@ lanes:
prometheusOperator: false
webProbe:
monitor:
configRef: config/hwlab-web-probe-monitor/runtime.nc01-v03.yaml#monitor
configRef: config/hwlab-web-probe-monitor/runtime.yaml#monitor
monitorRoot:
enabled: true
sentinelId: nc01-web-probe-sentinel
@@ -1,7 +1,7 @@
version: 1
kind: HwlabWebProbeMonitorRuntime
metadata:
id: hwlab-web-probe-monitor-nc01-v03
id: hwlab-web-probe-monitor-runtime
owner: UniDesk
specRef: PJ2026-01060508
relatedIssues:
@@ -16,22 +16,18 @@ monitor:
serviceName: hwlab-web-probe-monitor
servicePort: 8090
replicas: 1
imageRef: 127.0.0.1:5000/hwlab/web-probe-sentinel:p3-service
healthPath: /api/health
healthPath: /health
databaseSecret:
name: hwlab-web-probe-monitor-db
key: DATABASE_URL
sourceRef: hwlab/web-probe-monitor-db.env
timeout:
requestSeconds: 15
startupSeconds: 30
pool:
max: 4
idleTimeoutSeconds: 30
pageSize: 100
persistence:
mode: host-postgres
authority: prepared-central-only
authority: central-prepared-non-authoritative
database: web_probe_monitor
schema: monitor
migration:
@@ -39,7 +35,7 @@ monitor:
source:
registry: config/hwlab-node-lanes.yaml#lanes.v03.targets.NC01.observability.webProbe.sentinels
sentinelId: nc01-web-probe-sentinel
runtimeConfigRef: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.${NODE}.sentinels.${nodeLower}-web-probe-sentinel.sentinel.configRefs.runtime
runtimeConfigRef: config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.runtime
sqlitePath: /var/lib/web-probe-sentinel-nc01/index.sqlite
pvcName: hwlab-web-probe-sentinel-nc01-state
stateRoot: /var/lib/web-probe-sentinel-nc01
@@ -47,6 +43,7 @@ monitor:
name: hwlab-web-probe-monitor-migration
command: plan
readOnly: true
enabled: false
publicExposure:
enabled: false
mode: prepared-not-routed
@@ -54,13 +51,3 @@ monitor:
cicd:
consumer: sentinel-nc01-v03
mode: existing-pac-consumer
capabilities:
boundedReadModel: true
health: true
schema: true
schedulerDue: true
maintenance: true
runnerAvailability: true
runnerTriggerFacts: true
schedulerAuthority: legacy-sqlite
clientQueryAuthority: legacy-sqlite
+6
View File
@@ -90,6 +90,12 @@ sources:
OPENCODE_SERVER_PASSWORD:
bytes: 32
prefix: oc_
- sourceRef: hwlab/web-probe-monitor-db.env
type: env
requiredKeys:
- DATABASE_URL
createIfMissing:
enabled: false
targets:
- id: platform-infra-g14
@@ -1,16 +1,46 @@
import { expect, test } from "bun:test";
import { rootPath } from "./config";
import { hwlabRuntimeLaneSpecForNode } from "./hwlab-node-lanes";
import { releaseAMonitorManifests } from "./hwlab-node-web-probe-monitor";
import { releaseAMonitorManifests, releaseAMonitorMigrationJob, releaseAMonitorMigrationPool } from "./hwlab-node-web-probe-monitor";
import { readYamlRecord } from "./platform-infra-ops-library";
test("Release A central monitor renders internal service, deployment and read-only PVC migration job", () => {
const manifests = releaseAMonitorManifests(hwlabRuntimeLaneSpecForNode("v03", "NC01"));
const source = { sentinelId: "nc01-web-probe-sentinel", node: "NC01", lane: "v03", path: "/var/lib/web-probe-sentinel-nc01/index.sqlite", runtimeConfigRef: "config/hwlab-web-probe-sentinel/profiles.yaml#nodes.NC01.sentinels.nc01-web-probe-sentinel.runtime", stateRoot: "/var/lib/web-probe-sentinel-nc01", ownerPvc: "hwlab-web-probe-sentinel-nc01-state" };
const imageRef = "127.0.0.1:5000/hwlab/web-probe-sentinel-nc01:source-commit";
test("Release A live manifests exclude disabled migration Job and select only monitor service Pods", () => {
const spec = hwlabRuntimeLaneSpecForNode("v03", "NC01");
const manifests = releaseAMonitorManifests(spec, imageRef, source);
const service = manifests.find((item) => item.kind === "Service") as any;
const deployment = manifests.find((item) => item.kind === "Deployment") as any;
const job = manifests.find((item) => item.kind === "Job") as any;
expect(manifests.find((item) => item.kind === "Job")).toBeUndefined();
expect(service.spec.type).toBe("ClusterIP");
expect(service.spec.selector).toEqual({ "app.kubernetes.io/name": "hwlab-web-probe-monitor", "app.kubernetes.io/component": "monitor-service" });
expect(deployment.spec.template.metadata.labels["app.kubernetes.io/component"]).toBe("monitor-service");
expect(deployment.spec.template.spec.containers[0].image).toBe(imageRef);
expect(deployment.spec.template.spec.containers[0].readinessProbe.httpGet.path).toBe("/health");
expect(deployment.spec.template.spec.containers[0].env).toEqual(expect.arrayContaining([expect.objectContaining({ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: "hwlab-web-probe-monitor-db", key: "DATABASE_URL" } } })]));
expect(job.spec.template.spec.containers[0].args).toContain("plan");
expect(job.spec.template.spec.containers[0].volumeMounts).toEqual([{ name: "legacy-sqlite", mountPath: "/var/lib/web-probe-sentinel-nc01", readOnly: true }]);
expect(job.spec.template.spec.volumes).toEqual([{ name: "legacy-sqlite", persistentVolumeClaim: { claimName: "hwlab-web-probe-sentinel-nc01-state", readOnly: true } }]);
expect(releaseAMonitorMigrationPool(spec, source)).toEqual({ poolMax: 4, idleTimeoutSeconds: 30 });
});
test("explicit migration helper renders ordered read-only export import verify Job", () => {
const job = releaseAMonitorMigrationJob(hwlabRuntimeLaneSpecForNode("v03", "NC01"), imageRef, source, { enabled: true }) as any;
expect(job.metadata.name).toContain("source-commit");
expect(job.metadata.labels["app.kubernetes.io/component"]).toBe("migration");
expect(job.spec.template.metadata.labels["app.kubernetes.io/component"]).toBe("migration");
expect(job.spec.template.spec.initContainers.map((container: any) => container.name)).toEqual(["export", "import"]);
expect(job.spec.template.spec.initContainers[0].args).toEqual(expect.arrayContaining(["export", "--snapshot", "/migration/snapshot.json", "--confirm"]));
expect(job.spec.template.spec.initContainers[1].args).toEqual(expect.arrayContaining(["scripts/cli.ts", "import", "--snapshot", "/migration/snapshot.json", "--confirm"]));
expect(job.spec.template.spec.containers[0].args).toEqual(expect.arrayContaining(["scripts/cli.ts", "verify", "--snapshot", "/migration/snapshot.json"]));
for (const container of [...job.spec.template.spec.initContainers, ...job.spec.template.spec.containers]) {
expect(container.volumeMounts).toEqual(expect.arrayContaining([expect.objectContaining({ name: "legacy-sqlite", mountPath: "/var/lib/web-probe-sentinel-nc01", readOnly: true }), expect.objectContaining({ name: "snapshot", mountPath: "/migration" })]));
}
expect(job.spec.template.spec.volumes).toEqual([{ name: "legacy-sqlite", persistentVolumeClaim: { claimName: "hwlab-web-probe-sentinel-nc01-state", readOnly: true } }, { name: "snapshot", emptyDir: {} }]);
expect(job.spec.template.spec.containers[0].env).toEqual(expect.arrayContaining([expect.objectContaining({ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: "hwlab-web-probe-monitor-db", key: "DATABASE_URL" } } }), expect.objectContaining({ name: "MONITOR_CENTRAL_PG_POOL_MAX", value: "4" })]));
});
test("Release A declares the controlled NC01 monitor database Secret projection", () => {
const config = readYamlRecord<Record<string, any>>(rootPath("config/secrets-distribution.yaml"), "unidesk-secret-distribution");
expect(config.sources.files).toEqual(expect.arrayContaining([expect.objectContaining({ sourceRef: "hwlab/web-probe-monitor-db.env", type: "env", requiredKeys: ["DATABASE_URL"], createIfMissing: { enabled: false } })]));
expect(config.kubernetesSecrets).toEqual(expect.arrayContaining([expect.objectContaining({ name: "hwlab-web-probe-monitor-db", targetId: "hwlab-nc01-v03", secretName: "hwlab-web-probe-monitor-db", data: [{ sourceRef: "hwlab/web-probe-monitor-db.env", sourceKey: "DATABASE_URL", targetKey: "DATABASE_URL" }] })]));
});
+42 -14
View File
@@ -2,34 +2,62 @@
// Responsibility: YAML-first PG-capable central Monitor manifests while SQLite remains authoritative.
import { readWebProbeSentinelConfigRef } from "./hwlab-node-web-sentinel-config-ref";
import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
import type { MonitorSqliteSource } from "./monitor-sqlite-migration";
export function releaseAMonitorManifests(spec: HwlabRuntimeLaneSpec): readonly Record<string, unknown>[] {
export function releaseAMonitorManifests(spec: HwlabRuntimeLaneSpec, imageRef: string, resolvedSource: MonitorSqliteSource): readonly Record<string, unknown>[] {
const config = releaseAMonitorConfig(spec, resolvedSource);
const { resolved, runtime, job, namespace, deploymentName, port, secret, pool, labels } = config;
const env = [{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: textAt(secret, "name", resolved.ref), key: textAt(secret, "key", resolved.ref) } } }, { name: "MONITOR_CENTRAL_HOST", value: "0.0.0.0" }, { name: "MONITOR_CENTRAL_PORT", value: String(port) }, { name: "MONITOR_CENTRAL_PG_POOL_MAX", value: String(integerAt(pool, "max", resolved.ref)) }, { name: "MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS", value: String(integerAt(pool, "idleTimeoutSeconds", resolved.ref)) }, { name: "MONITOR_CENTRAL_PAGE_SIZE", value: String(integerAt(runtime, "pageSize", resolved.ref)) }];
const monitorLabels = { ...labels, "app.kubernetes.io/component": "monitor-service" };
return [
{ apiVersion: "v1", kind: "Service", metadata: { name: textAt(runtime, "serviceName", resolved.ref), namespace, labels: monitorLabels }, spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": deploymentName, "app.kubernetes.io/component": "monitor-service" }, ports: [{ name: "http", port, targetPort: "http" }] } },
{ apiVersion: "apps/v1", kind: "Deployment", metadata: { name: deploymentName, namespace, labels: monitorLabels }, spec: { replicas: integerAt(runtime, "replicas", resolved.ref), selector: { matchLabels: { "app.kubernetes.io/name": deploymentName, "app.kubernetes.io/component": "monitor-service" } }, template: { metadata: { labels: monitorLabels }, spec: { containers: [{ name: "monitor", image: imageRef, command: ["bun"], args: ["scripts/monitor-central-service.ts"], env, ports: [{ name: "http", containerPort: port }], readinessProbe: { httpGet: { path: textAt(runtime, "healthPath", resolved.ref), port: "http" } } }] } } } },
];
}
export function releaseAMonitorMigrationJob(spec: HwlabRuntimeLaneSpec, imageRef: string, resolvedSource: MonitorSqliteSource, options: { readonly enabled?: boolean } = {}): Record<string, unknown> | null {
const config = releaseAMonitorConfig(spec, resolvedSource);
const { resolved, runtime, job, namespace, secret, pool, labels } = config;
if ((options.enabled ?? job.enabled) !== true) return null;
const migrationLabels = { ...labels, "app.kubernetes.io/component": "migration" };
const mounts = [{ name: "legacy-sqlite", mountPath: resolvedSource.stateRoot, readOnly: true }, { name: "snapshot", mountPath: "/migration" }];
const env = [{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: textAt(secret, "name", resolved.ref), key: textAt(secret, "key", resolved.ref) } } }, { name: "MONITOR_CENTRAL_PG_POOL_MAX", value: String(integerAt(pool, "max", resolved.ref)) }, { name: "MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS", value: String(integerAt(pool, "idleTimeoutSeconds", resolved.ref)) }];
const cli = ["scripts/cli.ts", "web-probe", "sentinel", "migration"];
const sourceArgs = ["--node", spec.nodeId, "--lane", spec.lane, "--sentinel", resolvedSource.sentinelId, "--snapshot", "/migration/snapshot.json"];
return { apiVersion: "batch/v1", kind: "Job", metadata: { name: versionedJobName(textAt(job, "name", resolved.ref), imageRef), namespace, labels: migrationLabels }, spec: { backoffLimit: 0, template: { metadata: { labels: migrationLabels }, spec: { restartPolicy: "Never", initContainers: [{ name: "export", image: imageRef, command: ["bun"], args: [...cli, "export", ...sourceArgs, "--confirm"], volumeMounts: mounts }, { name: "import", image: imageRef, command: ["bun"], args: [...cli, "import", ...sourceArgs, "--confirm"], env, volumeMounts: mounts }], containers: [{ name: "verify", image: imageRef, command: ["bun"], args: [...cli, "verify", ...sourceArgs], env, volumeMounts: mounts }], volumes: [{ name: "legacy-sqlite", persistentVolumeClaim: { claimName: resolvedSource.ownerPvc, readOnly: true } }, { name: "snapshot", emptyDir: {} }] } } } };
}
export function releaseAMonitorMigrationPool(spec: HwlabRuntimeLaneSpec, resolvedSource: MonitorSqliteSource): { readonly poolMax: number; readonly idleTimeoutSeconds: number } {
const { resolved, pool } = releaseAMonitorConfig(spec, resolvedSource);
return { poolMax: integerAt(pool, "max", resolved.ref), idleTimeoutSeconds: integerAt(pool, "idleTimeoutSeconds", resolved.ref) };
}
function releaseAMonitorConfig(spec: HwlabRuntimeLaneSpec, resolvedSource: MonitorSqliteSource): { readonly resolved: ReturnType<typeof readWebProbeSentinelConfigRef>; readonly runtime: Record<string, unknown>; readonly job: Record<string, unknown>; readonly namespace: string; readonly deploymentName: string; readonly port: number; readonly secret: Record<string, unknown>; readonly pool: Record<string, unknown>; readonly labels: Record<string, string> } {
const configRef = spec.observability.webProbe?.monitor?.configRef;
if (!configRef) return [];
if (!configRef) throw new Error(`lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.monitor.configRef is required`);
const resolved = readWebProbeSentinelConfigRef(spec, configRef);
if (!isRecord(resolved.target)) throw new Error(`${resolved.ref} must resolve to an object`);
const runtime = recordAt(resolved.target, "runtime", resolved.ref);
const migration = recordAt(resolved.target, "migration", resolved.ref);
const monitor = resolved.target;
const runtime = recordAt(monitor, "runtime", resolved.ref);
const persistence = recordAt(monitor, "persistence", resolved.ref);
const migration = recordAt(monitor, "migration", resolved.ref);
const source = recordAt(migration, "source", resolved.ref);
const job = recordAt(migration, "job", resolved.ref);
if (textAt(migration, "mode", resolved.ref) !== "legacy-authority") throw new Error(`${resolved.ref}.migration.mode must be legacy-authority`);
if (textAt(source, "sentinelId", resolved.ref) !== "nc01-web-probe-sentinel" || textAt(source, "sqlitePath", resolved.ref) !== "/var/lib/web-probe-sentinel-nc01/index.sqlite") throw new Error(`${resolved.ref}.migration.source must declare the resolved NC01/v03 source`);
if (textAt(job, "command", resolved.ref) !== "plan" || job.readOnly !== true) throw new Error(`${resolved.ref}.migration.job must be a read-only plan`);
const exposure = recordAt(monitor, "publicExposure", resolved.ref);
const cicd = recordAt(monitor, "cicd", resolved.ref);
if (textAt(runtime, "node", resolved.ref) !== spec.nodeId || textAt(runtime, "lane", resolved.ref) !== spec.lane || textAt(persistence, "mode", resolved.ref) !== "host-postgres" || textAt(persistence, "authority", resolved.ref) !== "central-prepared-non-authoritative" || exposure.enabled !== false || textAt(cicd, "mode", resolved.ref) !== "existing-pac-consumer" || textAt(cicd, "consumer", resolved.ref) !== "sentinel-nc01-v03" || textAt(migration, "mode", resolved.ref) !== "legacy-authority") throw new Error(`${resolved.ref} must remain the NC01/v03 non-authoritative Release A configuration`);
if (textAt(source, "sentinelId", resolved.ref) !== resolvedSource.sentinelId || textAt(source, "runtimeConfigRef", resolved.ref) !== resolvedSource.runtimeConfigRef || textAt(source, "sqlitePath", resolved.ref) !== resolvedSource.path || textAt(source, "pvcName", resolved.ref) !== resolvedSource.ownerPvc || textAt(source, "stateRoot", resolved.ref) !== resolvedSource.stateRoot || textAt(job, "command", resolved.ref) !== "plan" || job.readOnly !== true || typeof job.enabled !== "boolean") throw new Error(`${resolved.ref}.migration must match the resolved active registry and declare a read-only plan`);
const namespace = textAt(runtime, "namespace", resolved.ref);
const deploymentName = textAt(runtime, "deploymentName", resolved.ref);
const port = integerAt(runtime, "servicePort", resolved.ref);
const secret = recordAt(runtime, "databaseSecret", resolved.ref);
if (textAt(secret, "name", resolved.ref) !== "hwlab-web-probe-monitor-db" || textAt(secret, "key", resolved.ref) !== "DATABASE_URL" || textAt(secret, "sourceRef", resolved.ref) !== "hwlab/web-probe-monitor-db.env") throw new Error(`${resolved.ref}.runtime.databaseSecret must be the controlled monitor DATABASE_URL projection`);
const pool = recordAt(runtime, "pool", resolved.ref);
const labels = { "app.kubernetes.io/name": deploymentName, "app.kubernetes.io/part-of": "hwlab-web-probe-monitor", "app.kubernetes.io/managed-by": "unidesk", "unidesk.ai/migration-mode": "legacy-authority" };
const env = [{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: textAt(secret, "name", resolved.ref), key: textAt(secret, "key", resolved.ref) } } }, { name: "MONITOR_CENTRAL_HOST", value: "0.0.0.0" }, { name: "MONITOR_CENTRAL_PORT", value: String(port) }, { name: "MONITOR_CENTRAL_PG_POOL_MAX", value: String(integerAt(pool, "max", resolved.ref)) }, { name: "MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS", value: String(integerAt(pool, "idleTimeoutSeconds", resolved.ref)) }, { name: "MONITOR_CENTRAL_PAGE_SIZE", value: String(integerAt(runtime, "pageSize", resolved.ref)) }];
return [
{ apiVersion: "v1", kind: "Service", metadata: { name: textAt(runtime, "serviceName", resolved.ref), namespace, labels }, spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": deploymentName }, ports: [{ name: "http", port, targetPort: "http" }] } },
{ apiVersion: "apps/v1", kind: "Deployment", metadata: { name: deploymentName, namespace, labels }, spec: { replicas: integerAt(runtime, "replicas", resolved.ref), selector: { matchLabels: { "app.kubernetes.io/name": deploymentName } }, template: { metadata: { labels }, spec: { containers: [{ name: "monitor", image: textAt(runtime, "imageRef", resolved.ref), command: ["bun"], args: ["scripts/monitor-central-service.ts"], env, ports: [{ name: "http", containerPort: port }], readinessProbe: { httpGet: { path: textAt(runtime, "healthPath", resolved.ref), port: "http" } } }] } } } },
{ apiVersion: "batch/v1", kind: "Job", metadata: { name: textAt(job, "name", resolved.ref), namespace, labels }, spec: { backoffLimit: 0, template: { metadata: { labels }, spec: { restartPolicy: "Never", containers: [{ name: "migration", image: textAt(runtime, "imageRef", resolved.ref), command: ["bun"], args: ["scripts/cli.ts", "web-probe", "sentinel", "migration", "plan", "--node", spec.nodeId, "--lane", spec.lane, "--sentinel", textAt(source, "sentinelId", resolved.ref), "--json"], env: [{ name: "DATABASE_URL", valueFrom: { secretKeyRef: { name: textAt(secret, "name", resolved.ref), key: textAt(secret, "key", resolved.ref) } } }], volumeMounts: [{ name: "legacy-sqlite", mountPath: textAt(source, "stateRoot", resolved.ref), readOnly: true }] }], volumes: [{ name: "legacy-sqlite", persistentVolumeClaim: { claimName: textAt(source, "pvcName", resolved.ref), readOnly: true } }] } } } },
];
return { resolved, runtime, job, namespace, deploymentName, port, secret, pool, labels: { "app.kubernetes.io/name": deploymentName, "app.kubernetes.io/part-of": "hwlab-web-probe-monitor", "app.kubernetes.io/managed-by": "unidesk", "unidesk.ai/migration-mode": "legacy-authority" } };
}
function recordAt(value: Record<string, unknown>, key: string, path: string): Record<string, unknown> { if (!isRecord(value[key])) throw new Error(`${path}.${key} must be an object`); return value[key] as Record<string, unknown>; }
function textAt(value: Record<string, unknown>, key: string, path: string): string { const item = value[key]; if (typeof item !== "string" || item.length === 0) throw new Error(`${path}.${key} must be a non-empty string`); return item; }
function integerAt(value: Record<string, unknown>, key: string, path: string): number { const item = value[key]; if (typeof item !== "number" || !Number.isSafeInteger(item) || item < 1) throw new Error(`${path}.${key} must be a positive integer`); return item; }
function isRecord(value: unknown): value is Record<string, unknown> { return typeof value === "object" && value !== null && !Array.isArray(value); }
function versionedJobName(base: string, imageRef: string): string { const suffix = imageRef.split(":").at(-1)?.replace(/[^a-z0-9-]/gu, "-").slice(0, 20) || "image"; return `${base}-${suffix}`.slice(0, 63).replace(/-+$/u, ""); }
+13 -10
View File
@@ -37,7 +37,7 @@ import { probeSentinelRuntimeHealthEndpoint, runSentinelDashboard, runSentinelEr
import { runChildCli, sentinelP5Next } from "./hwlab-node-web-sentinel-p5-observe";
import { emitWebProbeSentinelSpan, webProbeSentinelOtelSummary } from "./hwlab-node-web-sentinel-otel";
import { exportMonitorSqliteSnapshot, planMonitorSqliteSources, verifyMonitorSqliteSnapshot, type MonitorSqliteSource } from "./monitor-sqlite-migration";
import { releaseAMonitorManifests } from "./hwlab-node-web-probe-monitor";
import { releaseAMonitorManifests, releaseAMonitorMigrationPool } from "./hwlab-node-web-probe-monitor";
import {
arrayAt,
arrayAtNullable,
@@ -209,25 +209,28 @@ function runSentinelMigration(spec: HwlabRuntimeLaneSpec, options: Extract<WebPr
}
if (options.snapshotPath === null) throw new Error(`web-probe sentinel migration ${options.action} requires --snapshot`);
const snapshot = JSON.parse(readFileSync(options.snapshotPath, "utf8")) as Parameters<typeof verifyMonitorSqliteSnapshot>[0];
const source = sources.find((item) => sameMigrationSource(item, snapshot.source));
if (source === undefined) throw new Error("migration snapshot source is not in the current resolved registry");
const verification = verifyMonitorSqliteSnapshot(snapshot);
if (verification.ok !== true) throw new Error("migration snapshot verification failed");
if (options.action === "import") {
const source = sources.find((item) => item.sentinelId === snapshot.source.sentinelId && item.runtimeConfigRef === snapshot.source.runtimeConfigRef);
if (source === undefined) throw new Error("migration snapshot source is not in the current resolved registry");
const runtime = recordTarget(readWebProbeSentinelConfigRefTarget(spec, source.runtimeConfigRef), source.runtimeConfigRef);
const poolMax = numberAtNullable(runtime, "monitor.migration.poolMax");
const idleTimeoutSeconds = numberAtNullable(runtime, "monitor.migration.idleTimeoutSeconds");
if (stringAtNullable(runtime, "monitor.migration.databaseUrlSourceRef") === null || stringAtNullable(runtime, "monitor.migration.databaseUrlTargetKey") !== "DATABASE_URL" || poolMax === null || idleTimeoutSeconds === null) throw new Error("monitor.migration must declare databaseUrlSourceRef, databaseUrlTargetKey=DATABASE_URL, poolMax, and idleTimeoutSeconds");
if (!process.env.DATABASE_URL) throw new Error("monitor migration DATABASE_URL must be injected by the controlled Secret consumer");
const result = runCommand(["bun", "scripts/monitor-sqlite-migration-import.ts", options.snapshotPath], repoRoot, { timeoutMs: 120_000, env: { ...process.env, MONITOR_CENTRAL_PG_POOL_MAX: String(poolMax), MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS: String(idleTimeoutSeconds) } });
const pool = releaseAMonitorMigrationPool(spec, source);
const result = runCommand(["bun", "scripts/monitor-sqlite-migration-import.ts", options.snapshotPath], repoRoot, { timeoutMs: 120_000, env: { ...process.env, MONITOR_CENTRAL_PG_POOL_MAX: String(pool.poolMax), MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS: String(pool.idleTimeoutSeconds) } });
const projection = parseJsonObject(result.stdout) ?? { ok: false, command, error: "monitor-migration-import-unparseable", stderr: short(result.stderr), valuesRedacted: true };
return rendered(projection.ok === true, command, renderMigrationCompact(projection), projection);
}
if (!process.env.DATABASE_URL) throw new Error("monitor migration verify requires DATABASE_URL projected from hwlab-web-probe-monitor-db/DATABASE_URL");
const result = runCommand(["bun", "scripts/monitor-sqlite-migration-verify.ts", options.snapshotPath], repoRoot, { timeoutMs: 120_000, env: { ...process.env, MONITOR_CENTRAL_PG_POOL_MAX: "4", MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS: "30" } });
const pool = releaseAMonitorMigrationPool(spec, source);
const result = runCommand(["bun", "scripts/monitor-sqlite-migration-verify.ts", options.snapshotPath], repoRoot, { timeoutMs: 120_000, env: { ...process.env, MONITOR_CENTRAL_PG_POOL_MAX: String(pool.poolMax), MONITOR_CENTRAL_PG_IDLE_TIMEOUT_SECONDS: String(pool.idleTimeoutSeconds) } });
const projection = parseJsonObject(result.stdout) ?? { ok: false, command, error: "monitor-migration-verify-unparseable", stderr: short(result.stderr), valuesRedacted: true };
return rendered(projection.ok === true, command, renderMigrationCompact(projection), projection);
}
function sameMigrationSource(left: MonitorSqliteSource, right: MonitorSqliteSource): boolean {
return left.sentinelId === right.sentinelId && left.node === right.node && left.lane === right.lane && left.path === right.path && left.runtimeConfigRef === right.runtimeConfigRef && left.stateRoot === right.stateRoot && left.ownerPvc === right.ownerPvc;
}
function resolvedMigrationSources(spec: HwlabRuntimeLaneSpec, sentinelId: string | null): readonly MonitorSqliteSource[] {
const ids = sentinelId === null ? webProbeSentinelRegistryRows(spec).filter((item) => item.enabled).map((item) => item.id) : [sentinelId];
return ids.map((id) => {
@@ -1302,7 +1305,7 @@ function renderSentinelManifests(
spec: { type: "ClusterIP", selector: { "app.kubernetes.io/name": deploymentName }, ports: [{ name: "http", port: servicePort, targetPort: "http" }] },
},
...(cadenceJob === null ? [] : [cadenceJob]),
...releaseAMonitorManifests(spec),
...releaseAMonitorManifests(spec, image.ref, resolvedMigrationSources(spec, sentinelId)[0] ?? (() => { throw new Error("Release A monitor requires one resolved migration source"); })()),
{
apiVersion: "apps/v1",
kind: "Deployment",
+29 -4
View File
@@ -4,7 +4,7 @@ import { createHash } from "node:crypto";
import postgres from "postgres";
export const MONITOR_CENTRAL_SPEC_REF = "PJ2026-01060508 Web哨兵 draft-2026-07-12-p17-monitor-central-persistence";
export const MONITOR_CENTRAL_SCHEMA_VERSION = "2026-07-12-p17-2";
export const MONITOR_CENTRAL_SCHEMA_VERSION = "2026-07-12-p17-3";
export interface MonitorScope {
readonly sentinelId?: string;
@@ -52,7 +52,8 @@ export interface MonitorCentralStore {
schemaVersion(): Promise<string | null>;
capability(): Promise<{ readonly ingest: boolean; readonly query: boolean }>;
ingest(input: NormalizedMonitorCentralIngest): Promise<{ readonly disposition: "inserted" | "idempotent" }>;
importSource(manifest: MonitorImportManifest, inputs: readonly NormalizedMonitorCentralIngest[]): Promise<{ readonly disposition: "inserted" | "idempotent"; readonly runs: readonly { readonly disposition: "inserted" | "idempotent" }[] }>;
importSource(manifest: MonitorImportManifest, inputs: readonly NormalizedMonitorCentralIngest[], runtimeMetadata?: readonly MonitorRuntimeMetadata[]): Promise<{ readonly disposition: "inserted" | "idempotent"; readonly runs: readonly { readonly disposition: "inserted" | "idempotent" }[] }>;
runtimeMetadata(scope: Required<MonitorScope>): Promise<readonly MonitorRuntimeMetadata[]>;
updateArtifactAvailability(scope: Required<MonitorScope>, runId: string, locatorIndex: number, reachable: boolean): Promise<void>;
overview(scope: MonitorScope): Promise<Record<string, unknown>>;
runs(scope: MonitorScope, options: RunQueryOptions): Promise<readonly MonitorRun[]>;
@@ -71,6 +72,12 @@ export interface MonitorImportManifest {
readonly rowCount: number;
}
export interface MonitorRuntimeMetadata extends Required<MonitorScope> {
readonly key: string;
readonly value: unknown;
readonly updatedAt: string;
}
interface RunCursor {
readonly updatedAt: string;
readonly runId: string;
@@ -197,7 +204,7 @@ export function createPostgresMonitorCentralStore(databaseUrl: string, options:
async ingest(input) {
return sql.begin((transaction) => writePostgresIngest(transaction, input));
},
async importSource(manifest, inputs) {
async importSource(manifest, inputs, runtimeMetadata = []) {
return sql.begin(async (transaction) => {
const claimed = await transaction<{ manifest_id: string }[]>`
insert into monitor.import_manifests (manifest_id, source_fingerprint, row_count, imported_at)
@@ -213,9 +220,23 @@ export function createPostgresMonitorCentralStore(databaseUrl: string, options:
}
const runs = [] as { disposition: "inserted" | "idempotent" }[];
for (const input of inputs) runs.push(await writePostgresIngest(transaction, input));
for (const item of runtimeMetadata) {
await transaction`
insert into monitor.runners (sentinel_id, node, lane, updated_at)
values (${item.sentinelId}, ${item.node}, ${item.lane}, ${item.updatedAt})
on conflict (sentinel_id, node, lane) do update set updated_at = greatest(monitor.runners.updated_at, excluded.updated_at)`;
await transaction`
insert into monitor.runtime_metadata (sentinel_id, node, lane, key, value, updated_at)
values (${item.sentinelId}, ${item.node}, ${item.lane}, ${item.key}, ${JSON.stringify(item.value)}::jsonb, ${item.updatedAt})
on conflict (sentinel_id, node, lane, key) do update set value = excluded.value, updated_at = excluded.updated_at`;
}
return { disposition: "inserted" as const, runs };
});
},
async runtimeMetadata(scope) {
const rows = await sql<({ key: string; value: unknown; updated_at: string })[]>`select key, value, updated_at from monitor.runtime_metadata where sentinel_id = ${scope.sentinelId} and node = ${scope.node} and lane = ${scope.lane} order by key`;
return rows.map((item) => ({ ...scope, key: item.key, value: item.value, updatedAt: new Date(item.updated_at).toISOString() }));
},
async updateArtifactAvailability(scope, runId, locatorIndex, reachable) {
await sql`
update monitor.artifact_locators
@@ -303,12 +324,14 @@ export const MONITOR_CENTRAL_MIGRATIONS = [
"create table if not exists monitor.findings (sentinel_id text not null, node text not null, lane text not null, run_id text not null, finding_index integer not null, finding jsonb not null, primary key (sentinel_id, node, lane, run_id, finding_index), foreign key (sentinel_id, node, lane, run_id) references monitor.runs on delete cascade)",
"create table if not exists monitor.artifact_locators (sentinel_id text not null, node text not null, lane text not null, run_id text not null, locator_index integer not null, owner_node text not null, owner_lane text not null, owner_pvc text not null, state_dir text not null, relative_path text not null, sha256 text not null, size_bytes bigint not null, kind text not null, reachable boolean not null, primary key (sentinel_id, node, lane, run_id, locator_index), foreign key (sentinel_id, node, lane, run_id) references monitor.runs on delete cascade)",
"create table if not exists monitor.timeline_events (sentinel_id text not null, node text not null, lane text not null, run_id text not null, event_index integer not null, event jsonb not null, primary key (sentinel_id, node, lane, run_id, event_index), foreign key (sentinel_id, node, lane, run_id) references monitor.runs on delete cascade)",
"create table if not exists monitor.runtime_metadata (sentinel_id text not null, node text not null, lane text not null, key text not null, value jsonb not null, updated_at timestamptz not null, primary key (sentinel_id, node, lane, key), foreign key (sentinel_id, node, lane) references monitor.runners (sentinel_id, node, lane))",
"create table if not exists monitor.import_manifests (manifest_id text primary key, source_fingerprint text not null, row_count bigint not null, imported_at timestamptz not null)",
];
export function createInMemoryMonitorCentralStore(): MonitorCentralStore {
const records = new Map<string, NormalizedMonitorCentralIngest>();
const manifests = new Map<string, MonitorImportManifest>();
const metadata = new Map<string, MonitorRuntimeMetadata>();
return {
async close() {},
async migrate() {},
@@ -326,7 +349,7 @@ export function createInMemoryMonitorCentralStore(): MonitorCentralStore {
records.set(key, structuredClone(input));
return { disposition: "inserted" };
},
async importSource(manifest, inputs) {
async importSource(manifest, inputs, runtimeMetadata = []) {
const previous = manifests.get(manifest.manifestId);
if (previous) {
if (previous.sourceFingerprint !== manifest.sourceFingerprint || previous.rowCount !== manifest.rowCount) throw Object.assign(new Error("monitor import manifest conflict"), { code: "monitor_import_manifest_conflict" });
@@ -342,9 +365,11 @@ export function createInMemoryMonitorCentralStore(): MonitorCentralStore {
}
records.clear();
for (const [key, value] of staged) records.set(key, value);
for (const item of runtimeMetadata) metadata.set(`${item.sentinelId}/${item.node}/${item.lane}/${item.key}`, structuredClone(item));
manifests.set(manifest.manifestId, { ...manifest });
return { disposition: "inserted", runs };
},
async runtimeMetadata(scope) { return [...metadata.values()].filter((item) => item.sentinelId === scope.sentinelId && item.node === scope.node && item.lane === scope.lane).sort((left, right) => left.key.localeCompare(right.key)).map(structuredClone); },
async updateArtifactAvailability(scope, runId, locatorIndex, reachable) {
const key = identityKey({ ...scope, runId });
const existing = records.get(key);
+16 -8
View File
@@ -5,7 +5,7 @@ import { closeSync, existsSync, openSync, readFileSync, statSync, writeFileSync
import { dirname, join, resolve } from "node:path";
import { Database } from "bun:sqlite";
import type { MonitorArtifactLocator, MonitorCentralIngest, MonitorCentralStore } from "./monitor-central-persistence";
import type { MonitorArtifactLocator, MonitorCentralIngest, MonitorCentralStore, MonitorRuntimeMetadata } from "./monitor-central-persistence";
import { monitorCentralStableJson, normalizeMonitorCentralIngest } from "./monitor-central-persistence";
export const MONITOR_SQLITE_MIGRATION_SCHEMA = "2026-07-12-p17-2";
@@ -22,13 +22,13 @@ export interface MonitorMigrationReconciliation { readonly runs: number; readonl
export async function verifyMonitorSqliteSnapshotAgainstStore(snapshot: MonitorSqliteSnapshot, store: MonitorCentralStore): Promise<Record<string, unknown>> {
const snapshotVerification = verifyMonitorSqliteSnapshot(snapshot);
const expected = canonicalIngests(snapshot);
const expectedMetadata = canonicalRuntimeMetadata(snapshot);
const checks: Record<string, unknown>[] = [];
for (const input of expected) {
const scope = { sentinelId: input.sentinelId, node: input.node, lane: input.lane };
const actual = await store.run(scope, input.runId);
checks.push({ name: "run", runId: input.runId, ok: actual?.payloadHash === input.payloadHash && actual?.status === input.status && monitorCentralStableJson(actual?.payload) === monitorCentralStableJson(input.payload), valuesRedacted: true });
checks.push({ name: "run", runId: input.runId, ok: actual?.payloadHash === input.payloadHash && actual?.status === input.status && actual?.updatedAt === input.updatedAt && monitorCentralStableJson(actual?.payload) === monitorCentralStableJson(input.payload), valuesRedacted: true });
checks.push({ name: "finding", runId: input.runId, ok: monitorCentralStableJson(actual?.findings) === monitorCentralStableJson(input.findings), valuesRedacted: true });
checks.push({ name: "metadata", runId: input.runId, ok: monitorCentralStableJson(actual?.payload) === monitorCentralStableJson(input.payload), valuesRedacted: true });
checks.push({ name: "artifactLocator", runId: input.runId, ok: monitorCentralStableJson(actual?.artifactLocators) === monitorCentralStableJson(input.artifactLocators), valuesRedacted: true });
const views = isRecord(input.payload.views) ? input.payload.views : {};
for (const [name, value] of Object.entries(views)) {
@@ -39,14 +39,18 @@ export async function verifyMonitorSqliteSnapshotAgainstStore(snapshot: MonitorS
const latestFor = (items: readonly typeof expected) => [...items].sort((left, right) => right.updatedAt.localeCompare(left.updatedAt) || right.runId.localeCompare(left.runId))[0] ?? null;
const globalLatest = latestFor(expected);
const global = await store.overview({});
checks.push({ name: "globalLatest", ok: globalLatest === null ? global.latest === null : isRecord(global.latest) && global.latest.runId === globalLatest.runId && global.latest.updatedAt === globalLatest.updatedAt, valuesRedacted: true });
checks.push({ name: "globalOverview", ok: global.runCount === expected.length && (globalLatest === null ? global.latest === null : sameRun(global.latest, globalLatest)), valuesRedacted: true });
const scopes = [...new Map(expected.map((item) => [`${item.sentinelId}/${item.node}/${item.lane}`, { sentinelId: item.sentinelId, node: item.node, lane: item.lane }])).values()];
for (const scope of scopes) {
const latest = latestFor(expected.filter((item) => item.sentinelId === scope.sentinelId && item.node === scope.node && item.lane === scope.lane));
const overview = await store.overview(scope);
checks.push({ name: "scopedLatest", scope, ok: latest === null ? overview.latest === null : isRecord(overview.latest) && overview.latest.runId === latest.runId && overview.latest.updatedAt === latest.updatedAt, valuesRedacted: true });
const expectedCount = expected.filter((item) => item.sentinelId === scope.sentinelId && item.node === scope.node && item.lane === scope.lane).length;
checks.push({ name: "scopedOverview", scope, ok: overview.runCount === expectedCount && (latest === null ? overview.latest === null : sameRun(overview.latest, latest)), valuesRedacted: true });
const actualMetadata = await store.runtimeMetadata(scope);
const scopedMetadata = expectedMetadata.filter((item) => item.sentinelId === scope.sentinelId && item.node === scope.node && item.lane === scope.lane);
checks.push({ name: "runtimeMetadata", scope, ok: monitorCentralStableJson(actualMetadata) === monitorCentralStableJson(scopedMetadata), sourceCount: scopedMetadata.length, targetCount: actualMetadata.length, valuesRedacted: true });
}
return { ok: snapshotVerification.ok === true && checks.every((item) => item.ok === true), command: "web-probe sentinel migration verify", source: "resolved-active-registry", snapshot: snapshotVerification, reconciliation: snapshotVerification.reconciliation, checks, valuesRedacted: true };
return { ok: snapshotVerification.ok === true && checks.every((item) => item.ok === true), command: "web-probe sentinel migration verify", source: "resolved-active-registry", snapshot: snapshotVerification, reconciliation: { source: reconcileTables(snapshot.tables), target: { runs: expected.length, runtimeMetadata: expectedMetadata.length } }, checks, valuesRedacted: true };
}
export function planMonitorSqliteSources(sources: readonly MonitorSqliteSource[]): Record<string, unknown> {
@@ -87,11 +91,12 @@ export async function importMonitorSqliteSnapshot(snapshot: MonitorSqliteSnapsho
const verified = verifyMonitorSqliteSnapshot(snapshot);
if (verified.ok !== true) throw Object.assign(new Error("snapshot verification failed"), { code: "monitor-migration-snapshot-invalid", verification: verified });
const ingests = canonicalIngests(snapshot);
const runtimeMetadata = canonicalRuntimeMetadata(snapshot);
const manifest = { manifestId: snapshot.manifestContentHash, sourceFingerprint: snapshot.fingerprint, rowCount: snapshot.tables.reduce((total, table) => total + table.rowCount, 0) };
const batch = await store.importSource(manifest, ingests);
const batch = await store.importSource(manifest, ingests, runtimeMetadata);
const dispositions = batch.runs;
const reconciliation = reconcileTables(snapshot.tables);
return { ok: true, command: "web-probe sentinel migration import", source: snapshot.source, manifest, disposition: batch.disposition, imported: { inserted: dispositions.filter((item) => item.disposition === "inserted").length, idempotent: dispositions.filter((item) => item.disposition === "idempotent").length }, reconciliation, payloadHashCount: new Set(ingests.map((item) => item.payloadHash)).size, valuesRedacted: true };
return { ok: true, command: "web-probe sentinel migration import", source: snapshot.source, manifest, disposition: batch.disposition, imported: { inserted: dispositions.filter((item) => item.disposition === "inserted").length, idempotent: dispositions.filter((item) => item.disposition === "idempotent").length }, reconciliation: { source: reconciliation, target: { runs: ingests.length, runtimeMetadata: runtimeMetadata.length } }, payloadHashCount: new Set(ingests.map((item) => item.payloadHash)).size, valuesRedacted: true };
}
export function reconcileTables(tables: readonly MonitorSqliteTable[]): MonitorMigrationReconciliation {
@@ -118,6 +123,8 @@ function canonicalIngests(snapshot: MonitorSqliteSnapshot): readonly ReturnType<
return normalizeMonitorCentralIngest({ sentinelId: stringField(row, ["sentinel_id", "sentinelId"]) || snapshot.source.sentinelId, node: stringField(row, ["node"]) || snapshot.source.node, lane: stringField(row, ["lane"]) || snapshot.source.lane, runId, updatedAt: stringField(row, ["updated_at", "updatedAt", "created_at", "createdAt"]) || snapshot.createdAt, status: stringField(row, ["status"]) || "legacy", payload, findings: canonicalFindings, artifactLocators: reportLocator ? [...rowLocators, reportLocator] : rowLocators, timeline: [] });
});
}
function canonicalRuntimeMetadata(snapshot: MonitorSqliteSnapshot): readonly MonitorRuntimeMetadata[] { return tableRows(snapshot.tables, ["metadata", "run_metadata"]).map((row) => ({ sentinelId: snapshot.source.sentinelId, node: snapshot.source.node, lane: snapshot.source.lane, key: stringField(row, ["key"]), value: jsonValue(row, ["value_json", "value"]), updatedAt: stringField(row, ["updated_at", "updatedAt", "created_at", "createdAt"]) || snapshot.createdAt })).filter((item) => item.key.length > 0).sort((left, right) => left.key.localeCompare(right.key)); }
function sameRun(value: unknown, expected: { readonly sentinelId: string; readonly node: string; readonly lane: string; readonly runId: string; readonly updatedAt: string }): boolean { return isRecord(value) && value.sentinelId === expected.sentinelId && value.node === expected.node && value.lane === expected.lane && value.runId === expected.runId && value.updatedAt === expected.updatedAt; }
function locatorFromRow(row: Record<string, unknown>, source: MonitorSqliteSnapshot): MonitorArtifactLocator { const ownerPvc = stringField(row, ["owner_pvc", "ownerPvc"]); const relativePath = stringField(row, ["relative_path", "relativePath"]); const hash = stringField(row, ["sha256"]); const sizeBytes = numberField(row, ["size_bytes", "sizeBytes"]); if (!ownerPvc || !relativePath || !hash || sizeBytes === null) throw Object.assign(new Error("legacy artifact locator is incomplete"), { code: "monitor-migration-locator-invalid" }); return { ownerNode: stringField(row, ["owner_node", "ownerNode"]) || source.source.node, ownerLane: stringField(row, ["owner_lane", "ownerLane"]) || source.source.lane, ownerPvc, stateDir: stringField(row, ["state_dir", "stateDir"]), relativePath, sha256: hash, sizeBytes, kind: stringField(row, ["kind"]) || "artifact", reachable: row.reachable !== false }; }
function reportLocatorFromRun(row: Record<string, unknown>, snapshot: MonitorSqliteSnapshot): MonitorArtifactLocator | null { const stateDir = stringField(row, ["state_dir", "stateDir"]); const expectedHash = stringField(row, ["report_json_sha256", "reportJsonSha256"]); if (!stateDir && !expectedHash) return null; if (!stateDir || !expectedHash) throw Object.assign(new Error("legacy report locator is incomplete"), { code: "monitor-migration-report-locator-invalid" }); const path = join(snapshot.source.stateRoot, stateDir, "analysis", "report.json"); const bytes = readFileSync(path); const actualHash = `sha256:${sha256(bytes)}`; if (actualHash !== expectedHash) throw Object.assign(new Error("legacy report hash does not match artifact"), { code: "monitor-migration-report-locator-invalid" }); return { ownerNode: snapshot.source.node, ownerLane: snapshot.source.lane, ownerPvc: snapshot.source.ownerPvc, stateDir, relativePath: "analysis/report.json", sha256: actualHash, sizeBytes: bytes.byteLength, kind: "report-json", reachable: true }; }
function metadataValue(rows: readonly Record<string, unknown>[], key: string): Record<string, unknown> | null { return jsonField(rows.find((row) => stringField(row, ["key"]) === key), ["value_json", "value"]); }
@@ -130,6 +137,7 @@ function writeSnapshot(path: string, content: string): void { const parent = dir
function quoteIdentifier(value: string): string { return `"${value.replaceAll("\"", "\"\"")}"`; }
function normalizeRow(value: unknown): Record<string, unknown> { const row = value as Record<string, unknown>; return Object.fromEntries(Object.entries(row).map(([key, item]) => [key, item instanceof Uint8Array ? Buffer.from(item).toString("base64") : item])); }
function jsonField(row: Record<string, unknown> | undefined, fields: readonly string[]): Record<string, unknown> | null { if (!row) return null; const value = fields.map((field) => row[field]).find((item) => item !== undefined); if (typeof value === "string") try { const parsed = JSON.parse(value); return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed as Record<string, unknown> : null; } catch { return null; } return value && typeof value === "object" && !Array.isArray(value) ? value as Record<string, unknown> : null; }
function jsonValue(row: Record<string, unknown> | undefined, fields: readonly string[]): unknown { if (!row) return null; const value = fields.map((field) => row[field]).find((item) => item !== undefined); if (typeof value !== "string") return value ?? null; try { return JSON.parse(value); } catch { return value; } }
function stringField(row: Record<string, unknown> | undefined, fields: readonly string[]): string { const value = row === undefined ? null : fields.map((field) => row[field]).find((item) => typeof item === "string" && item.length > 0); return typeof value === "string" ? value : ""; }
function numberField(row: Record<string, unknown>, fields: readonly string[]): number | null { const value = fields.map((field) => row[field]).find((item) => typeof item === "number"); return typeof value === "number" && Number.isSafeInteger(value) && value >= 0 ? value : null; }
function sha256(value: Uint8Array): string { return createHash("sha256").update(value).digest("hex"); }
@@ -59,7 +59,7 @@ test("SQLite snapshot reconciles the 228/242/234 fixture shape without productio
database.run("CREATE TABLE metadata (id TEXT, key TEXT, value_json TEXT)");
for (let index = 0; index < 228; index += 1) database.run("INSERT INTO runs VALUES (?, ?, ?, ?)", [`run-${index}`, index === 0 ? "run-0" : null, index === 0 ? reportHash : null, "2026-07-12T00:00:00.000Z"]);
for (let index = 0; index < 242; index += 1) database.run("INSERT INTO findings VALUES (?, ?, ?, ?, ?, ?, ?, ?)", [index === 0 ? "run-0" : `run-${index % 228}`, "fixture", index === 0 ? "report-finding" : `finding-${index}`, "warning", 1, "indexed", reportHash, "2026-07-12T00:00:00.000Z"]);
for (let index = 0; index < 234; index += 1) database.run("INSERT INTO metadata VALUES (?, ?, ?)", [`metadata-${index}`, index === 0 ? "run.report.run-0" : `metadata-${index}`, index === 0 ? JSON.stringify({ views: { summary: { title: "legacy summary" } }, findings: [{ id: "report-finding", rootCause: "complete metadata detail" }] }) : "{}"]);
for (let index = 0; index < 234; index += 1) database.run("INSERT INTO metadata VALUES (?, ?, ?)", [`metadata-${index}`, index === 0 ? "run.report.run-0" : index === 1 ? "scheduler.heartbeat" : index === 2 ? "maintenance" : index === 3 ? "manual-trigger.latest" : `metadata-${index}`, index === 0 ? JSON.stringify({ views: { summary: { title: "legacy summary" } }, findings: [{ id: "report-finding", rootCause: "complete metadata detail" }] }) : index === 2 ? JSON.stringify(false) : JSON.stringify({ index })]);
database.close();
const source = { sentinelId: "fixture", node: "NC01", lane: "v03", path: sqlitePath, runtimeConfigRef: "fixture#runtime", stateRoot, ownerPvc: "fixture-pvc" };
const plan = planMonitorSqliteSources([source]);
@@ -73,6 +73,9 @@ test("SQLite snapshot reconciles the 228/242/234 fixture shape without productio
const store = createInMemoryMonitorCentralStore();
assert.deepEqual((await importMonitorSqliteSnapshot(snapshot, store)).imported, { inserted: 228, idempotent: 0 });
assert.deepEqual((await importMonitorSqliteSnapshot(snapshot, store)).imported, { inserted: 0, idempotent: 228 });
const importedMetadata = await store.runtimeMetadata({ sentinelId: "fixture", node: "NC01", lane: "v03" });
assert.equal(importedMetadata.length, 234);
assert.equal(importedMetadata.find((item) => item.key === "scheduler.heartbeat")?.updatedAt, snapshot.createdAt);
const importedRun = await store.run({ sentinelId: "fixture", node: "NC01", lane: "v03" }, "run-0");
assert.equal(importedRun?.artifactLocators[0]?.sha256, reportHash);
const reportFindings = importedRun?.findings.filter((finding) => finding.id === "report-finding") ?? [];
@@ -80,7 +83,9 @@ test("SQLite snapshot reconciles the 228/242/234 fixture shape without productio
assert.equal((await store.view({ sentinelId: "fixture", node: "NC01", lane: "v03" }, "run-0", "summary"))?.value.title, "legacy summary");
const pgVerification = await verifyMonitorSqliteSnapshotAgainstStore(snapshot, store);
assert.equal(pgVerification.ok, true);
assert.deepEqual([...new Set((pgVerification.checks as { name: string }[]).map((item) => item.name))].sort(), ["artifactLocator", "finding", "globalLatest", "metadata", "run", "scopedLatest", "view"]);
assert.deepEqual([...new Set((pgVerification.checks as { name: string }[]).map((item) => item.name))].sort(), ["artifactLocator", "finding", "globalOverview", "run", "runtimeMetadata", "scopedOverview", "view"]);
await store.importSource({ manifestId: "sha256:orphan-metadata", sourceFingerprint: "sha256:orphan-metadata", rowCount: 0 }, [], [{ sentinelId: "fixture", node: "NC01", lane: "v03", key: "orphan", value: { unexpected: true }, updatedAt: snapshot.createdAt }]);
assert.equal((await verifyMonitorSqliteSnapshotAgainstStore(snapshot, store)).ok, false);
assert.equal(verifyMonitorSqliteSnapshot({ ...snapshot, manifestContentHash: "sha256:tampered" }).ok, false);
} finally {
rmSync(directory, { recursive: true, force: true });
@@ -101,3 +106,9 @@ test("single-source batch rolls back conflicts and preserves manifest idempotenc
assert.equal((await store.importSource(manifest, [first])).disposition, "inserted");
assert.equal((await store.importSource(manifest, [first])).disposition, "idempotent");
});
test("runtime metadata import is scoped and does not require a legacy run", async () => {
const store = createInMemoryMonitorCentralStore();
await store.importSource({ manifestId: "sha256:metadata-only", sourceFingerprint: "sha256:metadata-only", rowCount: 1 }, [], [{ sentinelId: "fixture", node: "NC01", lane: "v03", key: "scheduler.heartbeat", value: { healthy: true }, updatedAt: "2026-07-12T00:00:00.000Z" }]);
assert.deepEqual(await store.runtimeMetadata({ sentinelId: "fixture", node: "NC01", lane: "v03" }), [{ sentinelId: "fixture", node: "NC01", lane: "v03", key: "scheduler.heartbeat", value: { healthy: true }, updatedAt: "2026-07-12T00:00:00.000Z" }]);
});