From 975816867655dd14160beb71a2a99c564e06db80 Mon Sep 17 00:00:00 2001 From: deopa0402 Date: Wed, 29 Apr 2026 00:53:21 +0900 Subject: [PATCH] test(auto-update): isolate cached version resolution Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus --- .../checker/cached-version.test.ts | 49 +++++++++---------- .../checker/cached-version.ts | 34 +++++++++---- 2 files changed, 46 insertions(+), 37 deletions(-) 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 9d4c55806..ecc950dbc 100644 --- a/src/hooks/auto-update-checker/checker/cached-version.test.ts +++ b/src/hooks/auto-update-checker/checker/cached-version.test.ts @@ -1,7 +1,8 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" +import { afterEach, beforeEach, describe, expect, it } from "bun:test" import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" import { tmpdir } from "node:os" import { join } from "node:path" +import { getCachedVersion } from "./cached-version" // Hold mutable mock state so beforeEach can swap the cache root for each test. const mockState: { candidates: string[]; walkUpResult: string | null } = { @@ -9,27 +10,14 @@ const mockState: { candidates: string[]; walkUpResult: string | null } = { walkUpResult: null, } -mock.module("../constants", () => ({ - INSTALLED_PACKAGE_JSON_CANDIDATES: new Proxy([], { - get(_, prop) { - const current = mockState.candidates - // Forward array methods/properties to the mutable candidates list - // so getCachedVersion's `for (... of ...)` sees fresh data per test. - const value = (unsafeTestValue>(current))[prop] - if (typeof value === "function") { - return (value as (...args: unknown[]) => unknown).bind(current) - } - return value - }, - }), -})) - -mock.module("./package-json-locator", () => ({ - findPackageJsonUp: () => mockState.walkUpResult, -})) - -import { getCachedVersion } from "./cached-version" -import { unsafeTestValue } from "../../../../test-support/unsafe-test-value" +function getIsolatedCachedVersion(): string | null { + return getCachedVersion({ + packageJsonCandidates: mockState.candidates, + findPackageJson: () => null, + currentDir: null, + execDir: null, + }) +} describe("getCachedVersion (GH-3257)", () => { let cacheRoot: string @@ -54,7 +42,7 @@ describe("getCachedVersion (GH-3257)", () => { mkdirSync(pkgDir, { recursive: true }) writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name: "oh-my-opencode", version: "3.16.0" })) - expect(getCachedVersion()).toBe("3.16.0") + expect(getIsolatedCachedVersion()).toBe("3.16.0") }) it("returns the version when the package is installed under oh-my-openagent", () => { @@ -65,7 +53,7 @@ describe("getCachedVersion (GH-3257)", () => { mkdirSync(pkgDir, { recursive: true }) writeFileSync(join(pkgDir, "package.json"), JSON.stringify({ name: "oh-my-openagent", version: "3.16.0" })) - expect(getCachedVersion()).toBe("3.16.0") + expect(getIsolatedCachedVersion()).toBe("3.16.0") }) it("prefers oh-my-opencode when both are installed", () => { @@ -77,11 +65,11 @@ describe("getCachedVersion (GH-3257)", () => { mkdirSync(aliasDir, { recursive: true }) writeFileSync(join(aliasDir, "package.json"), JSON.stringify({ name: "oh-my-openagent", version: "3.15.0" })) - expect(getCachedVersion()).toBe("3.16.0") + expect(getIsolatedCachedVersion()).toBe("3.16.0") }) it("returns null when neither candidate exists and fallbacks find nothing", () => { - expect(getCachedVersion()).toBeNull() + expect(getIsolatedCachedVersion()).toBeNull() }) it("prefers the loaded module's package.json over flat-install candidates", () => { @@ -100,6 +88,13 @@ describe("getCachedVersion (GH-3257)", () => { 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") + expect( + getCachedVersion({ + packageJsonCandidates: mockState.candidates, + findPackageJson: () => mockState.walkUpResult, + currentDir: sandboxDir, + execDir: null, + }) + ).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 59886a17b..7bf8ef75b 100644 --- a/src/hooks/auto-update-checker/checker/cached-version.ts +++ b/src/hooks/auto-update-checker/checker/cached-version.ts @@ -6,13 +6,23 @@ import type { PackageJson } from "../types" import { INSTALLED_PACKAGE_JSON_CANDIDATES } from "../constants" import { findPackageJsonUp } from "./package-json-locator" +interface CachedVersionOptions { + packageJsonCandidates?: readonly string[] + findPackageJson?: (startPath: string) => string | null + currentDir?: string | null + execDir?: string | null +} + function readPackageVersion(packageJsonPath: string): string | null { const content = fs.readFileSync(packageJsonPath, "utf-8") const pkg = JSON.parse(content) as PackageJson return pkg.version ?? null } -export function getCachedVersion(): string | null { +export function getCachedVersion(options: CachedVersionOptions = {}): string | null { + const packageJsonCandidates = options.packageJsonCandidates ?? INSTALLED_PACKAGE_JSON_CANDIDATES + const findPackageJson = options.findPackageJson ?? findPackageJsonUp + // 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 @@ -20,16 +30,18 @@ export function getCachedVersion(): string | null { // 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) - if (pkgPath) { - return readPackageVersion(pkgPath) + const currentDir = options.currentDir === undefined ? path.dirname(fileURLToPath(import.meta.url)) : options.currentDir + if (currentDir) { + const pkgPath = findPackageJson(currentDir) + if (pkgPath) { + return readPackageVersion(pkgPath) + } } } catch (err) { log("[auto-update-checker] Failed to resolve version from current directory:", err) } - for (const candidate of INSTALLED_PACKAGE_JSON_CANDIDATES) { + for (const candidate of packageJsonCandidates) { try { if (fs.existsSync(candidate)) { return readPackageVersion(candidate) @@ -40,10 +52,12 @@ export function getCachedVersion(): string | null { } try { - const execDir = path.dirname(fs.realpathSync(process.execPath)) - const pkgPath = findPackageJsonUp(execDir) - if (pkgPath) { - return readPackageVersion(pkgPath) + const execDir = options.execDir === undefined ? path.dirname(fs.realpathSync(process.execPath)) : options.execDir + if (execDir) { + const pkgPath = findPackageJson(execDir) + if (pkgPath) { + return readPackageVersion(pkgPath) + } } } catch (err) { log("[auto-update-checker] Failed to resolve version from execPath:", err)