From bef71f76dadaf19e8ad17ef50e33ef3d794dc90c Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sat, 9 May 2026 02:32:53 +0100 Subject: [PATCH] test: cover sqlite persisted enum parsing --- .../virtual-agent-fs.sqlite.test.ts | 19 +++++++++- src/proxy-capture/store.sqlite.test.ts | 35 +++++++++++++++++++ src/tasks/task-flow-registry.store.test.ts | 20 ++++++++++- src/tasks/task-registry.store.test.ts | 27 ++++++++++++++ 4 files changed, 99 insertions(+), 2 deletions(-) diff --git a/src/agents/filesystem/virtual-agent-fs.sqlite.test.ts b/src/agents/filesystem/virtual-agent-fs.sqlite.test.ts index 74baea3b1db..168deaf0413 100644 --- a/src/agents/filesystem/virtual-agent-fs.sqlite.test.ts +++ b/src/agents/filesystem/virtual-agent-fs.sqlite.test.ts @@ -1,9 +1,11 @@ import fs from "node:fs"; import os from "node:os"; import path from "node:path"; -import { afterEach, describe, expect, it } from "vitest"; +import { afterEach, describe, expect, expectTypeOf, it } from "vitest"; import { closeOpenClawAgentDatabasesForTest } from "../../state/openclaw-agent-db.js"; import { closeOpenClawStateDatabaseForTest } from "../../state/openclaw-state-db.js"; +import type { VirtualAgentFsEntry } from "./agent-filesystem.js"; +import { parseVirtualAgentFsEntryKind } from "./agent-filesystem.js"; import { createSqliteVirtualAgentFs } from "./virtual-agent-fs.sqlite.js"; function createTempStateDir(): string { @@ -16,6 +18,21 @@ afterEach(() => { }); describe("SqliteVirtualAgentFs", () => { + it("types public results and rejects invalid persisted entry kinds", () => { + const scratch = createSqliteVirtualAgentFs({ + agentId: "main", + namespace: "scratch", + env: { OPENCLAW_STATE_DIR: createTempStateDir() }, + }); + + expectTypeOf(scratch.stat("/tmp")).toEqualTypeOf(); + expect(parseVirtualAgentFsEntryKind("file")).toBe("file"); + expect(parseVirtualAgentFsEntryKind("directory")).toBe("directory"); + expect(() => parseVirtualAgentFsEntryKind("socket")).toThrow( + "Invalid persisted VFS entry kind", + ); + }); + it("stores scratch files by agent and namespace", () => { const env = { OPENCLAW_STATE_DIR: createTempStateDir() }; const mainScratch = createSqliteVirtualAgentFs({ diff --git a/src/proxy-capture/store.sqlite.test.ts b/src/proxy-capture/store.sqlite.test.ts index 96d997657ac..96763d500a7 100644 --- a/src/proxy-capture/store.sqlite.test.ts +++ b/src/proxy-capture/store.sqlite.test.ts @@ -196,4 +196,39 @@ describe("DebugProxyCaptureStore", () => { expect(store.readBlob(sharedPayload.dataBlobId ?? "")).toContain('"shared":true'); expect(store.listSessions(10).map((session) => session.id)).toEqual(["session-b"]); }); + + it("purges sessions, events, and blobs in one store mutation", () => { + const store = makeStore(); + store.upsertSession({ + id: "session-purge", + startedAt: Date.now(), + mode: "proxy-run", + sourceScope: "openclaw", + sourceProcess: "openclaw", + dbPath: store.dbPath, + blobDir: store.blobDir, + }); + const payload = persistEventPayload(store, { + data: "purge me", + contentType: "text/plain", + }); + store.recordEvent({ + sessionId: "session-purge", + ts: Date.now(), + sourceScope: "openclaw", + sourceProcess: "openclaw", + protocol: "https", + direction: "outbound", + kind: "request", + flowId: "flow-purge", + method: "POST", + host: "api.example.com", + path: "/v1/purge", + ...payload, + }); + + expect(store.purgeAll()).toEqual({ sessions: 1, events: 1, blobs: 1 }); + expect(store.listSessions(10)).toEqual([]); + expect(store.readBlob(payload.dataBlobId ?? "")).toBeNull(); + }); }); diff --git a/src/tasks/task-flow-registry.store.test.ts b/src/tasks/task-flow-registry.store.test.ts index d7fcd5d4715..5200dd9ee1d 100644 --- a/src/tasks/task-flow-registry.store.test.ts +++ b/src/tasks/task-flow-registry.store.test.ts @@ -14,7 +14,12 @@ import { resolveTaskFlowRegistrySqlitePath, } from "./task-flow-registry.paths.js"; import { configureTaskFlowRegistryRuntime } from "./task-flow-registry.store.js"; -import type { TaskFlowRecord } from "./task-flow-registry.types.js"; +import { + parseOptionalTaskFlowSyncMode, + parseTaskFlowStatus, + type TaskFlowRecord, +} from "./task-flow-registry.types.js"; +import { parseTaskNotifyPolicy } from "./task-registry.types.js"; function createStoredFlow(): TaskFlowRecord { return { @@ -125,6 +130,19 @@ describe("task-flow-registry store runtime", () => { expect(restoredFlow.goal).toBe("Restored flow"); }); + it("rejects invalid persisted flow enum values", () => { + expect(parseOptionalTaskFlowSyncMode("managed")).toBe("managed"); + expect(parseOptionalTaskFlowSyncMode(null)).toBeUndefined(); + expect(parseTaskFlowStatus("waiting")).toBe("waiting"); + expect(parseTaskNotifyPolicy("state_changes")).toBe("state_changes"); + + expect(() => parseOptionalTaskFlowSyncMode("legacy")).toThrow( + "Invalid persisted task flow sync mode", + ); + expect(() => parseTaskFlowStatus("done")).toThrow("Invalid persisted task flow status"); + expect(() => parseTaskNotifyPolicy("verbose")).toThrow("Invalid persisted task notify policy"); + }); + it("restores persisted wait-state, revision, and cancel intent from sqlite", async () => { await withFlowRegistryTempDir(async (root) => { process.env.OPENCLAW_STATE_DIR = root; diff --git a/src/tasks/task-registry.store.test.ts b/src/tasks/task-registry.store.test.ts index 261c16681f1..b775058f5a3 100644 --- a/src/tasks/task-registry.store.test.ts +++ b/src/tasks/task-registry.store.test.ts @@ -16,6 +16,14 @@ import { type TaskRegistryObserverEvent, } from "./task-registry.store.js"; import type { TaskRecord } from "./task-registry.types.js"; +import { + parseOptionalTaskTerminalOutcome, + parseTaskDeliveryStatus, + parseTaskNotifyPolicy, + parseTaskRuntime, + parseTaskScopeKind, + parseTaskStatus, +} from "./task-registry.types.js"; const ORIGINAL_STATE_DIR = process.env.OPENCLAW_STATE_DIR; @@ -96,6 +104,25 @@ describe("task-registry store runtime", () => { expect(latestSnapshot.tasks.get("task-restored")?.task).toBe("Restored task"); }); + it("rejects invalid persisted task enum values", () => { + expect(parseTaskRuntime("cron")).toBe("cron"); + expect(parseTaskScopeKind("system")).toBe("system"); + expect(parseTaskStatus("running")).toBe("running"); + expect(parseTaskDeliveryStatus("pending")).toBe("pending"); + expect(parseTaskNotifyPolicy("done_only")).toBe("done_only"); + expect(parseOptionalTaskTerminalOutcome("blocked")).toBe("blocked"); + expect(parseOptionalTaskTerminalOutcome(null)).toBeUndefined(); + + expect(() => parseTaskRuntime("timer")).toThrow("Invalid persisted task runtime"); + expect(() => parseTaskScopeKind("workspace")).toThrow("Invalid persisted task scope kind"); + expect(() => parseTaskStatus("done")).toThrow("Invalid persisted task status"); + expect(() => parseTaskDeliveryStatus("ok")).toThrow("Invalid persisted task delivery status"); + expect(() => parseTaskNotifyPolicy("verbose")).toThrow("Invalid persisted task notify policy"); + expect(() => parseOptionalTaskTerminalOutcome("failed")).toThrow( + "Invalid persisted task terminal outcome", + ); + }); + it("emits incremental observer events for restore, mutation, and delete", () => { const events: TaskRegistryObserverEvent[] = []; configureTaskRegistryRuntime({