Revert "refactor: move runtime state to SQLite"

This reverts commit f91de52f0d.
This commit is contained in:
Peter Steinberger
2026-05-13 13:33:38 +01:00
parent 3de5979bdc
commit 694ca50e97
3085 changed files with 106484 additions and 115317 deletions

View File

@@ -77,6 +77,21 @@
}
}
}
},
{
"id": "legacy_payload_fallback",
"defaultProvider": "elevenlabs",
"payloadValid": true,
"expectedSelection": {
"provider": "elevenlabs",
"normalizedPayload": true,
"voiceId": "voice-legacy",
"apiKey": "xxxxx"
},
"talk": {
"voiceId": "voice-legacy",
"apiKey": "xxxxx"
}
}
],
"timeoutCases": [

View File

@@ -30,6 +30,7 @@ export const CODEX_MODEL_PROMPT_FIXTURE_DIR =
const WORKSPACE_DIR = "/tmp/openclaw-happy-path/workspace";
const AGENT_DIR = "/tmp/openclaw-happy-path/agent";
const SESSION_FILE = "/tmp/openclaw-happy-path/session.jsonl";
const MODEL_ID = "gpt-5.5";
const CODEX_PROMPT_PERSONALITY = "pragmatic";
const CODEX_MODEL_PROMPT_FIXTURE_PATH = path.join(
@@ -280,6 +281,7 @@ function createAttempt(params: {
agentId: "main",
agentDir: AGENT_DIR,
workspaceDir: WORKSPACE_DIR,
sessionFile: SESSION_FILE,
sessionKey: params.sessionKey,
sessionId: `session-${params.scenario.id}`,
runId: `run-${params.scenario.id}`,

View File

@@ -1,7 +1,6 @@
import fs from "node:fs/promises";
import path from "node:path";
import { vi } from "vitest";
import { loadPersistedAuthProfileStore } from "../../src/agents/auth-profiles/persisted.js";
import type { RuntimeEnv } from "../../src/runtime.js";
import { makeTempWorkspace } from "../../src/test-helpers/workspace.js";
import { captureEnv } from "../../src/test-utils/env.js";
@@ -83,10 +82,11 @@ export function requireOpenClawAgentDir(): string {
return agentDir;
}
export async function readAuthProfilesForAgent<T>(agentDir: string): Promise<T> {
const store = loadPersistedAuthProfileStore(agentDir);
if (!store) {
throw new Error(`Expected SQLite auth profile store for ${agentDir}`);
}
return store as T;
function authProfilePathForAgent(agentDir: string): string {
return path.join(agentDir, "auth-profiles.json");
}
export async function readAuthProfilesForAgent<T>(agentDir: string): Promise<T> {
const raw = await fs.readFile(authProfilePathForAgent(agentDir), "utf8");
return JSON.parse(raw) as T;
}

View File

@@ -253,7 +253,7 @@ afterAll(async () => {
export async function withTempHome<T>(fn: (home: string) => Promise<T>): Promise<T> {
const home = join(suiteTempHomeRoot, `case-${++suiteTempHomeId}`);
const snapshot = snapshotTempHomeEnv();
await fs.mkdir(join(home, ".openclaw", "agents", "main", "agent"), { recursive: true });
await fs.mkdir(join(home, ".openclaw", "agents", "main", "sessions"), { recursive: true });
setTempHomeEnv(home);
try {
@@ -300,6 +300,7 @@ export function makeCfg(home: string): OpenClawConfig {
debounceMs: 0,
},
},
session: { store: join(home, "sessions.json") },
} as OpenClawConfig);
}
@@ -318,6 +319,14 @@ export function installTriggerHandlingReplyHarness(
installTriggerHandlingE2eTestHooks();
}
export function requireSessionStorePath(cfg: { session?: { store?: string } }): string {
const storePath = cfg.session?.store;
if (!storePath) {
throw new Error("expected session store path");
}
return storePath;
}
export async function expectInlineCommandHandledAndStripped(params: {
home: string;
getReplyFromConfig: typeof import("../../../src/auto-reply/reply.js").getReplyFromConfig;

View File

@@ -4,15 +4,14 @@ import os from "node:os";
import path from "node:path";
import { afterAll, afterEach, beforeAll, beforeEach, vi } from "vitest";
import { clearAllBootstrapSnapshots } from "../../../src/agents/bootstrap-cache.js";
import { clearSessionStoreCacheForTest } from "../../../src/config/sessions/store.js";
import { createCronServiceState, type CronServiceDeps } from "../../../src/cron/service/state.js";
import { saveCronStore } from "../../../src/cron/store.js";
import type { CronJob, CronJobState } from "../../../src/cron/types.js";
import { resetAgentRunContextForTest } from "../../../src/infra/agent-events.js";
import {
resetCommandQueueStateForTest,
waitForActiveTasks,
} from "../../../src/process/command-queue.js";
import { closeOpenClawStateDatabaseForTest } from "../../../src/state/openclaw-state-db.js";
import { useFrozenTime, useRealTime } from "../../../src/test-utils/frozen-time.js";
const TOP_OF_HOUR_STAGGER_MS = 5 * 60 * 1_000;
@@ -28,12 +27,9 @@ export const noopLogger = {
export function setupCronRegressionFixtures(options?: { prefix?: string; baseTimeIso?: string }) {
let fixtureRoot = "";
let fixtureCount = 0;
let originalOpenClawStateDir: string | undefined;
beforeAll(async () => {
fixtureRoot = await fs.mkdtemp(path.join(os.tmpdir(), options?.prefix ?? "cron-issues-"));
originalOpenClawStateDir = process.env.OPENCLAW_STATE_DIR;
process.env.OPENCLAW_STATE_DIR = path.join(fixtureRoot, "state");
});
beforeEach(() => {
@@ -47,26 +43,21 @@ export function setupCronRegressionFixtures(options?: { prefix?: string; baseTim
useRealTime();
await waitForActiveTasks(250);
resetCommandQueueStateForTest();
clearSessionStoreCacheForTest();
resetAgentRunContextForTest();
clearAllBootstrapSnapshots();
});
afterAll(async () => {
closeOpenClawStateDatabaseForTest();
if (originalOpenClawStateDir === undefined) {
delete process.env.OPENCLAW_STATE_DIR;
} else {
process.env.OPENCLAW_STATE_DIR = originalOpenClawStateDir;
}
useRealTime();
await waitForActiveTasks(250);
await fs.rm(fixtureRoot, { recursive: true, force: true });
});
return {
makeStoreKey() {
makeStorePath() {
return {
storeKey: `case-${fixtureCount++}`,
storePath: path.join(fixtureRoot, `case-${fixtureCount++}.jobs.json`),
};
},
};
@@ -83,14 +74,14 @@ export function createDeferred<T>() {
}
export function createRunningCronServiceState(params: {
storeKey?: string;
storePath: string;
log: CronServiceDeps["log"];
nowMs: () => number;
jobs: CronJob[];
}) {
const state = createCronServiceState({
cronEnabled: true,
storeKey: params.storeKey ?? "default",
storePath: params.storePath,
log: params.log,
nowMs: params.nowMs,
enqueueSystemEvent: vi.fn(),
@@ -189,10 +180,10 @@ export function createIsolatedRegressionJob(params: {
};
}
export async function writeCronJobs(storeKey: string, jobs: CronJob[]) {
await saveCronStore(storeKey, { version: 1, jobs });
export async function writeCronJobs(storePath: string, jobs: CronJob[]) {
await fs.writeFile(storePath, JSON.stringify({ version: 1, jobs }), "utf-8");
}
export async function writeCronStoreSnapshot(storeKey: string, jobs: unknown[]) {
await saveCronStore(storeKey, { version: 1, jobs: jobs as CronJob[] });
export async function writeCronStoreSnapshot(storePath: string, jobs: unknown[]) {
await fs.writeFile(storePath, JSON.stringify({ version: 1, jobs }), "utf-8");
}

View File

@@ -1,5 +1,6 @@
import { randomUUID } from "node:crypto";
import { request as httpRequest } from "node:http";
import path from "node:path";
import { GatewayClient } from "../../src/gateway/client.js";
import { connectGatewayClient } from "../../src/gateway/test-helpers.e2e.js";
import { loadOrCreateDeviceIdentity } from "../../src/infra/device-identity.js";
@@ -87,7 +88,8 @@ export async function connectNode(
inst: GatewayInstance,
label: string,
): Promise<{ client: GatewayClient; nodeId: string }> {
const deviceIdentity = loadOrCreateDeviceIdentity({ key: `test:${inst.name}:${label}` });
const identityPath = path.join(inst.homeDir, `${label}-device.json`);
const deviceIdentity = loadOrCreateDeviceIdentity(identityPath);
const nodeId = deviceIdentity.deviceId;
const client = await connectGatewayClient({
url: `ws://127.0.0.1:${inst.port}`,

View File

@@ -17,19 +17,6 @@ type HeartbeatSendFn = (
opts?: Record<string, unknown>,
) => Promise<Record<string, unknown>>;
function parseTelegramMessageThreadId(
threadId: string | number | null | undefined,
): number | undefined {
if (typeof threadId === "number") {
return Number.isInteger(threadId) ? threadId : undefined;
}
if (typeof threadId !== "string" || !threadId.trim()) {
return undefined;
}
const parsed = Number(threadId);
return Number.isInteger(parsed) ? parsed : undefined;
}
function createHeartbeatOutboundAdapter(channelId: HeartbeatSendChannelId): ChannelOutboundAdapter {
return {
deliveryMode: "direct",
@@ -45,14 +32,11 @@ function createHeartbeatOutboundAdapter(channelId: HeartbeatSendChannelId): Chan
};
const sendOptions =
channelId === "telegram"
? (() => {
const messageThreadId = parseTelegramMessageThreadId(threadId);
return {
...baseOptions,
...(messageThreadId === undefined ? {} : { messageThreadId }),
...(typeof replyToId === "string" ? { replyToMessageId: Number(replyToId) } : {}),
};
})()
? {
...baseOptions,
...(typeof threadId === "number" ? { messageThreadId: threadId } : {}),
...(typeof replyToId === "string" ? { replyToMessageId: Number(replyToId) } : {}),
}
: {
...baseOptions,
...opts,

View File

@@ -6,8 +6,6 @@ type PnpmBuildConfig = {
allowBuilds?: Record<string, boolean>;
blockExoticSubdeps?: boolean;
ignoredBuiltDependencies?: string[];
minimumReleaseAgeIgnoreMissingTime?: boolean;
minimumReleaseAgeStrict?: boolean;
onlyBuiltDependencies?: string[];
};
@@ -31,19 +29,4 @@ describe("package manager build policy", () => {
expect(workspace.blockExoticSubdeps).toBe(true);
expect(workspace.onlyBuiltDependencies).toBeUndefined();
});
it("keeps exotic subdependency builds blocked by default", () => {
const workspace = parse(fs.readFileSync("pnpm-workspace.yaml", "utf8")) as WorkspaceConfig;
expect(workspace.allowBuilds?.["baileys"]).toBe(true);
expect(workspace.allowBuilds?.["@whiskeysockets/libsignal-node"]).toBeUndefined();
expect(workspace.blockExoticSubdeps).toBe(true);
});
it("does not relax release-age installs for missing registry publish metadata", () => {
const workspace = parse(fs.readFileSync("pnpm-workspace.yaml", "utf8")) as WorkspaceConfig;
expect(workspace.minimumReleaseAgeIgnoreMissingTime).toBeUndefined();
expect(workspace.minimumReleaseAgeStrict).toBeUndefined();
});
});

View File

@@ -58,12 +58,6 @@ describe("plugin npm runtime build planning", () => {
expect(qqbotRuntimePlan.entry).toEqual({
api: path.join(repoRoot, "extensions", "qqbot", "api.ts"),
"channel-plugin-api": path.join(repoRoot, "extensions", "qqbot", "channel-plugin-api.ts"),
"doctor-legacy-state-api": path.join(
repoRoot,
"extensions",
"qqbot",
"doctor-legacy-state-api.ts",
),
index: path.join(repoRoot, "extensions", "qqbot", "index.ts"),
"runtime-api": path.join(repoRoot, "extensions", "qqbot", "runtime-api.ts"),
"secret-contract-api": path.join(repoRoot, "extensions", "qqbot", "secret-contract-api.ts"),

View File

@@ -91,8 +91,8 @@ describe("packed CLI smoke", () => {
]);
});
it("keeps packed completion smoke scoped to one generated shell script", () => {
expect(PACKED_COMPLETION_SMOKE_ARGS).toEqual(["completion", "--shell", "zsh"]);
it("keeps packed completion smoke scoped to one shell cache", () => {
expect(PACKED_COMPLETION_SMOKE_ARGS).toEqual(["completion", "--write-state", "--shell", "zsh"]);
});
it("builds a packed CLI smoke env with packaged-install guardrails", () => {
@@ -133,7 +133,7 @@ describe("packed CLI smoke", () => {
});
});
it("skips plugin command discovery during packed completion smoke", () => {
it("skips plugin command discovery during packed completion cache smoke", () => {
expect(
createPackedCompletionSmokeEnv(
{

View File

@@ -473,6 +473,7 @@ describe("scripts/changed-lanes", () => {
"guarded extension wildcard re-exports",
"plugin-sdk wildcard re-exports",
"duplicate scan target coverage",
"dependency pin guard",
"typecheck core tests",
"lint core",
"lint scripts",
@@ -752,6 +753,7 @@ describe("scripts/changed-lanes", () => {
"lint:extensions:no-guarded-wildcard-reexports",
"lint:extensions:no-plugin-sdk-wildcard-reexports",
"dup:check:coverage",
"deps:pins:check",
"release-metadata:check",
"ios:version:check",
"config:schema:check",
@@ -952,6 +954,7 @@ describe("scripts/changed-lanes", () => {
args: ["lint:extensions:no-plugin-sdk-wildcard-reexports"],
},
{ name: "duplicate scan target coverage", args: ["dup:check:coverage"] },
{ name: "dependency pin guard", args: ["deps:pins:check"] },
]);
});
@@ -972,6 +975,7 @@ describe("scripts/changed-lanes", () => {
args: ["lint:extensions:no-plugin-sdk-wildcard-reexports"],
},
{ name: "duplicate scan target coverage", args: ["dup:check:coverage"] },
{ name: "dependency pin guard", args: ["deps:pins:check"] },
]);
});
});

View File

@@ -1,94 +0,0 @@
import { describe, expect, it } from "vitest";
import { collectKyselyGuardrailViolations } from "../../scripts/check-kysely-guardrails.mjs";
function messagesFor(content: string, relativePath = "src/example/store.sqlite.ts"): string[] {
return collectKyselyGuardrailViolations(content, relativePath).map(
(violation) => violation.message,
);
}
describe("Kysely guardrails", () => {
it("rejects explicit sync-helper row generics for builder queries", () => {
expect(
messagesFor(`
import { executeSqliteQuerySync } from "../infra/kysely-sync.js";
executeSqliteQuerySync<{ id: string }>(db, query);
`),
).toContain("sync helper row generic at call site; let Kysely infer builder result rows");
});
it("rejects persisted row casts to enum-like types in SQLite stores", () => {
expect(
messagesFor(`
type TaskStatus = "running" | "succeeded";
function rowToRecord(row: { status: string }) {
return {
status: row.status as TaskStatus,
};
}
`),
).toContain(
"persisted SQLite enum-like values must be parsed through closed validators, not cast",
);
});
it("allows explicit local escape hatches for reviewed persisted casts", () => {
expect(
messagesFor(`
type TaskStatus = "running" | "succeeded";
function rowToRecord(row: { status: string }) {
return {
status: row.status as TaskStatus, // sqlite-allow-persisted-cast
};
}
`),
).toEqual([]);
});
it("rejects typed raw SQL outside allowlisted boundaries", () => {
expect(
messagesFor(
`
import { sql } from "kysely";
const count = sql<number>\`COUNT(*)\`;
`,
"src/example/report.ts",
),
).toContain("typed raw sql snippet needs a small helper or allowlisted boundary");
});
it("rejects direct raw node:sqlite prepare in new production files", () => {
expect(
messagesFor(
`
import { requireNodeSqlite } from "../infra/node-sqlite.js";
const sqlite = requireNodeSqlite();
const db = new sqlite.DatabaseSync(":memory:");
db.prepare("select 1").get();
`,
"src/example/raw-store.ts",
),
).toContain(
"new raw node:sqlite access requires Kysely or an explicit raw SQLite allowlist entry",
);
});
it("keeps ordinary static Kysely reference strings valid", () => {
expect(
messagesFor(`
import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
const query = getNodeSqliteKysely<{ task_runs: { task_id: string } }>(db)
.selectFrom("task_runs")
.select(["task_id"])
.where("task_id", "=", taskId);
executeSqliteQuerySync(db, query);
`),
).toEqual([]);
});
});

View File

@@ -179,8 +179,7 @@ describe("docker build helper", () => {
expect(runner).toContain("scripts/e2e/lib/plugin-update/unchanged-scenario.sh");
expect(probe).toContain("plugin install record changed unexpectedly");
expect(probe).toContain("readInstalledPluginRecords()");
expect(probe).toContain('records["lossless-claw"] ?? records["@example/lossless-claw"]');
expect(probe).toContain("index.installRecords ?? index.records ?? config.plugins?.installs");
expect(scenario).toContain("Config changed unexpectedly for modern package");
expect(scenario).not.toContain("before_hash");
});

View File

@@ -51,31 +51,15 @@ describe("install.sh", () => {
const tmp = mkdtempSync(join(tmpdir(), "openclaw-install-nvm-"));
const home = join(tmp, "home");
const systemBin = join(tmp, "system-bin");
const nvmBin = join(home, ".nvm/versions/node/v24.13.0/bin");
const nvmBin = join(home, ".nvm/versions/node/v22.22.1/bin");
mkdirSync(systemBin, { recursive: true });
mkdirSync(nvmBin, { recursive: true });
mkdirSync(join(home, ".nvm"), { recursive: true });
const systemNode = join(systemBin, "node");
const nvmNode = join(nvmBin, "node");
writeFileSync(
systemNode,
[
"#!/bin/sh",
'if [ "${1:-}" = "-p" ]; then echo "8 11"; exit 0; fi',
"echo v8.11.3",
"",
].join("\n"),
);
writeFileSync(
nvmNode,
[
"#!/bin/sh",
'if [ "${1:-}" = "-p" ]; then echo "24 13"; exit 0; fi',
"echo v24.13.0",
"",
].join("\n"),
);
writeFileSync(systemNode, "#!/bin/sh\necho v8.11.3\n");
writeFileSync(nvmNode, "#!/bin/sh\necho v22.22.1\n");
chmodSync(systemNode, 0o755);
chmodSync(nvmNode, 0o755);
writeFileSync(
@@ -85,7 +69,7 @@ describe("install.sh", () => {
"export NVM_DIR",
"nvm() {",
' if [ "$1" = "use" ]; then',
' export PATH="$NVM_DIR/versions/node/v24.13.0/bin:$PATH"',
' export PATH="$NVM_DIR/versions/node/v22.22.1/bin:$PATH"',
" return 0",
" fi",
" return 0",
@@ -122,7 +106,7 @@ describe("install.sh", () => {
const output = result?.stdout ?? "";
expect(output).toContain("status=0");
expect(output).toContain(`path=${nvmNode}`);
expect(output).toContain("version=v24.13.0");
expect(output).toContain("version=v22.22.1");
});
it("promotes a supported Linux Node binary over stale PATH entries", () => {
@@ -134,24 +118,8 @@ describe("install.sh", () => {
const staleNode = join(staleBin, "node");
const supportedNode = join(supportedBin, "node");
writeFileSync(
staleNode,
[
"#!/bin/sh",
'if [ "${1:-}" = "-p" ]; then echo "20 20"; exit 0; fi',
"echo v20.20.0",
"",
].join("\n"),
);
writeFileSync(
supportedNode,
[
"#!/bin/sh",
'if [ "${1:-}" = "-p" ]; then echo "24 13"; exit 0; fi',
"echo v24.13.0",
"",
].join("\n"),
);
writeFileSync(staleNode, "#!/bin/sh\necho v20.20.0\n");
writeFileSync(supportedNode, "#!/bin/sh\necho v22.22.0\n");
chmodSync(staleNode, 0o755);
chmodSync(supportedNode, 0o755);
@@ -184,7 +152,7 @@ describe("install.sh", () => {
expect(output).toContain("promote=0");
expect(output).toContain("active=0");
expect(output).toContain(`path=${supportedNode}`);
expect(output).toContain("version=v24.13.0");
expect(output).toContain("version=v22.22.0");
});
it("persists a supported Linux Node path before noninteractive shell guards", () => {

View File

@@ -91,10 +91,7 @@ describe("production lint suppressions", () => {
"scripts/lib/plugin-npm-release.ts|typescript/no-unnecessary-type-parameters|1",
"src/agents/agent-scope.ts|no-control-regex|1",
"src/agents/pi-embedded-runner/run/images.ts|no-control-regex|1",
"src/agents/runtime-worker.entry.ts|unicorn/require-post-message-target-origin|1",
"src/agents/runtime-worker.ts|unicorn/require-post-message-target-origin|1",
"src/agents/subagent-attachments.ts|no-control-regex|1",
"src/agents/subagent-registry.store.ts|typescript/no-unnecessary-type-parameters|1",
"src/agents/subagent-spawn.ts|no-control-regex|1",
"src/channels/plugins/channel-runtime-surface.types.ts|typescript/no-unnecessary-type-parameters|1",
"src/channels/plugins/contracts/test-helpers.ts|typescript/no-unnecessary-type-parameters|1",
@@ -120,7 +117,6 @@ describe("production lint suppressions", () => {
"src/plugin-sdk/test-helpers/package-manifest-contract.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugin-sdk/test-helpers/public-surface-loader.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugin-sdk/test-helpers/subagent-hooks.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugins/hook-types.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugins/hooks.ts|typescript/no-unnecessary-type-parameters|1",
"src/plugins/host-hook-runtime.ts|typescript/no-unnecessary-type-parameters|2",
"src/plugins/host-hook-state.ts|typescript/no-unnecessary-type-parameters|1",

View File

@@ -13,10 +13,12 @@ describe("live Docker state staging", () => {
expect(script).toContain("--exclude=.artifacts");
});
it("keeps host workspace artifacts out of the container state copy", () => {
it("keeps host-only generated registry state out of the container copy", () => {
const script = readFileSync(stageScriptPath, "utf8");
expect(script).toContain("--exclude=workspace");
expect(script).toContain("--exclude=sandboxes");
expect(script).toContain("--exclude=plugins/installs.json");
expect(script).toContain("host-absolute paths");
});
});

View File

@@ -345,9 +345,10 @@ console.log(JSON.stringify(result));
expect(missingKey.stderr).toContain("PARALLELS_TEST_MISSING_KEY is required");
});
it("seeds configured agent workspace files before OS smoke agent turns", () => {
it("seeds agent workspace state before OS smoke agent turns", () => {
const workspace = readFileSync(TS_PATHS.agentWorkspace, "utf8");
expect(workspace).toContain("workspace-state.json");
expect(workspace).toContain("IDENTITY.md");
expect(workspace).toContain("BOOTSTRAP.md");
@@ -527,7 +528,7 @@ console.log(JSON.stringify(result));
expect(script).toContain("agent turn attempt $attempt failed or finished without OK response");
expect(script).not.toContain("$config.models.providers");
expect(script).not.toContain("timeoutSeconds = 300");
expect(script).not.toContain("$sessionId.jsonl");
expect(script).toContain('"$sessionId.jsonl"');
});
it("gives GPT-5.5 enough Parallels model time on slower desktop guests", () => {

View File

@@ -1,23 +0,0 @@
import { execFileSync } from "node:child_process";
import path from "node:path";
import { describe, expect, it } from "vitest";
const scriptPath = path.join(process.cwd(), "scripts", "pre-commit", "filter-staged-files.mjs");
function filterFiles(mode: "format" | "lint", files: string[]): string[] {
const output = execFileSync(process.execPath, [scriptPath, mode, "--", ...files], {
encoding: "utf8",
});
return output.split("\0").filter(Boolean);
}
describe("pre-commit staged-file filter", () => {
it("does not format generated Kysely declaration files", () => {
expect(
filterFiles("format", [
"src/state/openclaw-state-db.generated.d.ts",
"src/state/openclaw-state-db.ts",
]),
).toEqual(["src/state/openclaw-state-db.ts"]);
});
});

View File

@@ -7,7 +7,6 @@ import type {
import type { OpenClawConfig } from "../src/config/config.js";
import type { OutboundSendDeps } from "../src/infra/outbound/deliver.js";
import type { PluginRegistry } from "../src/plugins/registry.js";
import { closeOpenClawAgentDatabasesForTest } from "../src/state/openclaw-agent-db.js";
import { installSharedTestSetup } from "./setup.shared.js";
installSharedTestSetup();
@@ -24,8 +23,14 @@ type WorkerPluginRuntimeHelpers = {
setActivePluginRegistry: typeof import("../src/plugins/runtime.js").setActivePluginRegistry;
};
type WorkerCleanupHelpers = {
clearSessionStoreCaches: typeof import("../src/config/sessions/store-cache.js").clearSessionStoreCaches;
drainFileLockStateForTest: typeof import("../src/infra/file-lock.js").drainFileLockStateForTest;
drainSessionStoreWriterQueuesForTest: typeof import("../src/config/sessions/store-writer-state.js").drainSessionStoreWriterQueuesForTest;
drainSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").drainSessionWriteLockStateForTest;
resetContextWindowCacheForTest: typeof import("../src/agents/context-runtime-state.js").resetContextWindowCacheForTest;
resetModelCatalogReadyCacheForTest: typeof import("../src/agents/models-config-state.js").resetModelCatalogReadyCacheForTest;
resetFileLockStateForTest: typeof import("../src/infra/file-lock.js").resetFileLockStateForTest;
resetModelsJsonReadyCacheForTest: typeof import("../src/agents/models-config-state.js").resetModelsJsonReadyCacheForTest;
resetSessionWriteLockStateForTest: typeof import("../src/agents/session-write-lock.js").resetSessionWriteLockStateForTest;
};
type ReplyToModeResolver = NonNullable<
@@ -69,10 +74,36 @@ function loadWorkerCleanupHelpers(): Promise<WorkerCleanupHelpers> {
vi.importActual<typeof import("../src/agents/models-config-state.js")>(
"../src/agents/models-config-state.js",
),
]).then(([contextRuntimeState, modelsConfigState]) => ({
resetContextWindowCacheForTest: contextRuntimeState.resetContextWindowCacheForTest,
resetModelCatalogReadyCacheForTest: modelsConfigState.resetModelCatalogReadyCacheForTest,
}));
vi.importActual<typeof import("../src/agents/session-write-lock.js")>(
"../src/agents/session-write-lock.js",
),
vi.importActual<typeof import("../src/config/sessions/store-cache.js")>(
"../src/config/sessions/store-cache.js",
),
vi.importActual<typeof import("../src/config/sessions/store-writer-state.js")>(
"../src/config/sessions/store-writer-state.js",
),
vi.importActual<typeof import("../src/infra/file-lock.js")>("../src/infra/file-lock.js"),
]).then(
([
contextRuntimeState,
modelsConfigState,
sessionWriteLock,
sessionStoreCache,
sessionStoreWriterState,
fileLock,
]) => ({
clearSessionStoreCaches: sessionStoreCache.clearSessionStoreCaches,
drainFileLockStateForTest: fileLock.drainFileLockStateForTest,
drainSessionStoreWriterQueuesForTest:
sessionStoreWriterState.drainSessionStoreWriterQueuesForTest,
drainSessionWriteLockStateForTest: sessionWriteLock.drainSessionWriteLockStateForTest,
resetContextWindowCacheForTest: contextRuntimeState.resetContextWindowCacheForTest,
resetFileLockStateForTest: fileLock.resetFileLockStateForTest,
resetModelsJsonReadyCacheForTest: modelsConfigState.resetModelsJsonReadyCacheForTest,
resetSessionWriteLockStateForTest: sessionWriteLock.resetSessionWriteLockStateForTest,
}),
);
return globalState[WORKER_CLEANUP_HELPERS];
}
@@ -360,14 +391,31 @@ beforeAll(async () => {
});
afterEach(async () => {
const { resetContextWindowCacheForTest, resetModelCatalogReadyCacheForTest } =
await loadWorkerCleanupHelpers();
closeOpenClawAgentDatabasesForTest();
const {
clearSessionStoreCaches,
drainFileLockStateForTest,
drainSessionStoreWriterQueuesForTest,
drainSessionWriteLockStateForTest,
resetContextWindowCacheForTest,
resetFileLockStateForTest,
resetModelsJsonReadyCacheForTest,
resetSessionWriteLockStateForTest,
} = await loadWorkerCleanupHelpers();
await drainSessionStoreWriterQueuesForTest();
clearSessionStoreCaches();
await drainFileLockStateForTest();
await drainSessionWriteLockStateForTest();
resetFileLockStateForTest();
resetContextWindowCacheForTest();
resetModelCatalogReadyCacheForTest();
resetModelsJsonReadyCacheForTest();
resetSessionWriteLockStateForTest();
await installDefaultPluginRegistry();
});
afterAll(async () => {
closeOpenClawAgentDatabasesForTest();
const { clearSessionStoreCaches, drainFileLockStateForTest, drainSessionWriteLockStateForTest } =
await loadWorkerCleanupHelpers();
clearSessionStoreCaches();
await drainFileLockStateForTest();
await drainSessionWriteLockStateForTest();
});

View File

@@ -2,11 +2,6 @@ import fs from "node:fs";
import path from "node:path";
import { importFreshModule } from "openclaw/plugin-sdk/test-fixtures";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
loadPersistedAuthProfileStore,
savePersistedAuthProfileSecretsStore,
} from "../src/agents/auth-profiles/persisted.js";
import { closeOpenClawStateDatabaseForTest } from "../src/state/openclaw-state-db.js";
import { cleanupTempDirs, makeTempDir } from "./helpers/temp-dir.js";
import { installTestEnv } from "./test-env.js";
@@ -66,7 +61,6 @@ function requireTelegramStreaming(
}
afterEach(() => {
closeOpenClawStateDatabaseForTest();
while (cleanupFns.length > 0) {
cleanupFns.pop()?.();
}
@@ -124,29 +118,9 @@ describe("installTestEnv", () => {
path.join(realHome, ".openclaw", "external-plugins", "glueclaw", "openclaw.plugin.json"),
'{"id":"glueclaw"}\n',
);
const realStateDir = path.join(realHome, ".openclaw");
const realAgentDir = path.join(realStateDir, "agents", "main", "agent");
fs.mkdirSync(realAgentDir, { recursive: true });
savePersistedAuthProfileSecretsStore(
{
version: 1,
profiles: {
"openai:default": {
type: "api_key",
provider: "openai",
key: "sk-test",
},
},
},
realAgentDir,
{
env: {
...process.env,
HOME: realHome,
USERPROFILE: realHome,
OPENCLAW_STATE_DIR: realStateDir,
},
},
writeFile(
path.join(realHome, ".openclaw", "agents", "main", "agent", "auth-profiles.json"),
JSON.stringify({ version: 1, profiles: { default: { provider: "openai" } } }, null, 2),
);
writeFile(path.join(realHome, ".claude", ".credentials.json"), '{"accessToken":"token"}\n');
writeFile(path.join(realHome, ".claude", "projects", "old-session.jsonl"), "session\n");
@@ -225,12 +199,11 @@ describe("installTestEnv", () => {
),
),
).toBe(true);
const tempAgentDir = path.join(testEnv.tempHome, ".openclaw", "agents", "main", "agent");
expect(loadPersistedAuthProfileStore(tempAgentDir)?.profiles["openai:default"]).toEqual({
type: "api_key",
provider: "openai",
key: "sk-test",
});
expect(
fs.existsSync(
path.join(testEnv.tempHome, ".openclaw", "agents", "main", "agent", "auth-profiles.json"),
),
).toBe(true);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", ".credentials.json"))).toBe(true);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", "projects"))).toBe(false);
expect(fs.existsSync(path.join(testEnv.tempHome, ".claude", "settings.local.json"))).toBe(

View File

@@ -1,12 +1,9 @@
import { execFileSync } from "node:child_process";
import fs from "node:fs";
import { createRequire } from "node:module";
import os from "node:os";
import path from "node:path";
import JSON5 from "json5";
import {
loadPersistedAuthProfileStore,
savePersistedAuthProfileSecretsStore,
} from "../src/agents/auth-profiles/persisted.js";
type RestoreEntry = { key: string; value: string | undefined };
@@ -19,6 +16,13 @@ const LIVE_EXTERNAL_AUTH_FILES = [
".codex/auth.json",
".codex/config.toml",
] as const;
const requireFromHere = createRequire(import.meta.url);
type LegacyConfigCompatApi = typeof import("../src/commands/doctor/shared/legacy-config-compat.js");
type ConfigValidationApi = typeof import("../src/config/validation.js");
let cachedLegacyConfigCompatApi: LegacyConfigCompatApi | undefined;
let cachedConfigValidationApi: ConfigValidationApi | undefined;
function isTruthyEnvValue(value: string | undefined): boolean {
if (!value) {
@@ -46,6 +50,20 @@ function restoreEnv(entries: RestoreEntry[]): void {
}
}
function loadLegacyConfigCompatApi(): LegacyConfigCompatApi {
cachedLegacyConfigCompatApi ??= requireFromHere(
"../src/commands/doctor/shared/legacy-config-compat.js",
) as LegacyConfigCompatApi;
return cachedLegacyConfigCompatApi;
}
function loadConfigValidationApi(): ConfigValidationApi {
cachedConfigValidationApi ??= requireFromHere(
"../src/config/validation.js",
) as ConfigValidationApi;
return cachedConfigValidationApi;
}
function resolveHomeRelativePath(input: string, homeDir: string): string {
const trimmed = input.trim();
if (trimmed === "~") {
@@ -137,6 +155,10 @@ function resolveRestoreEntries(): RestoreEntry[] {
key: "OPENCLAW_ALLOW_SLOW_REPLY_TESTS",
value: process.env.OPENCLAW_ALLOW_SLOW_REPLY_TESTS,
},
{
key: "OPENCLAW_LIVE_TEST_NORMALIZE_CONFIG",
value: process.env.OPENCLAW_LIVE_TEST_NORMALIZE_CONFIG,
},
{ key: "HOME", value: process.env.HOME },
{ key: "USERPROFILE", value: process.env.USERPROFILE },
{ key: "XDG_CONFIG_HOME", value: process.env.XDG_CONFIG_HOME },
@@ -306,47 +328,36 @@ function sanitizeLiveConfig(raw: string): string {
});
}
return `${JSON.stringify(parsed, null, 2)}\n`;
if (!isTruthyEnvValue(process.env.OPENCLAW_LIVE_TEST_NORMALIZE_CONFIG)) {
return `${JSON.stringify(parsed, null, 2)}\n`;
}
const { applyLegacyDoctorMigrations } = loadLegacyConfigCompatApi();
const migrated = applyLegacyDoctorMigrations(parsed);
if (!migrated.next) {
return `${JSON.stringify(parsed, null, 2)}\n`;
}
const { validateConfigObjectWithPlugins } = loadConfigValidationApi();
const validated = validateConfigObjectWithPlugins(migrated.next);
return `${JSON.stringify(validated.ok ? validated.config : migrated.next, null, 2)}\n`;
} catch {
return raw;
}
}
function stageLiveAuthProfiles(params: {
env: NodeJS.ProcessEnv;
realHome: string;
realStateDir: string;
tempHome: string;
tempStateDir: string;
}): void {
const agentsDir = path.join(params.realStateDir, "agents");
function copyLiveAuthProfiles(realStateDir: string, tempStateDir: string): void {
const agentsDir = path.join(realStateDir, "agents");
if (!fs.existsSync(agentsDir)) {
return;
}
const sourceEnv: NodeJS.ProcessEnv = {
...params.env,
HOME: params.realHome,
USERPROFILE: params.realHome,
OPENCLAW_STATE_DIR: params.realStateDir,
};
const targetEnv: NodeJS.ProcessEnv = {
...params.env,
HOME: params.tempHome,
USERPROFILE: params.tempHome,
OPENCLAW_STATE_DIR: params.tempStateDir,
};
for (const entry of fs.readdirSync(agentsDir, { withFileTypes: true })) {
if (!entry.isDirectory()) {
continue;
}
const sourceAgentDir = path.join(agentsDir, entry.name, "agent");
const store = loadPersistedAuthProfileStore(sourceAgentDir, { env: sourceEnv });
if (!store) {
continue;
}
const targetAgentDir = path.join(params.tempStateDir, "agents", entry.name, "agent");
fs.mkdirSync(targetAgentDir, { recursive: true });
savePersistedAuthProfileSecretsStore(store, targetAgentDir, { env: targetEnv });
const sourcePath = path.join(agentsDir, entry.name, "agent", "auth-profiles.json");
const targetPath = path.join(tempStateDir, "agents", entry.name, "agent", "auth-profiles.json");
copyFileIfExists(sourcePath, targetPath);
}
}
@@ -390,13 +401,7 @@ function stageLiveTestState(params: {
path.join(realStateDir, "external-plugins"),
path.join(tempStateDir, "external-plugins"),
);
stageLiveAuthProfiles({
env: params.env,
realHome: params.realHome,
realStateDir,
tempHome: params.tempHome,
tempStateDir,
});
copyLiveAuthProfiles(realStateDir, tempStateDir);
for (const authDir of LIVE_EXTERNAL_AUTH_DIRS) {
copyDirIfExists(path.join(params.realHome, authDir), path.join(params.tempHome, authDir));

View File

@@ -62,6 +62,7 @@ describe("unit-fast vitest lane", () => {
expect(testConfig.include).toContain("src/commands/status-overview-values.test.ts");
expect(testConfig.include).toContain("src/entry.respawn.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");

View File

@@ -352,6 +352,7 @@ export const sharedVitestConfig = {
"src/agents/pi-tool-definition-adapter.ts",
"src/agents/tools/discord-actions*.ts",
"src/agents/tools/slack-actions.ts",
"src/infra/state-migrations.ts",
"src/infra/skills-remote.ts",
"src/infra/update-check.ts",
"src/infra/ports-inspect.ts",

View File

@@ -63,7 +63,7 @@ export const forcedUnitFastTestFiles = [
"packages/memory-host-sdk/src/host/internal.test.ts",
"packages/memory-host-sdk/src/host/post-json.test.ts",
"packages/memory-host-sdk/src/host/qmd-process.test.ts",
"packages/memory-host-sdk/src/host/session-transcripts.test.ts",
"packages/memory-host-sdk/src/host/session-files.test.ts",
"src/acp/client.test.ts",
"src/acp/control-plane/manager.test.ts",
"src/acp/session-mapper.test.ts",
@@ -102,6 +102,7 @@ export const forcedUnitFastTestFiles = [
"src/entry.respawn.test.ts",
"src/entry.version-fast-path.test.ts",
"src/entry.test.ts",
"src/flows/doctor-startup-channel-maintenance.test.ts",
"src/flows/search-setup.test.ts",
"src/i18n/registry.test.ts",
"src/image-generation/openai-compatible-image-provider.test.ts",
@@ -178,6 +179,7 @@ export const forcedUnitFastTestFiles = [
"src/sessions/transcript-events.test.ts",
"src/status/status-message.test.ts",
"src/security/windows-acl.test.ts",
"src/trajectory/cleanup.test.ts",
"src/trajectory/export.test.ts",
"src/trajectory/metadata.test.ts",
"src/trajectory/runtime.test.ts",