
























11// Covers SQLite WAL maintenance configuration.
2+import childProcess from "node:child_process";
3+import fs from "node:fs";
4+import os from "node:os";
5+import path from "node:path";
26import type { DatabaseSync } from "node:sqlite";
37import { afterEach, describe, expect, it, vi } from "vitest";
48import { MAX_TIMER_TIMEOUT_MS } from "../shared/number-coercion.js";
@@ -10,11 +14,27 @@ import {
1014function createMockDb(): DatabaseSync {
1115return {
1216exec: vi.fn(),
17+prepare: vi.fn(() => ({
18+get: vi.fn(() => ({ journal_mode: "delete" })),
19+})),
1320} as unknown as DatabaseSync;
1421}
152223+function statfsFixture(type: number): ReturnType<typeof fs.statfsSync> {
24+return {
25+ type,
26+bsize: 1024,
27+blocks: 1,
28+bfree: 1,
29+bavail: 1,
30+files: 0,
31+ffree: 0,
32+};
33+}
34+1635describe("sqlite WAL maintenance", () => {
1736afterEach(() => {
37+vi.restoreAllMocks();
1838vi.useRealTimers();
1939});
2040@@ -30,6 +50,115 @@ describe("sqlite WAL maintenance", () => {
3050);
3151});
325253+it("uses rollback journaling for databases on NFS-backed volumes", () => {
54+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
55+try {
56+const db = createMockDb();
57+const statfs = vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0x6969));
58+59+const maintenance = configureSqliteWalMaintenance(db, {
60+checkpointIntervalMs: 0,
61+databasePath: path.join(tempDir, "missing", "openclaw.sqlite"),
62+});
63+64+expect(statfs).toHaveBeenCalledWith(tempDir);
65+expect(db["prepare"]).toHaveBeenCalledWith("PRAGMA journal_mode = DELETE;");
66+expect(db["exec"]).not.toHaveBeenCalled();
67+expect(maintenance.checkpoint()).toBe(true);
68+expect(maintenance.close()).toBe(true);
69+expect(db["exec"]).not.toHaveBeenCalled();
70+} finally {
71+fs.rmSync(tempDir, { recursive: true, force: true });
72+}
73+});
74+75+it("refuses NFS-backed databases when SQLite keeps WAL active", () => {
76+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
77+try {
78+const db = createMockDb();
79+vi.mocked(db["prepare"]).mockReturnValue({
80+get: vi.fn(() => ({ journal_mode: "wal" })),
81+} as unknown as ReturnType<DatabaseSync["prepare"]>);
82+vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0x6969));
83+84+expect(() =>
85+configureSqliteWalMaintenance(db, {
86+checkpointIntervalMs: 0,
87+databaseLabel: "test-db",
88+databasePath: path.join(tempDir, "openclaw.sqlite"),
89+}),
90+).toThrow(/test-db .*journal_mode=wal/);
91+} finally {
92+fs.rmSync(tempDir, { recursive: true, force: true });
93+}
94+});
95+96+it("uses mountinfo filesystem names when statfs magic is not enough", () => {
97+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
98+try {
99+const db = createMockDb();
100+vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
101+vi.spyOn(fs, "readFileSync").mockReturnValue(
102+`42 12 0:41 / ${tempDir} rw,relatime - nfs4 server:/share rw\n`,
103+);
104+105+configureSqliteWalMaintenance(db, {
106+checkpointIntervalMs: 0,
107+databasePath: path.join(tempDir, "openclaw.sqlite"),
108+});
109+110+expect(db["prepare"]).toHaveBeenCalledWith("PRAGMA journal_mode = DELETE;");
111+} finally {
112+fs.rmSync(tempDir, { recursive: true, force: true });
113+}
114+});
115+116+it("uses mount command filesystem names on platforms without proc mountinfo", () => {
117+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
118+try {
119+const db = createMockDb();
120+vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
121+vi.spyOn(fs, "readFileSync").mockImplementation(() => {
122+throw new Error("no proc mountinfo");
123+});
124+vi.spyOn(childProcess, "execFileSync").mockReturnValue(
125+Buffer.from(`server:/share on ${tempDir} (nfs, nodev, nosuid)\n`),
126+);
127+128+configureSqliteWalMaintenance(db, {
129+checkpointIntervalMs: 0,
130+databasePath: path.join(tempDir, "openclaw.sqlite"),
131+});
132+133+expect(db["prepare"]).toHaveBeenCalledWith("PRAGMA journal_mode = DELETE;");
134+} finally {
135+fs.rmSync(tempDir, { recursive: true, force: true });
136+}
137+});
138+139+it("parses Linux mount command filesystem names when proc mountinfo is unavailable", () => {
140+const tempDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-sqlite-nfs-"));
141+try {
142+const db = createMockDb();
143+vi.spyOn(fs, "statfsSync").mockReturnValue(statfsFixture(0));
144+vi.spyOn(fs, "readFileSync").mockImplementation(() => {
145+throw new Error("no proc mountinfo");
146+});
147+vi.spyOn(childProcess, "execFileSync").mockReturnValue(
148+Buffer.from(`server:/share on ${tempDir} type nfs4 (rw,relatime)\n`),
149+);
150+151+configureSqliteWalMaintenance(db, {
152+checkpointIntervalMs: 0,
153+databasePath: path.join(tempDir, "openclaw.sqlite"),
154+});
155+156+expect(db["prepare"]).toHaveBeenCalledWith("PRAGMA journal_mode = DELETE;");
157+} finally {
158+fs.rmSync(tempDir, { recursive: true, force: true });
159+}
160+});
161+33162it("runs periodic TRUNCATE checkpoints and stops them on close", () => {
34163vi.useFakeTimers();
35164const db = createMockDb();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。