fix(config): make plugin entry migration atomic with temp-file + rename

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
YeonGyu-Kim
2026-04-01 18:33:29 -07:00
parent 51d9685571
commit 9418927162
2 changed files with 50 additions and 3 deletions
+40 -1
View File
@@ -1,4 +1,6 @@
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
/// <reference path="../../bun-test.d.ts" />
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
import { mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
@@ -53,6 +55,43 @@ describe("migrateLegacyPluginEntry", () => {
})
})
describe("#given renaming the temp file fails after writing the migrated config", () => {
describe("#when migrating the config", () => {
it("#then keeps the original config untouched and writes the migrated content to a sibling temp file", async () => {
const configPath = join(testDir, "opencode.json")
const originalContent = JSON.stringify({ plugin: ["oh-my-opencode@latest"] }, null, 2)
const tempPath = `${configPath}.tmp`
writeFileSync(configPath, originalContent)
const fs = await import("node:fs")
const originalRenameSync = fs.renameSync
mock.module("node:fs", () => ({
...fs,
renameSync: () => {
throw new Error("simulated rename failure")
},
}))
try {
const { migrateLegacyPluginEntry } = await importFreshMigrationModule()
const result = migrateLegacyPluginEntry(configPath)
expect(result).toBe(false)
expect(readFileSync(configPath, "utf-8")).toBe(originalContent)
expect(readFileSync(tempPath, "utf-8")).toContain("oh-my-openagent@latest")
expect(readFileSync(tempPath, "utf-8")).not.toContain("oh-my-opencode")
} finally {
mock.module("node:fs", () => ({
...fs,
renameSync: originalRenameSync,
}))
}
})
})
})
describe("#given opencode.json contains pinned oh-my-opencode version", () => {
describe("#when migrating the config", () => {
it("#then preserves the version pin", async () => {
+10 -2
View File
@@ -1,4 +1,4 @@
import { existsSync, readFileSync, writeFileSync } from "node:fs"
import { closeSync, existsSync, fsyncSync, openSync, readFileSync, renameSync, writeFileSync } from "node:fs"
import { applyEdits, modify } from "jsonc-parser"
import { parseJsoncSafe } from "./jsonc-parser"
@@ -66,7 +66,15 @@ export function migrateLegacyPluginEntry(configPath: string): boolean {
: JSON.stringify({ ...(parseResult.data as OpenCodeConfig), plugin: updatedPluginEntries }, null, 2) + "\n"
if (!updated || updated === content) return false
writeFileSync(configPath, updated, "utf-8")
const tempPath = `${configPath}.tmp`
writeFileSync(tempPath, updated, "utf-8")
const tempFileDescriptor = openSync(tempPath, "r")
try {
fsyncSync(tempFileDescriptor)
} finally {
closeSync(tempFileDescriptor)
}
renameSync(tempPath, configPath)
log("[migrateLegacyPluginEntry] Auto-migrated opencode.json plugin entry", {
configPath,
from: LEGACY_PLUGIN_NAME,