refactor: keep legacy state paths in doctor

This commit is contained in:
Peter Steinberger
2026-05-09 05:10:56 +01:00
parent 31446c03bd
commit 47ba99dd98
16 changed files with 132 additions and 137 deletions

View File

@@ -131,7 +131,8 @@ The branch already has a real shared SQLite base:
The old TUI JSON file is doctor migration input only.
- Default TTS prefs now live in shared plugin-state SQLite rows keyed under the
`speech-core` plugin. The old `settings/tts.json` file is doctor migration
input only; runtime no longer reads or writes TTS prefs JSON files.
input only; runtime no longer reads or writes TTS prefs JSON files, and the
legacy path resolver lives in the doctor migration module.
- Subagent run recovery and OpenRouter model capability cache runtime modules
now keep SQLite readers/writers separate from doctor-only legacy JSON import
helpers.
@@ -154,6 +155,10 @@ The branch already has a real shared SQLite base:
same split: runtime modules expose SQLite-backed operations and narrow
migration writers, while doctor imports/removes the old JSON files through
`src/commands/doctor/legacy/*` modules.
- Core pairing and cron runtime modules no longer export legacy JSON path
builders. Doctor-owned legacy modules construct `pending.json`, `paired.json`,
`bootstrap.json`, and `cron/jobs.json` source paths for import tests and
migration only.
- `src/commands/doctor-sqlite-state.ts` already imports several legacy JSON
state files, including node host config, into SQLite from doctor.
- `src/commands/doctor/state-migrations.ts` imports legacy `sessions.json` and

View File

