Files
openclaw/test/vitest-unit-fast-config.test.ts
Josh Lehman 0a8e3604ba refactor: flip sessions and transcripts to sqlite storage (#98236)
* refactor(sessions): migrate runtime storage to sqlite

* test(sessions): fix sqlite CI regressions

* test(sessions): align remaining sqlite fixtures

* fix(codex): require sqlite trajectory recorder

* test(sessions): align orphan recovery sqlite fixture

* test(sessions): align sqlite rebase fixtures

* fix(sessions): finish current-main integration of the sqlite flip

Resolve the whole-store SDK removal across its owner boundary: drop the
loadSessionStore re-export and the registry whole-store wrappers, wire
hasTrackedActiveSessionRun into gateway chat, complete the
preserveLockedHarnessIds cleanup contract, flip the codex thread-history
import to storePath targets, and port remaining main-side tests from
file-store helpers to session accessor reads.

* chore: drop committed pebbles log, revert plugin-inspector bump, refresh generated docs

Remove the 1.8k-line .pebbles/events.jsonl work log from the branch, restore
the plugin-inspector advisory lane to main's pinned 0.3.10 so the supply-chain
bump gets its own review, and regenerate docs_map, the plugin SDK API baseline,
and the export-surface ratchet for the merged tree.

* feat(sessions): keep archived transcripts by default with zstd cold storage

Codex-style retention: deleting or resetting a session archives its
transcript as a zstd-compressed JSONL artifact (plain when the runtime
lacks node:zlib zstd) and keeps it until the disk budget evicts oldest
first. resetArchiveRetention now governs both deleted and reset archives
and defaults to keep; maxDiskBytes defaults to 2gb so retention stays
bounded, with archives evicted before live sessions. The cron reaper
follows the same knob instead of deleting archives on its own timer.

* fix(state): converge agent DB migration lineages and bound database growth

Merge coherence: run both structure-gated legacy memory-schema repairs
(flip-lineage drop, main-lineage identity rebuild) before the flip
migration so pre-flip v1/v2 and pre-merge flip v1/v4 databases all
converge, and hoist foreign_keys=OFF outside the schema transaction
where the pragma was silently ignored and the v1 sessions rebuild
cascade-deleted session_entries.

Growth guards: fresh agent DBs enable auto_vacuum=INCREMENTAL, WAL
maintenance releases freed pages in bounded passes (never a blocking
full VACUUM), and doctor reports state/agent DB bloat from freelist
stats.

* fix(codex): resolve the store path for thread-history import via the SDK

The supervision catalog passed the legacy sessionFile locator to the
storePath-targeted transcript mirror; resolve the agent store path with
the session-store SDK helper instead of a runtime-object seam so test
fakes and headless callers need no extra surface. Drop the obsolete
missing-session-id preprocessing case: sessions rows are NOT NULL on
session_id and upsert repairs id-less patches at write time.

* fix(sessions): fail safe on malformed disk-budget config and doctor stat errors

A malformed explicit maxDiskBytes disables the budget instead of
falling back to the destructive 2gb default the user never chose, and
the doctor bloat check skips databases whose paths stat-fail instead of
aborting doctor.

* fix(sessions): complete sqlite conflict translations

* test(sqlite): align hardening checks with maintenance

* test(sessions): inspect compressed transcript archives

* fix(tests): await session seeds and drop unused helpers flagged by CI lint

The five unawaited writeSessionStoreSeed calls raced their SQLite seeds
against the assertions, failing compact shards; the bloat probe drops a
useless initializer and the merged tests drop now-unused helpers.

* test(sessions): type legacy proof events directly

* test(sessions): align hardening contracts

* perf(sessions): read usage transcript sizes from SQL aggregates

Usage/cost scans walked every session and materialized every transcript
event just to re-stringify it for a byte estimate — the #86718 stall
class reborn on the DB. readTranscriptStatsSync sums stored JSON bytes
in SQLite without loading a single row.

* fix(sessions): re-root foreign-root transcript paths onto the current sessions dir

Restored backups, moved OPENCLAW_STATE_DIR, and rehearsal copies carry
absolute sessionFile paths from the old root; the containment fallback
kept those foreign paths, so migration read (and would archive) files in
the original root and reported local copies missing. Re-root the
canonical agents/<id>/sessions suffix onto the current dir when the file
exists there; genuine cross-root layouts still fall through unchanged.

* test(agents): seed harness admission through sqlite

* fix(sqlite): close agent db on pragma setup failure

* fix(doctor): compact and retrofit incremental auto-vacuum after session import

The migration is the sanctioned offline window: post-import compact
reclaims import churn and applies auto_vacuum=INCREMENTAL to databases
created before the fresh-DB pragma existed, so runtime maintenance can
release pages in bounded passes on every install.

---------

Co-authored-by: Peter Steinberger <steipete@gmail.com>
2026-07-11 14:50:37 -07:00

333 lines
15 KiB
TypeScript

// Vitest unit-fast config tests validate fast unit test project setup.
import path from "node:path";
import { beforeAll, describe, expect, it } from "vitest";
import { spawnNodeEvalSync } from "../src/test-utils/node-process.js";
import { createCommandsLightVitestConfig } from "./vitest/vitest.commands-light.config.ts";
import { createPluginSdkLightVitestConfig } from "./vitest/vitest.plugin-sdk-light.config.ts";
import { createUnitFastFakeTimersVitestConfig } from "./vitest/vitest.unit-fast-fake-timers.config.ts";
import {
classifyUnitFastTestFileContent,
collectBroadUnitFastTestCandidates,
collectUnitFastTestCandidates,
collectUnitFastTestFileAnalysis,
forcedUnitFastTestFiles,
getUnitFastTestFiles,
getUnitFastTestFilesForIncludePatterns,
getUnitFastTimerTestFiles,
isUnitFastTestFile,
isUnitFastTimerTestFile,
resolveUnitFastTestIncludePattern,
resolveUnitFastTimerTestIncludePattern,
} from "./vitest/vitest.unit-fast-paths.mjs";
import { createUnitFastVitestConfig } from "./vitest/vitest.unit-fast.config.ts";
const ENV_ISOLATION_SETUP_PATH = /[\\/]test[\\/]setup\.env\.ts$/u;
function requireTestConfig<T extends { test?: unknown }>(config: T): NonNullable<T["test"]> {
if (!config.test) {
throw new Error("expected unit-fast vitest test config");
}
return config.test as NonNullable<T["test"]>;
}
function countMatching<T>(items: readonly T[], predicate: (item: T) => boolean): number {
let count = 0;
for (const item of items) {
if (predicate(item)) {
count += 1;
}
}
return count;
}
type UnitFastAnalysisEntry = ReturnType<typeof collectUnitFastTestFileAnalysis>[number];
function collectUnroutedForcedFiles(
analysis: readonly UnitFastAnalysisEntry[],
forcedFiles: ReadonlySet<string>,
): Array<{ file: string; forced: boolean; unitFast: boolean }> {
const unrouted: Array<{ file: string; forced: boolean; unitFast: boolean }> = [];
for (const entry of analysis) {
if (!forcedFiles.has(entry.file)) {
continue;
}
if (!entry.forced || !entry.unitFast) {
unrouted.push({ file: entry.file, forced: entry.forced, unitFast: entry.unitFast });
}
}
return unrouted;
}
describe("unit-fast vitest lane", () => {
let configProbeResult: ReturnType<typeof spawnNodeEvalSync>;
let unitFastConfig: ReturnType<typeof createUnitFastVitestConfig>;
let unitFastTestFiles: ReturnType<typeof getUnitFastTestFiles>;
let unitFastTimerTestFiles: ReturnType<typeof getUnitFastTimerTestFiles>;
let unitFastAnalysis: ReturnType<typeof collectUnitFastTestFileAnalysis>;
let broadCandidates: ReturnType<typeof collectBroadUnitFastTestCandidates>;
let broadAnalysis: ReturnType<typeof collectUnitFastTestFileAnalysis>;
let currentCandidates: ReturnType<typeof collectUnitFastTestCandidates>;
beforeAll(() => {
const script = `
import childProcess from "node:child_process";
import fs from "node:fs";
import { syncBuiltinESMExports } from "node:module";
let gitLsFilesCalls = 0;
const originalSpawnSync = childProcess.spawnSync;
childProcess.spawnSync = function patchedSpawnSync(...args) {
const [command, commandArgs] = args;
if (command === "git" && commandArgs?.[0] === "ls-files") {
gitLsFilesCalls += 1;
const stdout = [
"src/agents/agent-tools.deferred-followup-guidance.test.ts",
"src/hooks/frontmatter.test.ts",
].join("\\n") + "\\n";
return {
pid: 0,
output: [null, stdout, ""],
stdout,
stderr: "",
status: 0,
signal: null,
};
}
return originalSpawnSync.apply(this, args);
};
syncBuiltinESMExports();
let readdirSyncCalls = 0;
let hookFileReads = 0;
let outsideFileReads = 0;
const originalReaddirSync = fs.readdirSync;
const originalReadFileSync = fs.readFileSync;
fs.readdirSync = function patchedReaddirSync(...args) {
readdirSyncCalls += 1;
return originalReaddirSync.apply(this, args);
};
fs.readFileSync = function patchedReadFileSync(...args) {
const file = String(args[0]).replaceAll("\\\\", "/");
if (file.endsWith("/src/hooks/frontmatter.test.ts")) {
hookFileReads += 1;
} else if (file.endsWith("/src/agents/agent-tools.deferred-followup-guidance.test.ts")) {
outsideFileReads += 1;
}
return originalReadFileSync.apply(this, args);
};
await import("./test/vitest/vitest.hooks.config.ts?scope-probe=" + Date.now());
const scopedHookFileReads = hookFileReads;
const scopedOutsideFileReads = outsideFileReads;
await import("./test/vitest/vitest.unit-fast.config.ts?io-probe=" + Date.now());
console.log(
"UNIT_FAST_IO_PROBE",
gitLsFilesCalls,
readdirSyncCalls,
scopedHookFileReads,
scopedOutsideFileReads,
);
`;
configProbeResult = spawnNodeEvalSync(script, {
env: {
...process.env,
FORCE_COLOR: "0",
NO_COLOR: "1",
OPENCLAW_VITEST_INCLUDE_FILE: undefined,
},
evalFlag: "-e",
imports: ["tsx"],
});
unitFastConfig = createUnitFastVitestConfig({});
unitFastTestFiles = getUnitFastTestFiles();
unitFastTimerTestFiles = getUnitFastTimerTestFiles();
unitFastAnalysis = collectUnitFastTestFileAnalysis();
currentCandidates = collectUnitFastTestCandidates();
broadCandidates = collectBroadUnitFastTestCandidates();
broadAnalysis = collectUnitFastTestFileAnalysis(process.cwd(), { scope: "broad" });
});
it("loads the config without recursively walking repo roots", () => {
expect(configProbeResult.status, configProbeResult.stderr).toBe(0);
const probeMatch = configProbeResult.stdout.match(
/UNIT_FAST_IO_PROBE (\d+) (\d+) (\d+) (\d+)/u,
);
expect(probeMatch, configProbeResult.stdout).not.toBeNull();
expect(Number(probeMatch?.[1])).toBe(1);
expect(Number(probeMatch?.[2])).toBeLessThan(20);
expect(Number(probeMatch?.[3])).toBe(1);
expect(Number(probeMatch?.[4])).toBe(0);
});
it("runs cache-friendly tests without the reset-heavy runner or runtime setup", () => {
const testConfig = requireTestConfig(unitFastConfig);
expect(testConfig.isolate).toBe(false);
expect(testConfig.runner).toBeUndefined();
// Env isolation only: the fast shards install the isolated test home but
// must not pull in the shared setup's module mocks.
expect(testConfig.setupFiles).toStrictEqual([expect.stringMatching(ENV_ISOLATION_SETUP_PATH)]);
expect(testConfig.include).toContain(
"src/agents/agent-tools.deferred-followup-guidance.test.ts",
);
expect(testConfig.include).toContain("src/acp/control-plane/runtime-cache.test.ts");
expect(testConfig.include).toContain("src/acp/runtime/registry.test.ts");
expect(testConfig.include).toContain("src/commands/status-overview-values.test.ts");
expect(testConfig.include).toContain("src/entry.version-fast-path.test.ts");
expect(testConfig.include).toContain("src/flows/doctor-startup-channel-maintenance.test.ts");
expect(testConfig.include).toContain("src/crestodian/rescue-policy.test.ts");
expect(testConfig.include).toContain("src/crestodian/assistant.configured.test.ts");
expect(testConfig.include).toContain("src/flows/search-setup.test.ts");
expect(testConfig.include).toContain("src/plugins/config-policy.test.ts");
expect(testConfig.include).toContain("src/proxy-capture/proxy-server.test.ts");
expect(testConfig.include).toContain("src/talk/agent-consult-tool.test.ts");
expect(testConfig.include).toContain("src/sessions/session-lifecycle-events.test.ts");
expect(testConfig.include).toContain("src/sessions/transcript-events.test.ts");
expect(testConfig.include).toContain("src/security/audit-channel-source-config-slack.test.ts");
expect(testConfig.include).toContain("src/security/audit-config-symlink.test.ts");
expect(testConfig.include).toContain("src/security/audit-exec-sandbox-host.test.ts");
expect(testConfig.include).toContain("src/security/audit-gateway.test.ts");
expect(testConfig.include).toContain("src/security/audit-gateway-auth-selection.test.ts");
expect(testConfig.include).toContain("src/security/audit-gateway-http-auth.test.ts");
expect(testConfig.include).toContain("src/security/audit-gateway-tools-http.test.ts");
expect(testConfig.include).toContain("src/security/audit-plugin-readonly-scope.test.ts");
expect(testConfig.include).toContain("src/security/audit-loopback-logging.test.ts");
expect(testConfig.include).toContain("src/video-generation/provider-registry.test.ts");
expect(testConfig.include).toContain("src/plugin-sdk/provider-entry.test.ts");
expect(testConfig.include).toContain("src/security/dangerous-config-flags.test.ts");
expect(testConfig.include).toContain("src/security/context-visibility.test.ts");
expect(testConfig.include).toContain("src/security/safe-regex.test.ts");
});
it("does not treat moved config paths as CLI include filters", () => {
const config = createUnitFastVitestConfig(
{},
{
argv: ["node", "vitest", "run", "--config", "test/vitest/vitest.unit-fast.config.ts"],
},
);
const testConfig = requireTestConfig(config);
expect(testConfig.include).toContain("src/plugin-sdk/provider-entry.test.ts");
expect(testConfig.include).toContain("src/commands/status-overview-values.test.ts");
});
it("keeps obvious stateful files out of the unit-fast lane", () => {
expect(isUnitFastTestFile("src/plugin-sdk/temp-path.test.ts")).toBe(false);
expect(isUnitFastTestFile("src/agents/sandbox.resolveSandboxContext.test.ts")).toBe(false);
expect(isUnitFastTestFile("src/acp/runtime/session-meta.test.ts")).toBe(false);
expect(isUnitFastTestFile("src/crestodian/assistant.test.ts")).toBe(false);
expect(isUnitFastTestFile("src/flows/channel-setup.test.ts")).toBe(false);
expect(isUnitFastTestFile("src/flows/doctor-health-contributions.test.ts")).toBe(false);
expect(isUnitFastTestFile("src/plugins/install.npm-spec.test.ts")).toBe(false);
expect(isUnitFastTestFile("src/secrets/runtime.test.ts")).toBe(false);
expect(resolveUnitFastTestIncludePattern("src/plugin-sdk/temp-path.ts")).toBeNull();
expect(classifyUnitFastTestFileContent("vi.resetModules(); await import('./x.js')")).toEqual([
"module-mocking",
"vitest-mock-api",
"dynamic-import",
]);
});
it("routes unit-fast source files to their unit-fast sibling tests", () => {
expect(resolveUnitFastTestIncludePattern("src/plugin-sdk/provider-entry.ts")).toBe(
"src/plugin-sdk/provider-entry.test.ts",
);
expect(resolveUnitFastTestIncludePattern("src/commands/status-overview-values.ts")).toBe(
"src/commands/status-overview-values.test.ts",
);
});
it("routes audited stateful-looking tests through the fast lane", () => {
const forcedFileSet = new Set(forcedUnitFastTestFiles);
const forcedAnalysisCount = countMatching(unitFastAnalysis, (entry) =>
forcedFileSet.has(entry.file),
);
expect(forcedAnalysisCount).toBe(forcedUnitFastTestFiles.length);
for (const file of forcedUnitFastTestFiles) {
expect(unitFastTestFiles).toContain(file);
expect(isUnitFastTestFile(file)).toBe(true);
}
const unroutedForcedFiles = collectUnroutedForcedFiles(unitFastAnalysis, forcedFileSet);
expect(unroutedForcedFiles).toStrictEqual([]);
});
it("routes fake-timer unit-fast tests through the serial fake-timer lane", () => {
const fakeTimerFiles = unitFastAnalysis
.filter((entry) => entry.unitFast && entry.reasons.includes("fake-timers"))
.map((entry) => entry.file);
expect(unitFastTimerTestFiles.length).toBeGreaterThan(0);
expect(unitFastTimerTestFiles).toEqual(fakeTimerFiles);
for (const file of unitFastTimerTestFiles) {
expect(isUnitFastTimerTestFile(file)).toBe(true);
expect(resolveUnitFastTestIncludePattern(file)).toBeNull();
expect(resolveUnitFastTimerTestIncludePattern(file)).toBe(file);
}
const fastConfig = requireTestConfig(unitFastConfig);
const timerConfig = requireTestConfig(createUnitFastFakeTimersVitestConfig({}));
expect(fastConfig.include).not.toEqual(expect.arrayContaining(unitFastTimerTestFiles));
expect(timerConfig.include).toEqual(unitFastTimerTestFiles);
expect(timerConfig.fileParallelism).toBe(false);
expect(timerConfig.maxWorkers).toBe(1);
expect(timerConfig.setupFiles).toStrictEqual([expect.stringMatching(ENV_ISOLATION_SETUP_PATH)]);
});
it("keeps broad audit candidates separate from automatically routed unit-fast tests", () => {
expect(currentCandidates.length).toBeGreaterThanOrEqual(unitFastTestFiles.length);
expect(broadCandidates.length).toBeGreaterThan(currentCandidates.length);
expect(countMatching(broadAnalysis, (entry) => entry.unitFast)).toBeGreaterThan(
unitFastTestFiles.length,
);
});
it("keeps scoped unit-fast exclusions equivalent to the full inventory", () => {
const cases = [
{ dir: "src/hooks", patterns: ["src/hooks/**/*.test.ts"] },
{ dir: "src", patterns: ["src/agents/*/**/*.test.ts"] },
{ dir: "src/acp", patterns: ["src/acp/client.test.ts"] },
{ dir: "extensions", patterns: ["extensions/**/*.test.ts"] },
{ dir: undefined, patterns: ["test/**/*.test.ts"] },
{ dir: undefined, patterns: ["src/{hooks,infra}/**/*.test.ts"] },
{ dir: "src", patterns: [] },
];
for (const { dir, patterns } of cases) {
const prefix = dir ? `${dir}/` : "";
const expected = unitFastTestFiles.filter((file) => {
if (prefix && !file.startsWith(prefix)) {
return false;
}
return patterns.some((pattern) => path.matchesGlob(file, pattern));
});
expect(getUnitFastTestFilesForIncludePatterns(patterns, { dir })).toEqual(expected);
}
const extensionUnitFastFiles = getUnitFastTestFilesForIncludePatterns(
["extensions/**/*.test.ts"],
{ dir: "extensions" },
);
expect(getUnitFastTestFilesForIncludePatterns(["**/*.test.ts"], { dir: "extensions" })).toEqual(
extensionUnitFastFiles,
);
expect(extensionUnitFastFiles).toEqual(
expect.arrayContaining([
"extensions/canvas/src/host/server.test.ts",
"extensions/canvas/src/host/server.state-dir.test.ts",
]),
);
expect(getUnitFastTestFilesForIncludePatterns(["!src/**/*.test.ts"])).toEqual(
unitFastTestFiles,
);
});
it("excludes unit-fast files from the older light lanes so full runs do not duplicate them", () => {
const pluginSdkLight = createPluginSdkLightVitestConfig({});
const commandsLight = createCommandsLightVitestConfig({});
expect(unitFastTestFiles).toContain("src/plugin-sdk/provider-entry.test.ts");
expect(requireTestConfig(pluginSdkLight).exclude).toContain(
"plugin-sdk/provider-entry.test.ts",
);
expect(requireTestConfig(commandsLight).exclude).toContain("status-overview-values.test.ts");
});
});