mirror of
https://github.com/openclaw/openclaw.git
synced 2026-06-28 15:13:36 +00:00
Detects NFS-backed SQLite database paths in the shared WAL helper and uses rollback journaling for those paths while preserving WAL/checkpoint maintenance on local filesystems. The NFS path now verifies SQLite's effective journal mode before disabling WAL maintenance, and core/memory/proxy-capture callers pass database path context into the centralized helper. Fixes #90491. Proof: local focused Vitest/format/lint; autoreview clean after fixing the journal-mode verification finding; Crabbox AWS focused test run `run_2ea7014350da`; Crabbox AWS changed gate `run_c828bbfe7d23`; exact-head GitHub CI green on `59674305ecd863d4815eec6098ccd3daab79ca4f`.
185 lines
5.3 KiB
TypeScript
185 lines
5.3 KiB
TypeScript
// Proxy capture SQLite store tests cover persisted capture reads and writes.
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
import {
|
|
acquireDebugProxyCaptureStore,
|
|
closeDebugProxyCaptureStore,
|
|
DebugProxyCaptureStore,
|
|
getDebugProxyCaptureStore,
|
|
persistEventPayload,
|
|
} from "./store.sqlite.js";
|
|
|
|
const cleanupDirs: string[] = [];
|
|
|
|
afterEach(() => {
|
|
closeDebugProxyCaptureStore();
|
|
vi.restoreAllMocks();
|
|
while (cleanupDirs.length > 0) {
|
|
const dir = cleanupDirs.pop();
|
|
if (dir) {
|
|
fs.rmSync(dir, { recursive: true, force: true });
|
|
}
|
|
}
|
|
});
|
|
|
|
function makeStore() {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-proxy-capture-"));
|
|
cleanupDirs.push(root);
|
|
return new DebugProxyCaptureStore(path.join(root, "capture.sqlite"), path.join(root, "blobs"));
|
|
}
|
|
|
|
describe("DebugProxyCaptureStore", () => {
|
|
it("keeps the cached store open until the last lease releases", () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-proxy-capture-lease-"));
|
|
cleanupDirs.push(root);
|
|
const dbPath = path.join(root, "capture.sqlite");
|
|
const blobDir = path.join(root, "blobs");
|
|
|
|
const first = acquireDebugProxyCaptureStore(dbPath, blobDir);
|
|
const second = acquireDebugProxyCaptureStore(dbPath, blobDir);
|
|
|
|
expect(second.store).toBe(first.store);
|
|
first.release();
|
|
expect(first.store.isClosed).toBe(false);
|
|
|
|
second.release();
|
|
expect(first.store.isClosed).toBe(true);
|
|
|
|
const reopened = getDebugProxyCaptureStore(dbPath, blobDir);
|
|
expect(Object.is(reopened, first.store)).toBe(false);
|
|
expect(reopened.isClosed).toBe(false);
|
|
});
|
|
|
|
it("uses rollback journaling for captures on NFS-backed volumes", () => {
|
|
const root = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-proxy-capture-nfs-"));
|
|
cleanupDirs.push(root);
|
|
vi.spyOn(fs, "statfsSync").mockReturnValue({
|
|
type: 0x6969,
|
|
bsize: 1024,
|
|
blocks: 1,
|
|
bfree: 1,
|
|
bavail: 1,
|
|
files: 0,
|
|
ffree: 0,
|
|
});
|
|
|
|
const store = new DebugProxyCaptureStore(
|
|
path.join(root, "capture.sqlite"),
|
|
path.join(root, "blobs"),
|
|
);
|
|
try {
|
|
expect(store.db.prepare("PRAGMA journal_mode").get()).toMatchObject({
|
|
journal_mode: "delete",
|
|
});
|
|
} finally {
|
|
store.close();
|
|
}
|
|
});
|
|
|
|
it("ignores duplicate close calls", () => {
|
|
const store = makeStore();
|
|
|
|
store.close();
|
|
store.close();
|
|
expect(store.isClosed).toBe(true);
|
|
});
|
|
|
|
it("stores sessions, blobs, and duplicate-send query results", () => {
|
|
const store = makeStore();
|
|
store.upsertSession({
|
|
id: "session-1",
|
|
startedAt: Date.now(),
|
|
mode: "proxy-run",
|
|
sourceScope: "openclaw",
|
|
sourceProcess: "openclaw",
|
|
dbPath: store.dbPath,
|
|
blobDir: store.blobDir,
|
|
});
|
|
const firstPayload = persistEventPayload(store, {
|
|
data: '{"ok":true}',
|
|
contentType: "application/json",
|
|
});
|
|
store.recordEvent({
|
|
sessionId: "session-1",
|
|
ts: 1,
|
|
sourceScope: "openclaw",
|
|
sourceProcess: "openclaw",
|
|
protocol: "https",
|
|
direction: "outbound",
|
|
kind: "request",
|
|
flowId: "flow-1",
|
|
method: "POST",
|
|
host: "api.example.com",
|
|
path: "/v1/send",
|
|
...firstPayload,
|
|
});
|
|
store.recordEvent({
|
|
sessionId: "session-1",
|
|
ts: 2,
|
|
sourceScope: "openclaw",
|
|
sourceProcess: "openclaw",
|
|
protocol: "https",
|
|
direction: "outbound",
|
|
kind: "request",
|
|
flowId: "flow-2",
|
|
method: "POST",
|
|
host: "api.example.com",
|
|
path: "/v1/send",
|
|
...firstPayload,
|
|
});
|
|
|
|
expect(store.listSessions(10)).toHaveLength(1);
|
|
const duplicateRows = store.queryPreset("double-sends", "session-1");
|
|
expect(duplicateRows).toHaveLength(1);
|
|
expect(duplicateRows[0]?.host).toBe("api.example.com");
|
|
expect(duplicateRows[0]?.path).toBe("/v1/send");
|
|
expect(duplicateRows[0]?.method).toBe("POST");
|
|
expect(duplicateRows[0]?.duplicateCount).toBe(2);
|
|
expect(store.readBlob(firstPayload.dataBlobId ?? "")).toContain('"ok":true');
|
|
});
|
|
|
|
it("keeps shared blobs when deleting one of multiple referencing sessions", () => {
|
|
const store = makeStore();
|
|
const sharedPayload = persistEventPayload(store, {
|
|
data: '{"shared":true}',
|
|
contentType: "application/json",
|
|
});
|
|
|
|
for (const sessionId of ["session-a", "session-b"]) {
|
|
store.upsertSession({
|
|
id: sessionId,
|
|
startedAt: Date.now(),
|
|
mode: "proxy-run",
|
|
sourceScope: "openclaw",
|
|
sourceProcess: "openclaw",
|
|
dbPath: store.dbPath,
|
|
blobDir: store.blobDir,
|
|
});
|
|
store.recordEvent({
|
|
sessionId,
|
|
ts: Date.now(),
|
|
sourceScope: "openclaw",
|
|
sourceProcess: "openclaw",
|
|
protocol: "https",
|
|
direction: "outbound",
|
|
kind: "request",
|
|
flowId: `flow-${sessionId}`,
|
|
method: "POST",
|
|
host: "api.example.com",
|
|
path: "/v1/shared",
|
|
...sharedPayload,
|
|
});
|
|
}
|
|
|
|
const result = store.deleteSessions(["session-a"]);
|
|
|
|
expect(result.sessions).toBe(1);
|
|
expect(result.events).toBe(1);
|
|
expect(result.blobs).toBe(0);
|
|
expect(store.readBlob(sharedPayload.dataBlobId ?? "")).toContain('"shared":true');
|
|
expect(store.listSessions(10).map((session) => session.id)).toEqual(["session-b"]);
|
|
});
|
|
});
|