diff --git a/config/hwlab-web-probe-monitor/runtime.yaml b/config/hwlab-web-probe-monitor/runtime.yaml index 965a8d81..68df1fab 100644 --- a/config/hwlab-web-probe-monitor/runtime.yaml +++ b/config/hwlab-web-probe-monitor/runtime.yaml @@ -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 diff --git a/scripts/src/hwlab-node-web-probe-monitor.test.ts b/scripts/src/hwlab-node-web-probe-monitor.test.ts index c55f892f..3f65d2ba 100644 --- a/scripts/src/hwlab-node-web-probe-monitor.test.ts +++ b/scripts/src/hwlab-node-web-probe-monitor.test.ts @@ -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]) { diff --git a/scripts/src/hwlab-node-web-probe-monitor.ts b/scripts/src/hwlab-node-web-probe-monitor.ts index 42823f51..3192c139 100644 --- a/scripts/src/hwlab-node-web-probe-monitor.ts +++ b/scripts/src/hwlab-node-web-probe-monitor.ts @@ -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[] { +export function releaseAMonitorManifests(spec: HwlabRuntimeLaneSpec, imageRef: string, resolvedSource: MonitorSqliteSource, options: { readonly migrationJobEnabled?: boolean } = {}): readonly Record[] { 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 | 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, key: string, path: string): stri function integerAt(value: Record, 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 { 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"); } diff --git a/scripts/src/hwlab-node-web-sentinel-cicd.ts b/scripts/src/hwlab-node-web-sentinel-cicd.ts index 3817ed2b..fa82b1cc 100644 --- a/scripts/src/hwlab-node-web-sentinel-cicd.ts +++ b/scripts/src/hwlab-node-web-sentinel-cicd.ts @@ -214,6 +214,7 @@ function runSentinelMigration(spec: HwlabRuntimeLaneSpec, options: Extract [`${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 { @@ -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[], runtimeMetadata: readonly MonitorRuntimeMetadata[]): Record { 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, 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 }; } diff --git a/scripts/src/monitor-terminal-ingest-migration.test.ts b/scripts/src/monitor-terminal-ingest-migration.test.ts index 1862ea94..17749a5d 100644 --- a/scripts/src/monitor-terminal-ingest-migration.test.ts +++ b/scripts/src/monitor-terminal-ingest-migration.test.ts @@ -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 }).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 }); + } });