fix: persist migrated tlon install specs

This commit is contained in:
Josh Lehman
2026-03-11 16:38:37 -07:00
parent e244bf893b
commit 89c59c50d5
2 changed files with 101 additions and 15 deletions

View File

@@ -3,6 +3,7 @@ import os from "node:os";
import path from "node:path";
import { describe, expect, it, vi } from "vitest";
import { createConfigIO } from "./io.js";
import { migrateLegacyConfig } from "./legacy-migrate.js";
async function withTempHome(run: (home: string) => Promise<void>): Promise<void> {
const home = await fs.mkdtemp(path.join(os.tmpdir(), "openclaw-config-"));
@@ -212,4 +213,61 @@ describe("config io paths", () => {
expect(snapshot.config.plugins?.installs?.tlon?.resolvedSpec).toBeUndefined();
});
});
it("persists migrated tlon install specs back to disk", async () => {
await withTempHome(async (home) => {
const configDir = path.join(home, ".openclaw");
await fs.mkdir(configDir, { recursive: true });
const configPath = path.join(configDir, "openclaw.json");
await fs.writeFile(
configPath,
JSON.stringify(
{
plugins: {
installs: {
tlon: {
source: "npm",
spec: "@openclaw/tlon@2026.2.21",
resolvedName: "@openclaw/tlon",
resolvedVersion: "2026.2.21",
resolvedSpec: "@openclaw/tlon@2026.2.21",
integrity: "sha512-old",
shasum: "old",
resolvedAt: "2026-03-01T00:00:00.000Z",
installPath: "/tmp/tlon",
version: "2026.2.21",
},
},
},
},
null,
2,
),
"utf-8",
);
const io = createIoForHome(home);
const parsed = JSON.parse(await fs.readFile(configPath, "utf-8")) as unknown;
const migrated = migrateLegacyConfig(parsed);
expect(migrated.config).toBeTruthy();
await io.writeConfigFile(migrated.config!);
const written = JSON.parse(await fs.readFile(configPath, "utf-8")) as {
plugins?: {
installs?: {
tlon?: {
spec?: string;
resolvedSpec?: string;
resolvedName?: string;
};
};
};
};
expect(written.plugins?.installs?.tlon?.spec).toBe("@tloncorp/openclaw@2026.2.21");
expect(written.plugins?.installs?.tlon?.resolvedSpec).toBeUndefined();
expect(written.plugins?.installs?.tlon?.resolvedName).toBeUndefined();
});
});
});

View File

@@ -41,8 +41,7 @@ import {
readConfigIncludeFileWithGuards,
resolveConfigIncludes,
} from "./includes.js";
import { migrateLegacyConfig } from "./legacy-migrate.js";
import { findLegacyConfigIssues } from "./legacy.js";
import { applyLegacyMigrations, findLegacyConfigIssues } from "./legacy.js";
import { applyMergePatch } from "./merge-patch.js";
import { normalizeExecSafeBinProfilesInConfig } from "./normalize-exec-safe-bin.js";
import { normalizeConfigPaths } from "./normalize-paths.js";
@@ -719,18 +718,49 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
) {
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw, sourceRaw);
if (shouldAutoMigrateLegacyIssues(legacyIssues)) {
const migrated = migrateLegacyConfig(resolvedConfigRaw);
if (migrated.config) {
return {
legacyIssues,
validated: validateConfigObjectWithPlugins(migrated.config),
};
const migrated = applyLegacyMigrations(resolvedConfigRaw);
if (migrated.next) {
const validatedMigrated = validateConfigObjectWithPlugins(migrated.next);
if (validatedMigrated.ok) {
return {
legacyIssues,
validated: validatedMigrated,
effectiveResolvedConfigRaw: migrated.next,
};
}
}
}
return {
legacyIssues,
validated: validateConfigObjectWithPlugins(resolvedConfigRaw),
effectiveResolvedConfigRaw: resolvedConfigRaw,
};
}
function validateResolvedRawConfigWithAutoMigration(
resolvedConfigRaw: unknown,
sourceRaw?: unknown,
) {
const legacyIssues = findLegacyConfigIssues(resolvedConfigRaw, sourceRaw);
if (shouldAutoMigrateLegacyIssues(legacyIssues)) {
const migrated = applyLegacyMigrations(resolvedConfigRaw);
if (migrated.next) {
const validatedMigrated = validateConfigObjectRawWithPlugins(migrated.next);
if (validatedMigrated.ok) {
return {
legacyIssues,
validated: validatedMigrated,
effectiveResolvedConfigRaw: migrated.next,
};
}
}
}
return {
legacyIssues,
validated: validateConfigObjectRawWithPlugins(resolvedConfigRaw),
effectiveResolvedConfigRaw: resolvedConfigRaw,
};
}
@@ -978,10 +1008,8 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
}));
const resolvedConfigRaw = readResolution.resolvedConfigRaw;
const { legacyIssues, validated } = validateResolvedConfigWithAutoMigration(
resolvedConfigRaw,
parsedRes.parsed,
);
const { legacyIssues, validated, effectiveResolvedConfigRaw } =
validateResolvedConfigWithAutoMigration(resolvedConfigRaw, parsedRes.parsed);
if (!validated.ok) {
return {
snapshot: {
@@ -989,7 +1017,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
exists: true,
raw,
parsed: parsedRes.parsed,
resolved: coerceConfig(resolvedConfigRaw),
resolved: coerceConfig(effectiveResolvedConfigRaw),
valid: false,
config: coerceConfig(resolvedConfigRaw),
hash,
@@ -1021,7 +1049,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
parsed: parsedRes.parsed,
// Use resolvedConfigRaw (after $include and ${ENV} substitution but BEFORE runtime defaults)
// for config set/unset operations (issue #6070)
resolved: coerceConfig(resolvedConfigRaw),
resolved: coerceConfig(effectiveResolvedConfigRaw),
valid: true,
config: snapshotConfig,
hash,
@@ -1118,7 +1146,7 @@ export function createConfigIO(overrides: ConfigIoDeps = {}) {
}
}
const validated = validateConfigObjectRawWithPlugins(persistCandidate);
const { validated } = validateResolvedRawConfigWithAutoMigration(persistCandidate);
if (!validated.ok) {
const issue = validated.issues[0];
const pathLabel = issue?.path ? issue.path : "<root>";