fix: align Release A migration semantics
This commit is contained in:
@@ -41,8 +41,8 @@ monitor:
|
||||
stateRoot: /var/lib/web-probe-sentinel-nc01
|
||||
job:
|
||||
name: hwlab-web-probe-monitor-migration
|
||||
command: plan
|
||||
readOnly: true
|
||||
workflow: export-import-verify
|
||||
readOnlySource: true
|
||||
enabled: false
|
||||
publicExposure:
|
||||
enabled: false
|
||||
|
||||
@@ -21,6 +21,7 @@ test("Release A live manifests exclude disabled migration Job and select only mo
|
||||
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(releaseAMonitorMigrationPool(spec, source)).toEqual({ poolMax: 4, idleTimeoutSeconds: 30 });
|
||||
expect(releaseAMonitorManifests(spec, imageRef, source, { migrationJobEnabled: true }).map((item) => item.kind)).toEqual(["Service", "Deployment", "Job"]);
|
||||
});
|
||||
|
||||
test("explicit migration helper renders ordered read-only export import verify Job", () => {
|
||||
@@ -29,7 +30,8 @@ test("explicit migration helper renders ordered read-only export import verify J
|
||||
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[0].args).toEqual(expect.arrayContaining(["export", "--snapshot", "/migration/snapshot.json"]));
|
||||
expect(job.spec.template.spec.initContainers[0].args).not.toContain("--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]) {
|
||||
|
||||
@@ -4,27 +4,31 @@ import { readWebProbeSentinelConfigRef } from "./hwlab-node-web-sentinel-config-
|
||||
import type { HwlabRuntimeLaneSpec } from "./hwlab-node-lanes";
|
||||
import type { MonitorSqliteSource } from "./monitor-sqlite-migration";
|
||||
|
||||
export function releaseAMonitorManifests(spec: HwlabRuntimeLaneSpec, imageRef: string, resolvedSource: MonitorSqliteSource): readonly Record<string, unknown>[] {
|
||||
export function releaseAMonitorManifests(spec: HwlabRuntimeLaneSpec, imageRef: string, resolvedSource: MonitorSqliteSource, options: { readonly migrationJobEnabled?: boolean } = {}): readonly Record<string, unknown>[] {
|
||||
const config = releaseAMonitorConfig(spec, resolvedSource);
|
||||
const { resolved, runtime, job, namespace, deploymentName, port, secret, pool, labels } = config;
|
||||
const { resolved, runtime, namespace, deploymentName, port, secret, pool, labels } = config;
|
||||
requireImageRef(imageRef);
|
||||
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 [
|
||||
const manifests = [
|
||||
{ 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" } } }] } } } },
|
||||
];
|
||||
const migrationJob = releaseAMonitorMigrationJob(spec, imageRef, resolvedSource, { enabled: options.migrationJobEnabled });
|
||||
return migrationJob === null ? manifests : [...manifests, migrationJob];
|
||||
}
|
||||
|
||||
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;
|
||||
requireImageRef(imageRef);
|
||||
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: {} }] } } } };
|
||||
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], 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 } {
|
||||
@@ -45,8 +49,10 @@ function releaseAMonitorConfig(spec: HwlabRuntimeLaneSpec, resolvedSource: Monit
|
||||
const job = recordAt(migration, "job", resolved.ref);
|
||||
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 expectedRegistry = `config/hwlab-node-lanes.yaml#lanes.${spec.lane}.targets.${spec.nodeId}.observability.webProbe.sentinels`;
|
||||
const expectedConsumer = `sentinel-${spec.nodeId.toLowerCase()}-${spec.lane}`;
|
||||
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" || textAt(persistence, "database", resolved.ref) !== "web_probe_monitor" || textAt(persistence, "schema", resolved.ref) !== "monitor" || exposure.enabled !== false || textAt(exposure, "mode", resolved.ref) !== "prepared-not-routed" || textAt(exposure, "hostname", resolved.ref).length === 0 || textAt(cicd, "mode", resolved.ref) !== "existing-pac-consumer" || textAt(cicd, "consumer", resolved.ref) !== expectedConsumer || textAt(migration, "mode", resolved.ref) !== "legacy-authority") throw new Error(`${resolved.ref} must remain a non-authoritative Release A configuration`);
|
||||
if (textAt(source, "registry", resolved.ref) !== expectedRegistry || 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, "workflow", resolved.ref) !== "export-import-verify" || job.readOnlySource !== true || typeof job.enabled !== "boolean" || !/^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/u.test(textAt(job, "name", resolved.ref))) throw new Error(`${resolved.ref}.migration must match the resolved active registry and declare an export-import-verify read-only workflow`);
|
||||
const namespace = textAt(runtime, "namespace", resolved.ref);
|
||||
const deploymentName = textAt(runtime, "deploymentName", resolved.ref);
|
||||
const port = integerAt(runtime, "servicePort", resolved.ref);
|
||||
@@ -61,3 +67,4 @@ function textAt(value: Record<string, unknown>, key: string, path: string): stri
|
||||
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, ""); }
|
||||
function requireImageRef(imageRef: string): void { if (typeof imageRef !== "string" || imageRef.trim().length === 0) throw new Error("Release A monitor imageRef must be a non-empty current PaC image reference"); }
|
||||
|
||||
@@ -214,6 +214,7 @@ function runSentinelMigration(spec: HwlabRuntimeLaneSpec, options: Extract<WebPr
|
||||
const verification = verifyMonitorSqliteSnapshot(snapshot);
|
||||
if (verification.ok !== true) throw new Error("migration snapshot verification failed");
|
||||
if (options.action === "import") {
|
||||
if (!options.confirm) throw new Error("web-probe sentinel migration import requires --confirm");
|
||||
if (!process.env.DATABASE_URL) throw new Error("monitor migration DATABASE_URL must be injected by the controlled Secret consumer");
|
||||
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) } });
|
||||
|
||||
@@ -223,7 +223,7 @@ export function parseNodeWebProbeSentinelOptions(args: string[]): NodeWebProbeSe
|
||||
if (migrationAction !== "plan" && migrationAction !== "export" && migrationAction !== "import" && migrationAction !== "verify") {
|
||||
throw new Error("web-probe sentinel migration usage: migration plan|export|import|verify --node NODE --lane vNN [--sentinel ID] [--snapshot PATH] [--confirm] [--json]");
|
||||
}
|
||||
if ((migrationAction === "import" || migrationAction === "export") && !confirm) throw new Error(`web-probe sentinel migration ${migrationAction} requires --confirm`);
|
||||
if (migrationAction === "import" && !confirm) throw new Error("web-probe sentinel migration import requires --confirm");
|
||||
sentinel = { kind: "migration", action: migrationAction, node, lane, sentinelId, snapshotPath: optionValue(args, "--snapshot") ?? null, confirm, json: args.includes("--json") };
|
||||
} else if (sentinelActionRaw === "report") {
|
||||
const view = parseWebProbeSentinelReportView(optionValue(args, "--view") ?? "summary");
|
||||
|
||||
@@ -40,7 +40,7 @@ export async function verifyMonitorSqliteSnapshotAgainstStore(snapshot: MonitorS
|
||||
const globalLatest = latestFor(expected);
|
||||
const global = await store.overview({});
|
||||
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()];
|
||||
const scopes = [...new Map([{ sentinelId: snapshot.source.sentinelId, node: snapshot.source.node, lane: snapshot.source.lane }, ...expected.map((item) => ({ sentinelId: item.sentinelId, node: item.node, lane: item.lane })), ...expectedMetadata.map((item) => ({ sentinelId: item.sentinelId, node: item.node, lane: item.lane }))].map((scope) => [`${scope.sentinelId}/${scope.node}/${scope.lane}`, scope])).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);
|
||||
@@ -50,7 +50,7 @@ export async function verifyMonitorSqliteSnapshotAgainstStore(snapshot: MonitorS
|
||||
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: { source: reconcileTables(snapshot.tables), target: { runs: expected.length, runtimeMetadata: expectedMetadata.length } }, 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: targetReconciliation(expected, expectedMetadata) }, checks, valuesRedacted: true };
|
||||
}
|
||||
|
||||
export function planMonitorSqliteSources(sources: readonly MonitorSqliteSource[]): Record<string, unknown> {
|
||||
@@ -96,7 +96,7 @@ export async function importMonitorSqliteSnapshot(snapshot: MonitorSqliteSnapsho
|
||||
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: { source: reconciliation, target: { runs: ingests.length, runtimeMetadata: runtimeMetadata.length } }, 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: targetReconciliation(ingests, runtimeMetadata) }, payloadHashCount: new Set(ingests.map((item) => item.payloadHash)).size, valuesRedacted: true };
|
||||
}
|
||||
|
||||
export function reconcileTables(tables: readonly MonitorSqliteTable[]): MonitorMigrationReconciliation {
|
||||
@@ -123,6 +123,7 @@ 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 targetReconciliation(ingests: readonly ReturnType<typeof normalizeMonitorCentralIngest>[], runtimeMetadata: readonly MonitorRuntimeMetadata[]): Record<string, number> { return { runs: ingests.length, findings: ingests.reduce((total, item) => total + item.findings.length, 0), payloads: ingests.length, artifactLocators: ingests.reduce((total, item) => total + item.artifactLocators.length, 0), views: ingests.reduce((total, item) => total + (isRecord(item.payload.views) ? Object.keys(item.payload.views).length : 0), 0), runtimeMetadata: runtimeMetadata.length }; }
|
||||
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 }; }
|
||||
|
||||
@@ -84,6 +84,7 @@ test("SQLite snapshot reconciles the 228/242/234 fixture shape without productio
|
||||
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", "globalOverview", "run", "runtimeMetadata", "scopedOverview", "view"]);
|
||||
assert.deepEqual((pgVerification.reconciliation as { target: Record<string, number> }).target, { runs: 228, findings: 242, payloads: 228, artifactLocators: 1, views: 1, runtimeMetadata: 234 });
|
||||
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);
|
||||
@@ -107,8 +108,23 @@ test("single-source batch rolls back conflicts and preserves manifest idempotenc
|
||||
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" }]);
|
||||
test("metadata-only snapshot verifies scoped runtime metadata and rejects extras", async () => {
|
||||
const directory = mkdtempSync(join(tmpdir(), "monitor-metadata-only-"));
|
||||
try {
|
||||
const sqlitePath = join(directory, "index.sqlite");
|
||||
const snapshotPath = join(directory, "snapshot.json");
|
||||
const database = new Database(sqlitePath);
|
||||
database.run("CREATE TABLE metadata (key TEXT, value_json TEXT)");
|
||||
database.run("INSERT INTO metadata VALUES (?, ?)", ["scheduler.heartbeat", JSON.stringify({ healthy: true })]);
|
||||
database.close();
|
||||
const source = { sentinelId: "fixture", node: "NC01", lane: "v03", path: sqlitePath, runtimeConfigRef: "fixture#runtime", stateRoot: directory, ownerPvc: "fixture-pvc" };
|
||||
const snapshot = exportMonitorSqliteSnapshot(source, snapshotPath);
|
||||
const store = createInMemoryMonitorCentralStore();
|
||||
await importMonitorSqliteSnapshot(snapshot, store);
|
||||
assert.equal((await verifyMonitorSqliteSnapshotAgainstStore(snapshot, store)).ok, true);
|
||||
await store.importSource({ manifestId: "sha256:metadata-only-extra", sourceFingerprint: "sha256:metadata-only-extra", rowCount: 0 }, [], [{ sentinelId: "fixture", node: "NC01", lane: "v03", key: "orphan", value: true, updatedAt: snapshot.createdAt }]);
|
||||
assert.equal((await verifyMonitorSqliteSnapshotAgainstStore(snapshot, store)).ok, false);
|
||||
} finally {
|
||||
rmSync(directory, { recursive: true, force: true });
|
||||
}
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user