97 lines
9.0 KiB
TypeScript
97 lines
9.0 KiB
TypeScript
// SPEC: PJ2026-01060508 Web哨兵 draft-2026-07-12-p17-monitor-central-persistence.
|
|
import assert from "node:assert/strict";
|
|
import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
import test from "node:test";
|
|
import { Database } from "bun:sqlite";
|
|
|
|
import { createInMemoryMonitorCentralStore } from "./monitor-central-persistence";
|
|
import { exportMonitorSqliteSnapshot, importMonitorSqliteSnapshot, planMonitorSqliteSources, verifyMonitorSqliteSnapshot } from "./monitor-sqlite-migration";
|
|
import { buildMonitorTerminalIngest, MonitorIngestFailedError, resolveMonitorTerminalWriter, submitMonitorTerminalIngest } from "./monitor-terminal-ingest";
|
|
import { analysisSummaryFromAnalyzeResult } from "./hwlab-node-web-sentinel-p5-observe";
|
|
|
|
test("terminal ingest payload is immutable and retries bounded failures", async () => {
|
|
const report = { ok: false, findings: [{ id: "blocked" }], updatedAt: "2026-07-12T00:00:00.000Z" };
|
|
const hash = `sha256:${"a".repeat(64)}`;
|
|
const artifact = [{ relativePath: "analysis/report.json", kind: "report", sha256: hash, sizeBytes: 42 }];
|
|
const ingest = buildMonitorTerminalIngest({ sentinelId: "fixture", node: "NC01", lane: "v03", runId: "run-1" }, report, artifact, { ownerNode: "NC01", ownerLane: "v03", ownerPvc: "fixture-pvc", stateDir: "/state" });
|
|
report.findings[0].id = "changed";
|
|
assert.equal((ingest.payload.report as { findings: { id: string }[] }).findings[0].id, "blocked");
|
|
assert.match(ingest.payloadHash ?? "", /^sha256:/u);
|
|
assert.equal(ingest.payloadHash, buildMonitorTerminalIngest({ sentinelId: "fixture", node: "NC01", lane: "v03", runId: "run-1" }, { ok: false, findings: [{ id: "blocked" }], updatedAt: "2026-07-12T00:00:00.000Z" }, artifact, { ownerNode: "NC01", ownerLane: "v03", ownerPvc: "fixture-pvc", stateDir: "/state" }).payloadHash);
|
|
assert.throws(() => buildMonitorTerminalIngest({ sentinelId: "fixture", node: "NC01", lane: "v03", runId: "" }, report, artifact, { ownerNode: "NC01", ownerLane: "v03", ownerPvc: "fixture-pvc", stateDir: "/state" }), MonitorIngestFailedError);
|
|
await assert.rejects(
|
|
() => submitMonitorTerminalIngest({ endpoint: "https://monitor.example.test/api/ingest", timeoutMs: 20, retry: { maxAttempts: 2, delayMs: 0 } }, ingest, async () => new Response("{}", { status: 503 })),
|
|
(error: unknown) => error instanceof MonitorIngestFailedError && error.details.attempts === 2,
|
|
);
|
|
});
|
|
|
|
test("terminal writer requires explicit mutually exclusive authority", () => {
|
|
assert.throws(() => resolveMonitorTerminalWriter({}));
|
|
assert.deepEqual(resolveMonitorTerminalWriter({ sqlite: { path: "/state/legacy.sqlite" } }), { mode: "legacy-sqlite" });
|
|
const central = resolveMonitorTerminalWriter({ monitor: { terminalWriter: { mode: "central", ingest: { endpoint: "https://monitor.example.test/api/ingest", timeoutMs: 1_000, retry: { maxAttempts: 2, delayMs: 0 } } } } });
|
|
assert.equal(central.mode, "central");
|
|
assert.throws(() => resolveMonitorTerminalWriter({ monitor: { terminalWriter: { mode: "central" } } }));
|
|
assert.throws(() => resolveMonitorTerminalWriter({ sqlite: { path: "/state/legacy.sqlite" }, monitor: { terminalWriter: { mode: "central", ingest: { endpoint: "https://monitor.example.test/api/ingest", timeoutMs: 1_000, retry: { maxAttempts: 2, delayMs: 0 } } } } }));
|
|
});
|
|
|
|
test("runner-shaped analyze result projects nested report artifact facts", () => {
|
|
const summary = analysisSummaryFromAnalyzeResult({ ok: true, parsed: { data: { analysis: { analyzer: { stateDir: "run-1", reportJsonPath: "/state/run-1/analysis/report.json", reportJsonSha256: `sha256:${"c".repeat(64)}`, reportJsonBytes: 12, reportUpdatedAt: "2026-07-12T00:00:00.000Z" } } } }, result: {} } as never, null);
|
|
assert.deepEqual(summary && { stateDir: summary.stateDir, reportJsonSha256: summary.reportJsonSha256, reportJsonBytes: summary.reportJsonBytes, reportUpdatedAt: summary.reportUpdatedAt }, { stateDir: "run-1", reportJsonSha256: `sha256:${"c".repeat(64)}`, reportJsonBytes: 12, reportUpdatedAt: "2026-07-12T00:00:00.000Z" });
|
|
});
|
|
|
|
test("SQLite snapshot reconciles the 228/242/234 fixture shape without production assumptions", async () => {
|
|
const directory = mkdtempSync(join(tmpdir(), "monitor-migration-"));
|
|
try {
|
|
const sqlitePath = join(directory, "legacy.sqlite");
|
|
const snapshotPath = join(directory, "snapshot.json");
|
|
const database = new Database(sqlitePath);
|
|
const stateRoot = join(directory, "state");
|
|
const reportPath = join(stateRoot, "run-0", "analysis", "report.json");
|
|
mkdirSync(join(stateRoot, "run-0", "analysis"), { recursive: true });
|
|
writeFileSync(reportPath, JSON.stringify({ updatedAt: "2026-07-12T00:00:00.000Z", views: { summary: { title: "legacy summary" } }, findings: [{ id: "report-finding" }] }));
|
|
const reportHash = `sha256:${Bun.CryptoHasher.hash("sha256", readFileSync(reportPath), "hex")}`;
|
|
database.run("CREATE TABLE runs (id TEXT, state_dir TEXT, report_json_sha256 TEXT, updated_at TEXT)");
|
|
database.run("CREATE TABLE findings (run_id TEXT, finding TEXT)");
|
|
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}`, JSON.stringify({ id: index === 0 ? "report-finding" : `finding-${index}`, summary: "indexed" })]);
|
|
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" }] }) : "{}"]);
|
|
database.close();
|
|
const source = { sentinelId: "fixture", node: "NC01", lane: "v03", path: sqlitePath, runtimeConfigRef: "fixture#runtime", stateRoot, ownerPvc: "fixture-pvc" };
|
|
const plan = planMonitorSqliteSources([source]);
|
|
assert.equal((plan.sources as { exists: boolean }[])[0].exists, true);
|
|
const snapshot = exportMonitorSqliteSnapshot(source, snapshotPath);
|
|
const verified = verifyMonitorSqliteSnapshot(snapshot);
|
|
assert.equal(verified.ok, true);
|
|
assert.deepEqual(verified.reconciliation, { runs: 228, findings: 242, metadata: 234, payloads: 0, locators: 0, latest: 0 });
|
|
const persisted = JSON.parse(readFileSync(snapshotPath, "utf8"));
|
|
assert.equal(persisted.schema, snapshot.schema);
|
|
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 importedRun = await store.run({ sentinelId: "fixture", node: "NC01", lane: "v03" }, "run-0");
|
|
assert.equal(importedRun?.artifactLocators[0]?.sha256, reportHash);
|
|
assert.equal((await store.view({ sentinelId: "fixture", node: "NC01", lane: "v03" }, "run-0", "summary"))?.value.title, "legacy summary");
|
|
assert.equal(verifyMonitorSqliteSnapshot({ ...snapshot, manifestContentHash: "sha256:tampered" }).ok, false);
|
|
} finally {
|
|
rmSync(directory, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("single-source batch rolls back conflicts and preserves manifest idempotency", async () => {
|
|
const store = createInMemoryMonitorCentralStore();
|
|
const owner = { ownerNode: "NC01", ownerLane: "v03", ownerPvc: "fixture-pvc", stateDir: "run" };
|
|
const artifact = [{ relativePath: "analysis/report.json", kind: "report-json", sha256: `sha256:${"b".repeat(64)}`, sizeBytes: 1 }];
|
|
const first = buildMonitorTerminalIngest({ sentinelId: "fixture", node: "NC01", lane: "v03", runId: "first" }, { updatedAt: "2026-07-12T00:00:00.000Z" }, artifact, owner);
|
|
const existing = buildMonitorTerminalIngest({ sentinelId: "fixture", node: "NC01", lane: "v03", runId: "existing" }, { updatedAt: "2026-07-12T00:00:00.000Z", status: "ok" }, artifact, owner);
|
|
const conflicting = buildMonitorTerminalIngest({ sentinelId: "fixture", node: "NC01", lane: "v03", runId: "existing" }, { updatedAt: "2026-07-12T00:00:00.000Z", status: "blocked" }, artifact, owner);
|
|
await store.ingest(existing);
|
|
await assert.rejects(() => store.importSource({ manifestId: "sha256:batch-conflict", sourceFingerprint: "sha256:source", rowCount: 2 }, [first, conflicting]), (error: unknown) => (error as { code?: string }).code === "payload_hash_conflict");
|
|
assert.equal(await store.run({ sentinelId: "fixture", node: "NC01", lane: "v03" }, "first"), null);
|
|
const manifest = { manifestId: "sha256:batch-ok", sourceFingerprint: "sha256:source", rowCount: 1 };
|
|
assert.equal((await store.importSource(manifest, [first])).disposition, "inserted");
|
|
assert.equal((await store.importSource(manifest, [first])).disposition, "idempotent");
|
|
});
|