@@ -72,7 +72,7 @@ type LedgerStore = {
sessions: Record<string, LedgerSession>;
};
export type AcpEventLedgerMigrationStore = LedgerStore;
export type AcpEventLedgerSnapshot = LedgerStore;
type LedgerOptions = {
maxSessions?: number;
@@ -622,14 +622,12 @@ function writeStoreToSqlite(
}, options);
}
export function normalizeAcpEventLedgerStoreForMigration(
raw: unknown,
): AcpEventLedgerMigrationStore {
export function normalizeAcpEventLedgerSnapshot(raw: unknown): AcpEventLedgerSnapshot {
return normalizeStore(raw);
}
export function writeAcpEventLedgerStoreToSqliteForMigration(
store: AcpEventLedgerMigrationStore,
export function writeAcpEventLedgerSnapshotToSqlite(
store: AcpEventLedgerSnapshot,
options: OpenClawStateDatabaseOptions & { now?: () => number } = {},
): void {
writeStoreToSqlite(store, {

View File

@@ -4,12 +4,7 @@ import { promisify } from "node:util";
import { formatCliCommand } from "../cli/command-format.js";
import type { OpenClawConfig } from "../config/types.openclaw.js";
import { resolveCronRunLogPruneOptions } from "../cron/run-log.js";
import {
resolveCronStoreKey,
resolveLegacyCronStorePath,
loadCronStore,
saveCronStore,
} from "../cron/store.js";
import { resolveCronStoreKey, loadCronStore, saveCronStore } from "../cron/store.js";
import type { CronJob } from "../cron/types.js";
import {
normalizeOptionalLowercaseString,
@@ -32,6 +27,7 @@ import {
legacyCronStoreFileExists,
legacyCronStateFileExists,
loadLegacyCronStoreForMigration,
resolveLegacyCronStorePath,
} from "./doctor/legacy/cron-store.js";
type CronDoctorOutcome = {

View File

@@ -3,8 +3,8 @@ import fs from "node:fs/promises";
import path from "node:path";
import {
ACP_EVENT_LEDGER_VERSION,
normalizeAcpEventLedgerStoreForMigration,
writeAcpEventLedgerStoreToSqliteForMigration,
normalizeAcpEventLedgerSnapshot,
writeAcpEventLedgerSnapshotToSqlite,
} from "../../../acp/event-ledger.js";
import { resolveStateDir } from "../../../config/paths.js";
@@ -54,8 +54,8 @@ export async function importLegacyAcpEventLedgerFileToSqlite(
if (!isLegacyAcpEventLedgerShape(parsed)) {
return { imported: false, sessions: 0, events: 0 };
}
const store = normalizeAcpEventLedgerStoreForMigration(parsed);
writeAcpEventLedgerStoreToSqliteForMigration(store, { env });
const store = normalizeAcpEventLedgerSnapshot(parsed);
writeAcpEventLedgerSnapshotToSqlite(store, { env });
await fs.rm(filePath, { force: true }).catch(() => undefined);
return {
imported: true,

View File

@@ -1,14 +1,36 @@
import fs from "node:fs";
import path from "node:path";
import {
extractCronStateFileForMigration,
type CronStateFile,
type CronStateFileEntry,
writeCronJobRuntimeStateForMigration,
writeCronJobsForMigration,
extractCronRuntimeStateSnapshot,
saveCronStore,
type CronRuntimeStateEntry,
type CronRuntimeStateSnapshot,
writeCronRuntimeStateSnapshot,
} from "../../../cron/store.js";
import type { CronStoreFile } from "../../../cron/types.js";
import { expandHomePrefix } from "../../../infra/home-dir.js";
import { resolveConfigDir } from "../../../utils.js";
import { parseJsonWithJson5Fallback } from "../../../utils/parse-json-compat.js";
function resolveDefaultCronDir(): string {
return path.join(resolveConfigDir(), "cron");
}
function resolveDefaultLegacyCronStorePath(): string {
return path.join(resolveDefaultCronDir(), "jobs.json");
}
export function resolveLegacyCronStorePath(configuredLegacyStorePath?: string): string {
if (configuredLegacyStorePath?.trim()) {
const raw = configuredLegacyStorePath.trim();
if (raw.startsWith("~")) {
return path.resolve(expandHomePrefix(raw));
}
return path.resolve(raw);
}
return resolveDefaultLegacyCronStorePath();
}
function resolveStatePath(storePath: string): string {
if (storePath.endsWith(".json")) {
return storePath.replace(/\.json$/, "-state.json");
@@ -20,16 +42,16 @@ function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
function normalizeCronStateFile(value: unknown): CronStateFile | null {
function normalizeCronStateFile(value: unknown): CronRuntimeStateSnapshot | null {
if (!isRecord(value) || value.version !== 1 || !isRecord(value.jobs)) {
return null;
}
const jobs: Record<string, CronStateFileEntry> = {};
const jobs: Record<string, CronRuntimeStateEntry> = {};
for (const [jobId, entry] of Object.entries(value.jobs)) {
if (!isRecord(entry)) {
continue;
}
const normalized: CronStateFileEntry = {};
const normalized: CronRuntimeStateEntry = {};
if (typeof entry.updatedAtMs === "number" && Number.isFinite(entry.updatedAtMs)) {
normalized.updatedAtMs = entry.updatedAtMs;
}
@@ -60,7 +82,7 @@ export function legacyCronStateFileExists(storePath: string): boolean {
}
}
async function loadStateFile(statePath: string): Promise<CronStateFile | null> {
async function loadStateFile(statePath: string): Promise<CronRuntimeStateSnapshot | null> {
let raw: string;
try {
raw = await fs.promises.readFile(statePath, "utf-8");
@@ -125,7 +147,7 @@ export async function importLegacyCronStateFileToSqlite(params: {
if (!stateFile) {
return { imported: false, importedJobs: 0 };
}
const importedJobs = writeCronJobRuntimeStateForMigration(params.storeKey, stateFile);
const importedJobs = writeCronRuntimeStateSnapshot(params.storeKey, stateFile);
try {
await fs.promises.rm(statePath, { force: true });
} catch {
@@ -150,11 +172,11 @@ export async function importLegacyCronStoreToSqlite(params: {
if (!store) {
return { imported: false, importedJobs: 0 };
}
const stateFile =
const stateSnapshot =
(await loadStateFile(resolveStatePath(params.legacyStorePath))) ??
extractCronStateFileForMigration(store);
writeCronJobsForMigration(params.storeKey, store);
writeCronJobRuntimeStateForMigration(params.storeKey, stateFile);
extractCronRuntimeStateSnapshot(store);
await saveCronStore(params.storeKey, store);
writeCronRuntimeStateSnapshot(params.storeKey, stateSnapshot);
try {
await fs.promises.rm(params.legacyStorePath, { force: true });
} catch {

View File

@@ -1,10 +1,11 @@
import fs from "node:fs/promises";
import path from "node:path";
import { type DeviceBootstrapState } from "../../../infra/device-bootstrap.js";
import { resolvePairingPaths, writePairingStateRecord } from "../../../infra/pairing-files.js";
import { writePairingStateRecord } from "../../../infra/pairing-files.js";
import { resolveLegacyPairingPaths } from "./pairing-files.js";
function resolveBootstrapPath(baseDir?: string): string {
return path.join(resolvePairingPaths(baseDir, "devices").dir, "bootstrap.json");
return path.join(resolveLegacyPairingPaths(baseDir, "devices").dir, "bootstrap.json");
}
export async function legacyDeviceBootstrapFileExists(baseDir?: string): Promise<boolean> {

View File

@@ -1,16 +1,24 @@
import fs from "node:fs/promises";
import path from "node:path";
import { resolveStateDir } from "../../../config/paths.js";
import { readJsonIfExists } from "../../../infra/json-files.js";
import {
coercePairingStateRecord,
resolvePairingPaths,
writePairingStateRecord,
} from "../../../infra/pairing-files.js";
import { coercePairingStateRecord, writePairingStateRecord } from "../../../infra/pairing-files.js";
export function resolveLegacyPairingPaths(baseDir: string | undefined, subdir: string) {
const root = baseDir ?? resolveStateDir();
const dir = path.join(root, subdir);
return {
dir,
pendingPath: path.join(dir, "pending.json"),
pairedPath: path.join(dir, "paired.json"),
};
}
export async function legacyPairingStateFilesExist(params: {
baseDir?: string;
subdir: string;
}): Promise<boolean> {
const { pendingPath, pairedPath } = resolvePairingPaths(params.baseDir, params.subdir);
const { pendingPath, pairedPath } = resolveLegacyPairingPaths(params.baseDir, params.subdir);
const [pendingExists, pairedExists] = await Promise.all([
fs
.access(pendingPath)
@@ -32,7 +40,7 @@ export async function importLegacyPairingStateFilesToSqlite(params: {
paired: number;
files: number;
}> {
const { pendingPath, pairedPath } = resolvePairingPaths(params.baseDir, params.subdir);
const { pendingPath, pairedPath } = resolveLegacyPairingPaths(params.baseDir, params.subdir);
const [pending, paired] = await Promise.all([
readJsonIfExists<unknown>(pendingPath),
readJsonIfExists<unknown>(pairedPath),

View File

@@ -1,9 +1,11 @@
import fs from "node:fs/promises";
import {
resolveLegacyDefaultTtsPrefsPath,
writeTtsUserPrefsForMigration,
type TtsUserPrefs,
} from "../../../tts/tts-prefs-store.js";
import path from "node:path";
import { type TtsUserPrefs, writeTtsUserPrefsSnapshot } from "../../../tts/tts-prefs-store.js";
import { resolveConfigDir } from "../../../utils.js";
function resolveLegacyDefaultTtsPrefsPath(env: NodeJS.ProcessEnv = process.env): string {
return path.join(resolveConfigDir(env), "settings", "tts.json");
}
function coerceTtsPrefs(value: unknown): TtsUserPrefs {
return value && typeof value === "object" && !Array.isArray(value) ? (value as TtsUserPrefs) : {};
@@ -36,7 +38,7 @@ export async function importLegacyTtsPrefsFileToSqlite(
}
prefs = {};
}
writeTtsUserPrefsForMigration(prefs, env);
writeTtsUserPrefsSnapshot(prefs, env);
await fs.rm(filePath, { force: true }).catch(() => undefined);
return { imported: true };
}

View File

@@ -6,15 +6,10 @@ import {
importLegacyCronStateFileToSqlite,
importLegacyCronStoreToSqlite,
loadLegacyCronStoreForMigration,
resolveLegacyCronStorePath,
} from "../commands/doctor/legacy/cron-store.js";
import { closeOpenClawStateDatabaseForTest } from "../state/openclaw-state-db.js";
import {
loadCronStore,
loadCronStoreSync,
resolveLegacyCronStorePath,
saveCronStore,
updateCronStoreJobs,
} from "./store.js";
import { loadCronStore, loadCronStoreSync, saveCronStore, updateCronStoreJobs } from "./store.js";
import type { CronStoreFile } from "./types.js";
let fixtureRoot = "";

View File

@@ -1,13 +1,10 @@
import path from "node:path";
import type { Insertable } from "kysely";
import { expandHomePrefix } from "../infra/home-dir.js";
import { executeSqliteQuerySync, getNodeSqliteKysely } from "../infra/kysely-sync.js";
import type { DB as OpenClawStateKyselyDatabase } from "../state/openclaw-state-db.generated.js";
import {
openOpenClawStateDatabase,
runOpenClawStateWriteTransaction,
} from "../state/openclaw-state-db.js";
import { resolveConfigDir } from "../utils.js";
import { tryCronScheduleIdentity } from "./schedule-identity.js";
import type { CronJob, CronStoreFile } from "./types.js";
@@ -23,27 +20,19 @@ type CronJobRow = {
state_json: string;
};
const DEFAULT_CRON_STORE_KEY = "default";
function resolveDefaultCronDir(): string {
return path.join(resolveConfigDir(), "cron");
}
function resolveDefaultLegacyCronStorePath(): string {
return path.join(resolveDefaultCronDir(), "jobs.json");
}
export type CronStateFileEntry = {
export type CronRuntimeStateEntry = {
updatedAtMs?: number;
scheduleIdentity?: string;
state?: Record<string, unknown>;
};
export type CronStateFile = {
export type CronRuntimeStateSnapshot = {
version: 1;
jobs: Record<string, CronStateFileEntry>;
jobs: Record<string, CronRuntimeStateEntry>;
};
const DEFAULT_CRON_STORE_KEY = "default";
function cronStoreKey(storeKey: string): string {
const normalized = storeKey.trim();
return normalized || DEFAULT_CRON_STORE_KEY;
@@ -62,8 +51,8 @@ function stripRuntimeOnlyCronJobFields(job: CronJob): Record<string, unknown> {
return { ...rest, state: {} };
}
function extractStateFile(store: CronStoreFile): CronStateFile {
const jobs: Record<string, CronStateFileEntry> = {};
export function extractCronRuntimeStateSnapshot(store: CronStoreFile): CronRuntimeStateSnapshot {
const jobs: Record<string, CronRuntimeStateEntry> = {};
for (const job of store.jobs) {
jobs[job.id] = {
updatedAtMs: job.updatedAtMs,
@@ -74,23 +63,10 @@ function extractStateFile(store: CronStoreFile): CronStateFile {
return { version: 1, jobs };
}
export const extractCronStateFileForMigration = extractStateFile;
export function resolveCronStoreKey(): string {
return DEFAULT_CRON_STORE_KEY;
}
export function resolveLegacyCronStorePath(configuredLegacyStorePath?: string): string {
if (configuredLegacyStorePath?.trim()) {
const raw = configuredLegacyStorePath.trim();
if (raw.startsWith("~")) {
return path.resolve(expandHomePrefix(raw));
}
return path.resolve(raw);
}
return resolveDefaultLegacyCronStorePath();
}
function ensureJobStateObject(job: CronStoreFile["jobs"][number]): void {
if (!job.state || typeof job.state !== "object") {
job.state = {} as never;
@@ -116,7 +92,10 @@ function resolveUpdatedAtMs(job: CronStoreFile["jobs"][number], updatedAtMs: unk
: Date.now();
}
function mergeStateFileEntry(job: CronStoreFile["jobs"][number], entry: CronStateFileEntry): void {
function mergeRuntimeStateSnapshotEntry(
job: CronStoreFile["jobs"][number],
entry: CronRuntimeStateEntry,
): void {
job.updatedAtMs = resolveUpdatedAtMs(job, entry.updatedAtMs);
job.state = (entry.state ?? {}) as never;
if (
@@ -138,7 +117,7 @@ function parseCronStateJson(value: string): Record<string, unknown> {
}
function mergeCronJobRowRuntimeState(job: CronStoreFile["jobs"][number], row: CronJobRow): void {
mergeStateFileEntry(job, {
mergeRuntimeStateSnapshotEntry(job, {
updatedAtMs: row.runtime_updated_at_ms ?? undefined,
scheduleIdentity: row.schedule_identity ?? undefined,
state: parseCronStateJson(row.state_json),
@@ -257,17 +236,16 @@ function writeCronJobsToSqlite(storeKey: string, store: CronStoreFile): void {
});
}
export function writeCronJobsForMigration(storeKey: string, store: CronStoreFile): void {
writeCronJobsToSqlite(storeKey, store);
}
function writeCronJobRuntimeStateToSqlite(storeKey: string, stateFile: CronStateFile): number {
export function writeCronRuntimeStateSnapshot(
storeKey: string,
stateSnapshot: CronRuntimeStateSnapshot,
): number {
const normalizedStoreKey = cronStoreKey(storeKey);
const updatedAt = Date.now();
let importedJobs = 0;
runOpenClawStateWriteTransaction((database) => {
const db = getCronJobsKysely(database.db);
for (const [jobId, entry] of Object.entries(stateFile.jobs)) {
for (const [jobId, entry] of Object.entries(stateSnapshot.jobs)) {
const result = executeSqliteQuerySync(
database.db,
db
@@ -293,13 +271,6 @@ function writeCronJobRuntimeStateToSqlite(storeKey: string, stateFile: CronState
return importedJobs;
}
export function writeCronJobRuntimeStateForMigration(
storeKey: string,
stateFile: CronStateFile,
): number {
return writeCronJobRuntimeStateToSqlite(storeKey, stateFile);
}
export async function saveCronStore(
storeKey: string,
store: CronStoreFile,
@@ -307,7 +278,7 @@ export async function saveCronStore(
) {
void opts?.skipBackup;
if (opts?.stateOnly === true) {
writeCronJobRuntimeStateToSqlite(storeKey, extractStateFile(store));
writeCronRuntimeStateSnapshot(storeKey, extractCronRuntimeStateSnapshot(store));
return;
}
writeCronJobsToSqlite(storeKey, store);

View File

@@ -22,11 +22,16 @@ import {
type PairedDevice,
type RotateDeviceTokenResult,
} from "./device-pairing.js";
import {
readPairingStateRecord,
resolvePairingPaths,
writePairingStateRecord,
} from "./pairing-files.js";
import { readPairingStateRecord, writePairingStateRecord } from "./pairing-files.js";
function resolveLegacyPairingFixturePaths(baseDir: string, subdir: string) {
const dir = path.join(baseDir, subdir);
return {
dir,
pendingPath: path.join(dir, "pending.json"),
pairedPath: path.join(dir, "paired.json"),
};
}
async function setupPairedOperatorDevice(baseDir: string, scopes: string[]) {
const request = await requestDevicePairing(
@@ -175,7 +180,7 @@ describe("device pairing tokens", () => {
test("ignores legacy pairing state files at runtime", async () => {
const baseDir = await makeDevicePairingDir();
const paths = resolvePairingPaths(baseDir, "devices");
const paths = resolveLegacyPairingFixturePaths(baseDir, "devices");
await mkdir(paths.dir, { recursive: true });
await writeFile(paths.pendingPath, "[]", "utf8");
await writeFile(paths.pairedPath, "[]", "utf8");
@@ -1425,7 +1430,7 @@ describe("device pairing tokens", () => {
},
baseDir,
);
const { pairedPath } = resolvePairingPaths(baseDir, "devices");
const { pairedPath } = resolveLegacyPairingFixturePaths(baseDir, "devices");
await mkdir(path.dirname(pairedPath), { recursive: true });
await writeFile(pairedPath, "{not-json}", "utf8");

View File

@@ -1,4 +1,5 @@
import fs from "node:fs/promises";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, test } from "vitest";
import { createSuiteTempRootTracker } from "../test-helpers/temp-dir.js";
import {
@@ -10,7 +11,16 @@ import {
updatePairedNodeMetadata,
verifyNodeToken,
} from "./node-pairing.js";
import { readPairingStateRecord, resolvePairingPaths } from "./pairing-files.js";
import { readPairingStateRecord } from "./pairing-files.js";
function resolveLegacyPairingFixturePaths(baseDir: string, subdir: string) {
const dir = path.join(baseDir, subdir);
return {
dir,
pendingPath: path.join(dir, "pending.json"),
pairedPath: path.join(dir, "paired.json"),
};
}
async function setupPairedNode(baseDir: string): Promise<string> {
const request = await requestNodePairing(
@@ -131,7 +141,7 @@ describe("node pairing tokens", () => {
test("ignores legacy pairing state files at runtime", async () => {
await withNodePairingDir(async (baseDir) => {
const paths = resolvePairingPaths(baseDir, "nodes");
const paths = resolveLegacyPairingFixturePaths(baseDir, "nodes");
await fs.mkdir(paths.dir, { recursive: true });
await fs.writeFile(paths.pendingPath, "[]", "utf8");
await fs.writeFile(paths.pairedPath, "[]", "utf8");
@@ -268,7 +278,7 @@ describe("node pairing tokens", () => {
test("ignores corrupt legacy paired node state when requesting pairing", async () => {
await withNodePairingDir(async (baseDir) => {
const { dir, pairedPath } = resolvePairingPaths(baseDir, "nodes");
const { dir, pairedPath } = resolveLegacyPairingFixturePaths(baseDir, "nodes");
await fs.mkdir(dir, { recursive: true });
await fs.writeFile(pairedPath, "{not-json}", "utf8");

View File

@@ -1,5 +1,3 @@
import path from "node:path";
import { resolveStateDir } from "../config/paths.js";
import type { OpenClawStateDatabaseOptions } from "../state/openclaw-state-db.js";
import {
readOpenClawStateKvJson,
@@ -11,16 +9,6 @@ export { createAsyncLock } from "./json-files.js";
const PAIRING_STATE_SCOPE_PREFIX = "pairing";
export function resolvePairingPaths(baseDir: string | undefined, subdir: string) {
const root = baseDir ?? resolveStateDir();
const dir = path.join(root, subdir);
return {
dir,
pendingPath: path.join(dir, "pending.json"),
pairedPath: path.join(dir, "paired.json"),
};
}
function sqliteOptionsForBaseDir(baseDir: string | undefined): OpenClawStateDatabaseOptions {
return baseDir ? { env: { ...process.env, OPENCLAW_STATE_DIR: baseDir } } : {};
}

View File

@@ -5,7 +5,7 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
import type { OpenClawConfig } from "../config/types.js";
import { resetPluginStateStoreForTests } from "../plugin-state/plugin-state-store.js";
import { resolveStatusTtsSnapshot } from "./status-config.js";
import { writeTtsUserPrefsForMigration } from "./tts-prefs-store.js";
import { writeTtsUserPrefsSnapshot } from "./tts-prefs-store.js";
let fixtureRoot = "";
let fixtureId = 0;
@@ -54,7 +54,7 @@ function restoreEnv(key: string, value: string | undefined): void {
describe("resolveStatusTtsSnapshot", () => {
it("uses prefs overrides without loading speech providers", async () => {
await withStatusTempHome(async () => {
writeTtsUserPrefsForMigration({
writeTtsUserPrefsSnapshot({
tts: {
auto: "always",
provider: "edge",
@@ -283,7 +283,7 @@ describe("resolveStatusTtsSnapshot", () => {
it("uses provider metadata for local provider prefs overrides", async () => {
await withStatusTempHome(async () => {
writeTtsUserPrefsForMigration({
writeTtsUserPrefsSnapshot({
tts: {
auto: "always",
provider: "edge",
@@ -326,7 +326,7 @@ describe("resolveStatusTtsSnapshot", () => {
delete process.env.OPENCLAW_STATE_DIR;
vi.stubEnv("OPENCLAW_CONFIG_PATH", path.join(stateDir, "openclaw.json"));
try {
writeTtsUserPrefsForMigration({
writeTtsUserPrefsSnapshot({
tts: {
auto: "always",
provider: "openai",

View File

@@ -9,7 +9,7 @@ import {
resolveEffectiveTtsConfig,
shouldAttemptTtsPayload,
} from "./tts-config.js";
import { writeTtsUserPrefsForMigration } from "./tts-prefs-store.js";
import { writeTtsUserPrefsSnapshot } from "./tts-prefs-store.js";
describe("shouldAttemptTtsPayload", () => {
let originalStateDir: string | undefined;
@@ -59,7 +59,7 @@ describe("shouldAttemptTtsPayload", () => {
});
it("honors session auto state before prefs and config", () => {
writeTtsUserPrefsForMigration({ tts: { auto: "off" } });
writeTtsUserPrefsSnapshot({ tts: { auto: "off" } });
const cfg = { messages: { tts: { auto: "off" } } } as OpenClawConfig;
expect(shouldAttemptTtsPayload({ cfg, ttsAuto: "always" })).toBe(true);
@@ -69,10 +69,10 @@ describe("shouldAttemptTtsPayload", () => {
it("uses local prefs before config auto mode", () => {
const cfg = { messages: { tts: { auto: "off" } } } as OpenClawConfig;
writeTtsUserPrefsForMigration({ tts: { enabled: true } });
writeTtsUserPrefsSnapshot({ tts: { enabled: true } });
expect(shouldAttemptTtsPayload({ cfg })).toBe(true);
writeTtsUserPrefsForMigration({ tts: { auto: "off" } });
writeTtsUserPrefsSnapshot({ tts: { auto: "off" } });
expect(
shouldAttemptTtsPayload({ cfg: { messages: { tts: { enabled: true } } } as OpenClawConfig }),
).toBe(false);

View File

@@ -1,7 +1,5 @@
import path from "node:path";
import type { TtsAutoMode, TtsProvider } from "../config/types.tts.js";
import { createPluginStateSyncKeyedStore } from "../plugin-state/plugin-state-store.js";
import { resolveConfigDir } from "../utils.js";
const TTS_PREFS_PLUGIN_ID = "speech-core";
const TTS_PREFS_NAMESPACE = "tts-prefs";
@@ -40,10 +38,6 @@ export function isSqliteTtsPrefsRef(value: string): boolean {
return value === SQLITE_TTS_PREFS_REF;
}
export function resolveLegacyDefaultTtsPrefsPath(env: NodeJS.ProcessEnv = process.env): string {
return path.join(resolveConfigDir(env), "settings", "tts.json");
}
export function resolveTtsPrefsRef(
_prefsPath?: string,
_env: NodeJS.ProcessEnv = process.env,
@@ -58,7 +52,7 @@ export function readTtsUserPrefs(
return coercePrefs(openTtsPrefsStore(env).lookup(TTS_PREFS_KEY));
}
export function writeTtsUserPrefsForMigration(
export function writeTtsUserPrefsSnapshot(
prefs: TtsUserPrefs,
env: NodeJS.ProcessEnv = process.env,
): void {