diff --git a/src/hooks/auto-update-checker/checker/cached-version.test.ts b/src/hooks/auto-update-checker/checker/cached-version.test.ts index 6a6790134..352de6d19 100644 --- a/src/hooks/auto-update-checker/checker/cached-version.test.ts +++ b/src/hooks/auto-update-checker/checker/cached-version.test.ts @@ -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 + // //node_modules//, while a parallel flat + // install at /node_modules// 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") + }) }) diff --git a/src/hooks/auto-update-checker/checker/cached-version.ts b/src/hooks/auto-update-checker/checker/cached-version.ts index 4cf6ebc1c..59886a17b 100644 --- a/src/hooks/auto-update-checker/checker/cached-version.ts +++ b/src/hooks/auto-update-checker/checker/cached-version.ts @@ -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 //node_modules//, while + // a parallel flat install at /node_modules// 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)