Preserve migration history during config migration
This commit is contained in:
@@ -35,6 +35,31 @@ describe("migrateLegacyConfigFile", () => {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
describe("#given a legacy config sidecar exists", () => {
|
||||||
|
describe("#when migrating the config file", () => {
|
||||||
|
it("#then copies applied migration history to the canonical sidecar", () => {
|
||||||
|
const legacyPath = join(testDir, "oh-my-opencode.json")
|
||||||
|
const legacySidecarPath = `${legacyPath}.migrations.json`
|
||||||
|
const canonicalSidecarPath = join(testDir, "oh-my-openagent.json.migrations.json")
|
||||||
|
writeFileSync(legacyPath, '{ "agents": { "oracle": { "model": "anthropic/claude-opus-4-6" } } }')
|
||||||
|
writeFileSync(
|
||||||
|
legacySidecarPath,
|
||||||
|
JSON.stringify({
|
||||||
|
appliedMigrations: [
|
||||||
|
"model-version:anthropic/claude-opus-4-6->anthropic/claude-opus-4-7",
|
||||||
|
],
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
|
||||||
|
const result = migrateLegacyConfigFile(legacyPath)
|
||||||
|
|
||||||
|
expect(result).toBe(true)
|
||||||
|
expect(existsSync(canonicalSidecarPath)).toBe(true)
|
||||||
|
expect(readFileSync(canonicalSidecarPath, "utf-8")).toBe(readFileSync(legacySidecarPath, "utf-8"))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
describe("#given oh-my-opencode.json exists but oh-my-openagent.json does not", () => {
|
describe("#given oh-my-opencode.json exists but oh-my-openagent.json does not", () => {
|
||||||
describe("#when migrating the config file", () => {
|
describe("#when migrating the config file", () => {
|
||||||
it("#then copies to oh-my-openagent.json", () => {
|
it("#then copies to oh-my-openagent.json", () => {
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { existsSync, readFileSync, renameSync, rmSync } from "node:fs"
|
|||||||
import { join, dirname, basename } from "node:path"
|
import { join, dirname, basename } from "node:path"
|
||||||
|
|
||||||
import { log } from "./logger"
|
import { log } from "./logger"
|
||||||
|
import { getSidecarPath } from "./migration/migrations-sidecar"
|
||||||
import { CONFIG_BASENAME, LEGACY_CONFIG_BASENAME } from "./plugin-identity"
|
import { CONFIG_BASENAME, LEGACY_CONFIG_BASENAME } from "./plugin-identity"
|
||||||
import { writeFileAtomically } from "./write-file-atomically"
|
import { writeFileAtomically } from "./write-file-atomically"
|
||||||
|
|
||||||
@@ -42,6 +43,31 @@ function archiveLegacyConfigFile(legacyPath: string): boolean {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function migrateLegacySidecarFile(legacyPath: string, canonicalPath: string): boolean {
|
||||||
|
const legacySidecarPath = getSidecarPath(legacyPath)
|
||||||
|
if (!existsSync(legacySidecarPath)) return true
|
||||||
|
|
||||||
|
const canonicalSidecarPath = getSidecarPath(canonicalPath)
|
||||||
|
if (existsSync(canonicalSidecarPath)) return true
|
||||||
|
|
||||||
|
try {
|
||||||
|
const content = readFileSync(legacySidecarPath, "utf-8")
|
||||||
|
writeFileAtomically(canonicalSidecarPath, content)
|
||||||
|
log("[migrateLegacyConfigFile] Migrated legacy migration sidecar to canonical path", {
|
||||||
|
from: legacySidecarPath,
|
||||||
|
to: canonicalSidecarPath,
|
||||||
|
})
|
||||||
|
return true
|
||||||
|
} catch (error) {
|
||||||
|
log("[migrateLegacyConfigFile] Failed to migrate legacy migration sidecar", {
|
||||||
|
legacySidecarPath,
|
||||||
|
canonicalSidecarPath,
|
||||||
|
error,
|
||||||
|
})
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export function migrateLegacyConfigFile(legacyPath: string): boolean {
|
export function migrateLegacyConfigFile(legacyPath: string): boolean {
|
||||||
if (!existsSync(legacyPath)) return false
|
if (!existsSync(legacyPath)) return false
|
||||||
if (!basename(legacyPath).startsWith(LEGACY_CONFIG_BASENAME)) return false
|
if (!basename(legacyPath).startsWith(LEGACY_CONFIG_BASENAME)) return false
|
||||||
@@ -52,10 +78,12 @@ export function migrateLegacyConfigFile(legacyPath: string): boolean {
|
|||||||
try {
|
try {
|
||||||
const content = readFileSync(legacyPath, "utf-8")
|
const content = readFileSync(legacyPath, "utf-8")
|
||||||
writeFileAtomically(canonicalPath, content)
|
writeFileAtomically(canonicalPath, content)
|
||||||
|
const migratedSidecar = migrateLegacySidecarFile(legacyPath, canonicalPath)
|
||||||
const archivedLegacyConfig = archiveLegacyConfigFile(legacyPath)
|
const archivedLegacyConfig = archiveLegacyConfigFile(legacyPath)
|
||||||
log("[migrateLegacyConfigFile] Migrated legacy config to canonical path", {
|
log("[migrateLegacyConfigFile] Migrated legacy config to canonical path", {
|
||||||
from: legacyPath,
|
from: legacyPath,
|
||||||
to: canonicalPath,
|
to: canonicalPath,
|
||||||
|
migratedSidecar,
|
||||||
archivedLegacyConfig,
|
archivedLegacyConfig,
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
|
|||||||
@@ -118,6 +118,37 @@ describe("migrateConfigFile sidecar write ordering", () => {
|
|||||||
)
|
)
|
||||||
expect(statSync(getSidecarPath(configPath)).isDirectory()).toBe(true)
|
expect(statSync(getSidecarPath(configPath)).isDirectory()).toBe(true)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test("treats top-level appliedMigrations as migration history and does not reapply the model update", () => {
|
||||||
|
// given
|
||||||
|
const workdir = createWorkdir()
|
||||||
|
const configPath = join(workdir, "oh-my-openagent.json")
|
||||||
|
const rawConfig: Record<string, unknown> = {
|
||||||
|
agents: {
|
||||||
|
oracle: { model: "anthropic/claude-opus-4-6" },
|
||||||
|
},
|
||||||
|
appliedMigrations: ["model-version:anthropic/claude-opus-4-6->anthropic/claude-opus-4-7"],
|
||||||
|
}
|
||||||
|
|
||||||
|
writeFileSync(configPath, JSON.stringify(rawConfig, null, 2) + "\n")
|
||||||
|
|
||||||
|
// when
|
||||||
|
const needsWrite = migrateConfigFile(configPath, rawConfig)
|
||||||
|
|
||||||
|
// then
|
||||||
|
expect(needsWrite).toBe(true)
|
||||||
|
expect(rawConfig.appliedMigrations).toBeUndefined()
|
||||||
|
expect((rawConfig.agents as Record<string, Record<string, unknown>>).oracle.model).toBe(
|
||||||
|
"anthropic/claude-opus-4-6",
|
||||||
|
)
|
||||||
|
|
||||||
|
const sidecar = JSON.parse(readFileSync(getSidecarPath(configPath), "utf-8")) as {
|
||||||
|
appliedMigrations: string[]
|
||||||
|
}
|
||||||
|
expect(sidecar.appliedMigrations).toEqual([
|
||||||
|
"model-version:anthropic/claude-opus-4-6->anthropic/claude-opus-4-7",
|
||||||
|
])
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
describe("migrateConfigFile backup skipping", () => {
|
describe("migrateConfigFile backup skipping", () => {
|
||||||
|
|||||||
@@ -22,13 +22,18 @@ export function migrateConfigFile(
|
|||||||
// that still carry `_migrations` working without a forced reset.
|
// that still carry `_migrations` working without a forced reset.
|
||||||
const sidecarMigrations = readAppliedMigrations(configPath)
|
const sidecarMigrations = readAppliedMigrations(configPath)
|
||||||
const inConfigMigrations = Array.isArray(copy._migrations)
|
const inConfigMigrations = Array.isArray(copy._migrations)
|
||||||
? new Set(copy._migrations as string[])
|
? new Set(copy._migrations.filter((migration): migration is string => typeof migration === "string"))
|
||||||
|
: new Set<string>()
|
||||||
|
const inlineAppliedMigrations = Array.isArray(copy.appliedMigrations)
|
||||||
|
? new Set(copy.appliedMigrations.filter((migration): migration is string => typeof migration === "string"))
|
||||||
: new Set<string>()
|
: new Set<string>()
|
||||||
const existingMigrations = new Set<string>([
|
const existingMigrations = new Set<string>([
|
||||||
...sidecarMigrations,
|
...sidecarMigrations,
|
||||||
...inConfigMigrations,
|
...inConfigMigrations,
|
||||||
|
...inlineAppliedMigrations,
|
||||||
])
|
])
|
||||||
const hadLegacyInConfigMigrations = inConfigMigrations.size > 0
|
const hadLegacyInConfigMigrations = inConfigMigrations.size > 0
|
||||||
|
const hadInlineAppliedMigrations = inlineAppliedMigrations.size > 0
|
||||||
const allNewMigrations: string[] = []
|
const allNewMigrations: string[] = []
|
||||||
|
|
||||||
if (copy.agents && typeof copy.agents === "object") {
|
if (copy.agents && typeof copy.agents === "object") {
|
||||||
@@ -78,12 +83,13 @@ export function migrateConfigFile(
|
|||||||
...existingMigrations,
|
...existingMigrations,
|
||||||
...newMigrationsToRecord,
|
...newMigrationsToRecord,
|
||||||
])
|
])
|
||||||
const shouldWriteSidecar = newMigrationsToRecord.length > 0 || hadLegacyInConfigMigrations
|
const shouldWriteSidecar = newMigrationsToRecord.length > 0 || hadLegacyInConfigMigrations || hadInlineAppliedMigrations
|
||||||
if (newMigrationsToRecord.length > 0) {
|
if (newMigrationsToRecord.length > 0) {
|
||||||
needsWrite = true
|
needsWrite = true
|
||||||
}
|
}
|
||||||
if (hadLegacyInConfigMigrations) {
|
if (hadLegacyInConfigMigrations || hadInlineAppliedMigrations) {
|
||||||
// Migrating state out of the config body is itself a config write.
|
// Migrating state out of the config body is itself a config write.
|
||||||
|
delete copy.appliedMigrations
|
||||||
needsWrite = true
|
needsWrite = true
|
||||||
}
|
}
|
||||||
if (shouldWriteSidecar) {
|
if (shouldWriteSidecar) {
|
||||||
|
|||||||
Reference in New Issue
Block a user