Merge pull request #3718 from ismetanin/fix/cached-version-per-plugin-sandbox

fix(auto-update-checker): read loaded plugin's package.json, not the legacy flat install
This commit is contained in:
acamq
2026-04-29 08:16:19 -06:00
committed by GitHub
2 changed files with 42 additions and 12 deletions
@@ -4,7 +4,10 @@ import { tmpdir } from "node:os"
import { join } from "node:path"
// Hold mutable mock state so beforeEach can swap the cache root for each test.
const mockState: { candidates: string[] } = { candidates: [] }
const mockState: { candidates: string[]; walkUpResult: string | null } = {
candidates: [],
walkUpResult: null,
}
mock.module("../constants", () => ({
INSTALLED_PACKAGE_JSON_CANDIDATES: new Proxy([], {
@@ -22,7 +25,7 @@ mock.module("../constants", () => ({
}))
mock.module("./package-json-locator", () => ({
findPackageJsonUp: () => null,
findPackageJsonUp: () => mockState.walkUpResult,
}))
import { getCachedVersion } from "./cached-version"
@@ -36,11 +39,13 @@ describe("getCachedVersion (GH-3257)", () => {
join(cacheRoot, "node_modules", "oh-my-opencode", "package.json"),
join(cacheRoot, "node_modules", "oh-my-openagent", "package.json"),
]
mockState.walkUpResult = null
})
afterEach(() => {
rmSync(cacheRoot, { recursive: true, force: true })
mockState.candidates = []
mockState.walkUpResult = null
})
it("returns the version when the package is installed under oh-my-opencode", () => {
@@ -77,4 +82,23 @@ describe("getCachedVersion (GH-3257)", () => {
it("returns null when neither candidate exists and fallbacks find nothing", () => {
expect(getCachedVersion()).toBeNull()
})
it("prefers the loaded module's package.json over flat-install candidates", () => {
// OpenCode loads plugins from a per-plugin sandbox at
// <CACHE_DIR>/<plugin-entry>/node_modules/<pkg>/, while a parallel flat
// install at <CACHE_DIR>/node_modules/<pkg>/ can drift independently when
// bun re-resolves "latest". The flat install must NOT take precedence,
// because that's the path the user is actually running.
const sandboxDir = join(cacheRoot, "oh-my-openagent@latest", "node_modules", "oh-my-openagent")
mkdirSync(sandboxDir, { recursive: true })
const sandboxPkgJson = join(sandboxDir, "package.json")
writeFileSync(sandboxPkgJson, JSON.stringify({ name: "oh-my-openagent", version: "3.17.5" }))
mockState.walkUpResult = sandboxPkgJson
const flatDir = join(cacheRoot, "node_modules", "oh-my-opencode")
mkdirSync(flatDir, { recursive: true })
writeFileSync(join(flatDir, "package.json"), JSON.stringify({ name: "oh-my-opencode", version: "3.17.6" }))
expect(getCachedVersion()).toBe("3.17.5")
})
})
@@ -13,16 +13,12 @@ function readPackageVersion(packageJsonPath: string): string | null {
}
export function getCachedVersion(): string | null {
for (const candidate of INSTALLED_PACKAGE_JSON_CANDIDATES) {
try {
if (fs.existsSync(candidate)) {
return readPackageVersion(candidate)
}
} catch {
// ignore; try next candidate
}
}
// Walk up from the loaded module first. OpenCode loads plugins from a
// per-plugin sandbox at <CACHE_DIR>/<plugin-entry>/node_modules/<pkg>/, while
// a parallel flat install at <CACHE_DIR>/node_modules/<pkg>/ can drift
// independently when bun re-resolves "latest". Reading the flat install
// first means the toast can announce a version the runtime isn't running.
// The module-relative walk-up always reflects what is actually loaded.
try {
const currentDir = path.dirname(fileURLToPath(import.meta.url))
const pkgPath = findPackageJsonUp(currentDir)
@@ -33,6 +29,16 @@ export function getCachedVersion(): string | null {
log("[auto-update-checker] Failed to resolve version from current directory:", err)
}
for (const candidate of INSTALLED_PACKAGE_JSON_CANDIDATES) {
try {
if (fs.existsSync(candidate)) {
return readPackageVersion(candidate)
}
} catch {
// ignore; try next candidate
}
}
try {
const execDir = path.dirname(fs.realpathSync(process.execPath))
const pkgPath = findPackageJsonUp(execDir)