mirror of
https://github.com/openclaw/openclaw.git
synced 2026-09-28 14:12:28 +08:00
* test: prepare bounded TypeScript child probes Prepare native test children through the invocation-owned compiler before their existing deadlines. Preserve module boundaries for import guards and script entry detection, and keep source-loader contracts explicit. Retain native process isolation, worker lifetimes, assertions, and deadlines. Cover MTS declaration selection in the existing compiler fixture. Also declare the generated fallback plugin fixture as ESM and remove one unused internal script export. * test: prepare remaining worker and tooling fixtures Keep installed tooling at its native package location so prepared probes retain native bindings and relative assets. Preserve existing child deadlines, assertions, and source-layout contracts. * test: assert native CLI delegation before option rejection * test: preserve sparse prepared fixture entry contracts * test: mark prepared budget fixtures as ES modules * test: preserve serialized fixture inputs under type-aware lint * test: prepare sparse wrapper and report fixtures * test: prepare remaining CLI wrapper children * test: complete bounded child fixture preparation
525 lines
22 KiB
TypeScript
525 lines
22 KiB
TypeScript
// SQLite reliability proof tests cover CLI safety and one real snapshot round trip.
|
|
import { fork, spawnSync, type ChildProcess } from "node:child_process";
|
|
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import { afterEach, describe, expect, it } from "vitest";
|
|
import { parseSqliteReliabilityCli } from "../../scripts/lib/sqlite-reliability-cli.js";
|
|
import {
|
|
formatReliabilityStderr,
|
|
STRESS_TABLE_SQL,
|
|
type ReliabilityReport,
|
|
} from "../../scripts/lib/sqlite-reliability-contract.js";
|
|
import { monitorSqliteWalDuring } from "../../scripts/lib/sqlite-reliability-wal-monitor.js";
|
|
import {
|
|
canonicalPathWithExistingParent,
|
|
isPendingPathInRepository,
|
|
} from "../../scripts/lib/sqlite-reliability-worker-paths.js";
|
|
import { resolveVitestNodeArgs } from "../../scripts/lib/vitest-process-env.mts";
|
|
import { openNodeSqliteDatabase } from "../../src/infra/node-sqlite.js";
|
|
import {
|
|
resolveRuntimeWorkerThreadExecArgv,
|
|
resolveRuntimeWorkerUrl,
|
|
} from "../../src/infra/runtime-worker-url.js";
|
|
import {
|
|
closeOpenClawStateDatabaseForTest,
|
|
openOpenClawStateDatabase,
|
|
} from "../../src/state/openclaw-state-db.js";
|
|
import { resolveTestNodeExecPath } from "../../src/test-utils/node-process.js";
|
|
import { useAutoCleanupTempDirTracker } from "../helpers/temp-dir.js";
|
|
import { toolingTsEntrypoints } from "./tooling-ts-runtime.test-support.js";
|
|
|
|
const tempDirs = useAutoCleanupTempDirTracker(afterEach);
|
|
const nodeExecutable = resolveTestNodeExecPath();
|
|
const nodeArgs = resolveVitestNodeArgs();
|
|
// Windows repeats ACL checks and crash/restore copies throughout the full proof.
|
|
const RELIABILITY_PROOF_TIMEOUT_MS = process.platform === "win32" ? 480_000 : 240_000;
|
|
const RELIABILITY_SMOKE_TEST_TIMEOUT_MS = process.platform === "win32" ? 1_200_000 : 300_000;
|
|
const MIN_MULTICHUNK_RESTORE_BYTES = 2 * 1024 * 1024;
|
|
const COMPACTION_FIXTURE_ROWS = 12;
|
|
const COMPACTION_PAYLOAD_BYTES = 256 * 1024;
|
|
const VACUUM_PROOF_ROWS = 64;
|
|
|
|
function reliabilitySmokeTest(name: string, test: () => void): void {
|
|
it(name, test, RELIABILITY_SMOKE_TEST_TIMEOUT_MS);
|
|
}
|
|
|
|
function runProof(args: string[], env: NodeJS.ProcessEnv = {}) {
|
|
const result = spawnSync(
|
|
nodeExecutable,
|
|
[...nodeArgs, "--import", "tsx", "scripts/bench-sqlite-reliability.ts", ...args],
|
|
{
|
|
cwd: process.cwd(),
|
|
env: { ...process.env, ...env },
|
|
encoding: "utf8",
|
|
timeout: RELIABILITY_PROOF_TIMEOUT_MS,
|
|
},
|
|
);
|
|
if (result.error) {
|
|
throw result.error;
|
|
}
|
|
return result;
|
|
}
|
|
|
|
async function waitForChildReady(child: ChildProcess): Promise<void> {
|
|
await new Promise<void>((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
cleanup();
|
|
reject(new Error("writer child did not become ready"));
|
|
}, 10_000);
|
|
const onMessage = (message: unknown) => {
|
|
if (
|
|
message &&
|
|
typeof message === "object" &&
|
|
(message as { kind?: unknown }).kind === "ready"
|
|
) {
|
|
cleanup();
|
|
resolve();
|
|
}
|
|
};
|
|
const onError = (error: Error) => {
|
|
cleanup();
|
|
reject(error);
|
|
};
|
|
const onExit = () => {
|
|
cleanup();
|
|
reject(new Error("writer child exited before ready"));
|
|
};
|
|
const cleanup = () => {
|
|
clearTimeout(timeout);
|
|
child.off("message", onMessage);
|
|
child.off("error", onError);
|
|
child.off("exit", onExit);
|
|
};
|
|
child.on("message", onMessage);
|
|
child.on("error", onError);
|
|
child.on("exit", onExit);
|
|
});
|
|
}
|
|
|
|
async function waitForChildExit(child: ChildProcess): Promise<{
|
|
code: number | null;
|
|
signal: NodeJS.Signals | null;
|
|
}> {
|
|
return await new Promise((resolve, reject) => {
|
|
const timeout = setTimeout(() => {
|
|
cleanup();
|
|
reject(new Error("writer child did not exit after IPC disconnect"));
|
|
}, 10_000);
|
|
const onExit = (code: number | null, signal: NodeJS.Signals | null) => {
|
|
cleanup();
|
|
resolve({ code, signal });
|
|
};
|
|
const onError = (error: Error) => {
|
|
cleanup();
|
|
reject(error);
|
|
};
|
|
const cleanup = () => {
|
|
clearTimeout(timeout);
|
|
child.off("exit", onExit);
|
|
child.off("error", onError);
|
|
};
|
|
child.on("exit", onExit);
|
|
child.on("error", onError);
|
|
});
|
|
}
|
|
|
|
describe("scripts/bench-sqlite-reliability", () => {
|
|
it.each([
|
|
["whitespace-only stderr", " \n\t ", ""],
|
|
[
|
|
"multiline quoted stderr",
|
|
' first line\n"quoted"\\path ',
|
|
' stderr="first line\\n\\"quoted\\"\\\\path"',
|
|
],
|
|
])("formats %s", (_name, stderr, expected) => {
|
|
expect(formatReliabilityStderr(stderr)).toBe(expected);
|
|
});
|
|
|
|
it("detects a transient WAL overrun before the file shrinks", async () => {
|
|
const walPath = path.join(
|
|
tempDirs.make("openclaw-sqlite-reliability-test-"),
|
|
"database.sqlite-wal",
|
|
);
|
|
let stopRequests = 0;
|
|
|
|
await expect(
|
|
monitorSqliteWalDuring({
|
|
maxWalBytes: 1024,
|
|
onLimitExceeded: () => {
|
|
stopRequests += 1;
|
|
},
|
|
operation: async () => {
|
|
fs.writeFileSync(walPath, Buffer.alloc(2048));
|
|
await new Promise((resolve) => {
|
|
setTimeout(resolve, 25);
|
|
});
|
|
fs.truncateSync(walPath, 0);
|
|
return "complete";
|
|
},
|
|
pollIntervalMs: 5,
|
|
walPath,
|
|
}),
|
|
).rejects.toThrow("SQLite reliability WAL exceeded the 1024-byte profile limit: 2048 bytes");
|
|
expect(stopRequests).toBe(1);
|
|
});
|
|
|
|
it("rejects malformed arguments before creating state", () => {
|
|
const unknown = runProof(["--wat"]);
|
|
expect(unknown.status).toBe(2);
|
|
expect(unknown.stdout).toBe("");
|
|
expect(unknown.stderr.trim()).toBe("error: Unknown argument: --wat");
|
|
|
|
expect(() => parseSqliteReliabilityCli(["--profile", "smoke", "--profile", "large"])).toThrow(
|
|
"--profile was provided more than once",
|
|
);
|
|
expect(() => parseSqliteReliabilityCli(["--profile", "huge"])).toThrow(
|
|
'--profile must be one of smoke, default, large; got "huge"',
|
|
);
|
|
expect(parseSqliteReliabilityCli(["--help", "--profile", "huge"])).toEqual({
|
|
help: true,
|
|
});
|
|
});
|
|
|
|
reliabilitySmokeTest("proves snapshot reliability while safely reusing state", () => {
|
|
const stateDir = tempDirs.make("openclaw-sqlite-reliability-test-");
|
|
const previousSyncedRepository = path.join(
|
|
stateDir,
|
|
"sqlite-reliability-runs",
|
|
"previous-run",
|
|
"synced-snapshots",
|
|
);
|
|
const previousArtifact = path.join(previousSyncedRepository, "previous-artifact");
|
|
fs.mkdirSync(previousSyncedRepository, { recursive: true, mode: 0o700 });
|
|
fs.writeFileSync(previousArtifact, "retained");
|
|
|
|
const existingDatabase = openOpenClawStateDatabase({
|
|
env: { ...process.env, OPENCLAW_STATE_DIR: stateDir },
|
|
});
|
|
try {
|
|
existingDatabase.db.exec(STRESS_TABLE_SQL);
|
|
existingDatabase.db
|
|
.prepare(
|
|
"INSERT INTO openclaw_reliability_entries (batch, ordinal, payload) VALUES (?, ?, ?)",
|
|
)
|
|
.run(999_999, 0, "stale-profile-row");
|
|
} finally {
|
|
closeOpenClawStateDatabaseForTest();
|
|
}
|
|
|
|
const output = path.join(stateDir, "report.json");
|
|
const compilerPolicyProbe = path.join(stateDir, "compiler-policy-probe.mjs");
|
|
fs.writeFileSync(
|
|
compilerPolicyProbe,
|
|
`import { isMainThread } from "node:worker_threads";
|
|
if (isMainThread && !process.execArgv.includes("--no-concurrent-sparkplug")) {
|
|
throw new Error("SQLite proof subprocess discarded the selected Node compiler policy");
|
|
}
|
|
`,
|
|
);
|
|
const result = runProof(["--profile", "smoke", "--state-dir", stateDir, "--output", output], {
|
|
NODE_OPTIONS: [
|
|
process.env.NODE_OPTIONS,
|
|
`--import=${pathToFileURL(compilerPolicyProbe).href}`,
|
|
]
|
|
.filter(Boolean)
|
|
.join(" "),
|
|
});
|
|
|
|
expect(result.status, result.stderr).toBe(0);
|
|
expect(result.stderr).toBe("");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_TARGET=global");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_RESTORES_VERIFIED=5");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_CRASH_RECOVERY=verified");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_PUBLICATION_INTERRUPTION=verified");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_RESTORE_INTERRUPTION=verified");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_REPOSITORY_INTERRUPTION=verified");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_INDEX_REPAIR_INTERRUPTION=verified");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_VACUUM_INTERRUPTION=verified");
|
|
expect(result.stdout).toContain("SQLITE_RELIABILITY_POST_COMPACT_RESTORE=verified");
|
|
expect(result.stdout).not.toContain("=missing");
|
|
const firstReport = JSON.parse(fs.readFileSync(output, "utf8")) as ReliabilityReport;
|
|
expect(firstReport.concurrentRestoresVerified).toBe(2);
|
|
expect(firstReport.restoresVerified).toBe(5);
|
|
expect(
|
|
firstReport.crashRecoveryProof.exit.code !== null ||
|
|
firstReport.crashRecoveryProof.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(firstReport.crashRecoveryProof.stateAfterRecovery).toEqual(
|
|
firstReport.crashRecoveryProof.stateBeforeKill,
|
|
);
|
|
expect(firstReport.publicationInterruptionProof.beforePublish).toMatchObject({
|
|
recoveryVerified: true,
|
|
retryPublished: true,
|
|
sourceStatePreserved: true,
|
|
targetVerifiedAfterCrash: false,
|
|
targetVisibleAfterCrash: false,
|
|
});
|
|
expect(firstReport.publicationInterruptionProof.beforePublish.stagingEntries).toBeGreaterThan(
|
|
0,
|
|
);
|
|
expect(
|
|
firstReport.publicationInterruptionProof.beforePublish.exit.code !== null ||
|
|
firstReport.publicationInterruptionProof.beforePublish.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(firstReport.publicationInterruptionProof.afterPublish).toMatchObject({
|
|
existingTargetPreserved: true,
|
|
recoveryVerified: true,
|
|
sourceStatePreserved: true,
|
|
targetVerifiedAfterCrash: true,
|
|
targetVisibleAfterCrash: true,
|
|
});
|
|
expect(firstReport.publicationInterruptionProof.afterPublish.stagingEntries).toBeGreaterThan(0);
|
|
expect(
|
|
firstReport.publicationInterruptionProof.afterPublish.exit.code !== null ||
|
|
firstReport.publicationInterruptionProof.afterPublish.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(firstReport.indexRepairInterruptionProof.rollbackJournal).toMatchObject({
|
|
recoveryVerified: true,
|
|
repairedIndexes: ["idx_openclaw_reliability_records_identity"],
|
|
});
|
|
expect(
|
|
firstReport.indexRepairInterruptionProof.rollbackJournal.journalBytesObserved,
|
|
).toBeGreaterThan(0);
|
|
expect(
|
|
firstReport.indexRepairInterruptionProof.rollbackJournal.exit.code !== null ||
|
|
firstReport.indexRepairInterruptionProof.rollbackJournal.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(firstReport.indexRepairInterruptionProof.wal).toMatchObject({
|
|
recoveryVerified: true,
|
|
repairedIndexes: ["idx_openclaw_reliability_records_identity"],
|
|
});
|
|
expect(firstReport.indexRepairInterruptionProof.wal.walBytesObserved).toBeGreaterThan(0);
|
|
expect(
|
|
firstReport.indexRepairInterruptionProof.wal.exit.code !== null ||
|
|
firstReport.indexRepairInterruptionProof.wal.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(firstReport.transactionProof.committedWalSentinel).toBe(true);
|
|
expect(firstReport.transactionProof.heldRows).toBeGreaterThan(0);
|
|
expect(firstReport.transactionProof.visibleAfterRestore).toBe(false);
|
|
expect(firstReport.writer.rowsCommitted).toBeGreaterThan(0);
|
|
expect(firstReport.maintenanceProof.bloatBytes).toBe(
|
|
VACUUM_PROOF_ROWS * COMPACTION_PAYLOAD_BYTES,
|
|
);
|
|
expect(firstReport.maintenanceProof.compaction.autoVacuum.after).toBe(2);
|
|
expect(firstReport.maintenanceProof.compaction.freelistPages.before).toBeGreaterThan(0);
|
|
expect(firstReport.maintenanceProof.compaction.freelistPages.after).toBe(0);
|
|
expect(firstReport.maintenanceProof.compaction.reclaimedBytes).toBeGreaterThan(0);
|
|
expect(firstReport.maintenanceProof.compaction.walBytes.after).toBe(0);
|
|
expect(firstReport.maintenanceProof.vacuumInterruption.recoveryVerified).toBe(true);
|
|
expect(firstReport.maintenanceProof.vacuumInterruption.autoVacuumBeforeKill).toBe(0);
|
|
expect(firstReport.maintenanceProof.vacuumInterruption.autoVacuumAfterRecovery).toBe(0);
|
|
expect(
|
|
firstReport.maintenanceProof.vacuumInterruption.exit.code !== null ||
|
|
firstReport.maintenanceProof.vacuumInterruption.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(
|
|
firstReport.maintenanceProof.vacuumInterruption.journalBytesObserved > 0 ||
|
|
firstReport.maintenanceProof.vacuumInterruption.walBytesObserved > 0,
|
|
).toBe(true);
|
|
expect(firstReport.maintenanceProof.vacuumInterruption.payloadAfterRecovery).toEqual(
|
|
firstReport.maintenanceProof.vacuumInterruption.payloadBeforeKill,
|
|
);
|
|
expect(firstReport.maintenanceProof.vacuumInterruption.payloadBeforeKill).toEqual({
|
|
bytes: VACUUM_PROOF_ROWS * COMPACTION_PAYLOAD_BYTES,
|
|
idSum: (VACUUM_PROOF_ROWS * (VACUUM_PROOF_ROWS + 1)) / 2,
|
|
rows: VACUUM_PROOF_ROWS,
|
|
});
|
|
expect(firstReport.maintenanceProof.vacuumInterruption.stateAfterRecovery).toEqual(
|
|
firstReport.maintenanceProof.vacuumInterruption.stateBeforeKill,
|
|
);
|
|
expect(firstReport.maintenanceProof.repositoryInterruption.beforePending).toMatchObject({
|
|
crashSnapshotVerifiedAfterCrash: false,
|
|
crashSnapshotVisibleAfterCrash: false,
|
|
incompleteEntries: 1,
|
|
repositoryVerified: true,
|
|
retryCreated: true,
|
|
sourcePayloadPreserved: true,
|
|
sourceStatePreserved: true,
|
|
visibleSnapshotsAfterCrash: 1,
|
|
});
|
|
expect(
|
|
firstReport.maintenanceProof.repositoryInterruption.beforePending.stagingEntries,
|
|
).toBeGreaterThan(0);
|
|
expect(
|
|
firstReport.maintenanceProof.repositoryInterruption.beforePending.exit.code !== null ||
|
|
firstReport.maintenanceProof.repositoryInterruption.beforePending.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(firstReport.maintenanceProof.repositoryInterruption.pending).toMatchObject({
|
|
crashSnapshotVerifiedAfterCrash: true,
|
|
crashSnapshotVisibleAfterCrash: true,
|
|
incompleteEntries: 0,
|
|
repositoryVerified: true,
|
|
retryCreated: true,
|
|
sourcePayloadPreserved: true,
|
|
sourceStatePreserved: true,
|
|
visibleSnapshotsAfterCrash: 3,
|
|
});
|
|
expect(
|
|
firstReport.maintenanceProof.repositoryInterruption.pending.stagingEntries,
|
|
).toBeGreaterThan(0);
|
|
expect(
|
|
firstReport.maintenanceProof.repositoryInterruption.pending.exit.code !== null ||
|
|
firstReport.maintenanceProof.repositoryInterruption.pending.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(firstReport.maintenanceProof.repositoryInterruption.afterCommit).toMatchObject({
|
|
crashSnapshotVerifiedAfterCrash: true,
|
|
crashSnapshotVisibleAfterCrash: true,
|
|
incompleteEntries: 0,
|
|
repositoryVerified: true,
|
|
retryCreated: true,
|
|
sourcePayloadPreserved: true,
|
|
sourceStatePreserved: true,
|
|
visibleSnapshotsAfterCrash: 5,
|
|
});
|
|
expect(
|
|
firstReport.maintenanceProof.repositoryInterruption.afterCommit.stagingEntries,
|
|
).toBeGreaterThan(0);
|
|
expect(
|
|
firstReport.maintenanceProof.repositoryInterruption.afterCommit.exit.code !== null ||
|
|
firstReport.maintenanceProof.repositoryInterruption.afterCommit.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(firstReport.maintenanceProof.repositoryInterruption.beforePending.state).toEqual(
|
|
firstReport.maintenanceProof.repositoryInterruption.pending.state,
|
|
);
|
|
expect(firstReport.maintenanceProof.repositoryInterruption.pending.state).toEqual(
|
|
firstReport.maintenanceProof.repositoryInterruption.afterCommit.state,
|
|
);
|
|
expect(firstReport.maintenanceProof.repositoryInterruption.beforePending.payload).toEqual(
|
|
firstReport.maintenanceProof.repositoryInterruption.pending.payload,
|
|
);
|
|
expect(firstReport.maintenanceProof.repositoryInterruption.pending.payload).toEqual(
|
|
firstReport.maintenanceProof.repositoryInterruption.afterCommit.payload,
|
|
);
|
|
expect(firstReport.maintenanceProof.repositoryInterruption.beforePending.payload).toEqual({
|
|
bytes: COMPACTION_FIXTURE_ROWS * COMPACTION_PAYLOAD_BYTES,
|
|
idSum: (COMPACTION_FIXTURE_ROWS * (COMPACTION_FIXTURE_ROWS + 1)) / 2,
|
|
rows: COMPACTION_FIXTURE_ROWS,
|
|
});
|
|
expect(firstReport.maintenanceProof.restoreInterruption.snapshotBytes).toBeGreaterThan(
|
|
MIN_MULTICHUNK_RESTORE_BYTES,
|
|
);
|
|
expect(firstReport.maintenanceProof.restoreInterruption.beforePublish).toMatchObject({
|
|
existingTargetPreserved: false,
|
|
recoveryVerified: true,
|
|
repositoryVerified: true,
|
|
retryRestored: true,
|
|
targetVerifiedAfterCrash: false,
|
|
targetVisibleAfterCrash: false,
|
|
});
|
|
expect(
|
|
firstReport.maintenanceProof.restoreInterruption.beforePublish.stagingEntries,
|
|
).toBeGreaterThan(0);
|
|
expect(
|
|
firstReport.maintenanceProof.restoreInterruption.beforePublish.exit.code !== null ||
|
|
firstReport.maintenanceProof.restoreInterruption.beforePublish.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(
|
|
firstReport.maintenanceProof.restoreInterruption.beforePublish.stateAfterRecovery,
|
|
).toEqual(firstReport.maintenanceProof.postCompact.state);
|
|
expect(
|
|
firstReport.maintenanceProof.restoreInterruption.beforePublish.payloadAfterRecovery,
|
|
).toEqual({
|
|
bytes: COMPACTION_FIXTURE_ROWS * COMPACTION_PAYLOAD_BYTES,
|
|
idSum: (COMPACTION_FIXTURE_ROWS * (COMPACTION_FIXTURE_ROWS + 1)) / 2,
|
|
rows: COMPACTION_FIXTURE_ROWS,
|
|
});
|
|
expect(firstReport.maintenanceProof.restoreInterruption.afterPublish).toMatchObject({
|
|
existingTargetPreserved: true,
|
|
recoveryVerified: true,
|
|
repositoryVerified: true,
|
|
retryRestored: false,
|
|
targetVerifiedAfterCrash: true,
|
|
targetVisibleAfterCrash: true,
|
|
});
|
|
expect(
|
|
firstReport.maintenanceProof.restoreInterruption.afterPublish.stagingEntries,
|
|
).toBeGreaterThan(0);
|
|
expect(
|
|
firstReport.maintenanceProof.restoreInterruption.afterPublish.exit.code !== null ||
|
|
firstReport.maintenanceProof.restoreInterruption.afterPublish.exit.signal !== null,
|
|
).toBe(true);
|
|
expect(
|
|
firstReport.maintenanceProof.restoreInterruption.afterPublish.stateAfterRecovery,
|
|
).toEqual(firstReport.maintenanceProof.restoreInterruption.beforePublish.stateAfterRecovery);
|
|
expect(
|
|
firstReport.maintenanceProof.restoreInterruption.afterPublish.payloadAfterRecovery,
|
|
).toEqual(firstReport.maintenanceProof.restoreInterruption.beforePublish.payloadAfterRecovery);
|
|
expect(firstReport.maintenanceProof.postCompact.restoreVerified).toBe(true);
|
|
expect(firstReport.maintenanceProof.postCompact.state.batches).toBeGreaterThan(0);
|
|
expect(firstReport.maintenanceProof.postCompact.state.rows).toBeGreaterThan(0);
|
|
expect(firstReport.maintenanceProof.postCompact.state.sha256).toMatch(/^[a-f0-9]{64}$/);
|
|
expect(firstReport.walBytes.limit).toBeGreaterThan(0);
|
|
expect(firstReport.walBytes.peak).toBeGreaterThan(0);
|
|
expect(firstReport.walBytes.peak).toBeLessThanOrEqual(firstReport.walBytes.limit);
|
|
|
|
expect(firstReport.paths.syncedRepository).not.toBe(previousSyncedRepository);
|
|
expect(fs.readFileSync(previousArtifact, "utf8")).toBe("retained");
|
|
|
|
const database = openNodeSqliteDatabase(firstReport.paths.sourceDatabase, { readOnly: true });
|
|
try {
|
|
const staleRows = database
|
|
.prepare("SELECT COUNT(*) AS rows FROM openclaw_reliability_entries WHERE batch = ?")
|
|
.get(999_999) as { rows?: unknown };
|
|
expect(Number(staleRows.rows)).toBe(0);
|
|
} finally {
|
|
database.close();
|
|
}
|
|
});
|
|
|
|
it("matches crash barriers across filesystem path aliases", () => {
|
|
const realRoot = tempDirs.make("openclaw-sqlite-reliability-test-");
|
|
const aliasRoot = path.join(tempDirs.make("openclaw-sqlite-reliability-test-"), "alias");
|
|
fs.symlinkSync(realRoot, aliasRoot, process.platform === "win32" ? "junction" : "dir");
|
|
const repositoryPath = path.join(realRoot, "snapshots");
|
|
const snapshotPath = path.join(repositoryPath, "snapshot");
|
|
fs.mkdirSync(snapshotPath, { recursive: true });
|
|
|
|
const canonicalRepositoryPath = canonicalPathWithExistingParent(
|
|
path.join(aliasRoot, "snapshots"),
|
|
);
|
|
expect(canonicalRepositoryPath).toBe(path.join(fs.realpathSync.native(realRoot), "snapshots"));
|
|
expect(
|
|
isPendingPathInRepository(
|
|
path.join(aliasRoot, "snapshots", "snapshot", ".pending"),
|
|
canonicalRepositoryPath,
|
|
),
|
|
).toBe(true);
|
|
|
|
const finalAlias = path.join(realRoot, "final-alias");
|
|
fs.symlinkSync(snapshotPath, finalAlias, process.platform === "win32" ? "junction" : "dir");
|
|
expect(canonicalPathWithExistingParent(finalAlias)).toBe(
|
|
path.join(fs.realpathSync.native(realRoot), "final-alias"),
|
|
);
|
|
});
|
|
|
|
it("stops the writer when its parent IPC channel disconnects", async () => {
|
|
const databasePath = path.join(
|
|
tempDirs.make("openclaw-sqlite-reliability-test-"),
|
|
"writer.sqlite",
|
|
);
|
|
const writerUrl = resolveRuntimeWorkerUrl(toolingTsEntrypoints.sqliteReliabilityWriter);
|
|
const child = fork(
|
|
fileURLToPath(writerUrl),
|
|
[databasePath, "8", "64", "4", "256", String(64 * 1024 * 1024), "1"],
|
|
{
|
|
cwd: process.cwd(),
|
|
execPath: nodeExecutable,
|
|
execArgv: [...nodeArgs, ...resolveRuntimeWorkerThreadExecArgv(writerUrl, nodeExecutable)],
|
|
serialization: "json",
|
|
stdio: ["ignore", "ignore", "pipe", "ipc"],
|
|
},
|
|
);
|
|
try {
|
|
await waitForChildReady(child);
|
|
const exitPromise = waitForChildExit(child);
|
|
child.disconnect();
|
|
await expect(exitPromise).resolves.toEqual({ code: 0, signal: null });
|
|
} finally {
|
|
if (child.exitCode === null && child.signalCode === null) {
|
|
child.kill();
|
|
}
|
|
}
|
|
});
|
|
});
|