fix(auto-update): break successful install restart loop
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { existsSync } from "node:fs"
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs"
|
||||||
import { join } from "node:path"
|
import { dirname, join } from "node:path"
|
||||||
import { runBunInstallWithDetails } from "../../../cli/config-manager"
|
import { runBunInstallWithDetails } from "../../../cli/config-manager"
|
||||||
import { log } from "../../../shared/logger"
|
import { log } from "../../../shared/logger"
|
||||||
import { getOpenCodeCacheDir, getOpenCodeConfigPaths } from "../../../shared"
|
import { getOpenCodeCacheDir, getOpenCodeConfigPaths } from "../../../shared"
|
||||||
@@ -12,6 +12,10 @@ import { showAutoUpdatedToast, showUpdateAvailableToast } from "./update-toasts"
|
|||||||
|
|
||||||
type BackgroundUpdateCheckDeps = {
|
type BackgroundUpdateCheckDeps = {
|
||||||
existsSync: typeof existsSync
|
existsSync: typeof existsSync
|
||||||
|
mkdirSync: typeof mkdirSync
|
||||||
|
readFileSync: typeof readFileSync
|
||||||
|
writeFileSync: typeof writeFileSync
|
||||||
|
dirname: typeof dirname
|
||||||
join: typeof join
|
join: typeof join
|
||||||
runBunInstallWithDetails: typeof runBunInstallWithDetails
|
runBunInstallWithDetails: typeof runBunInstallWithDetails
|
||||||
log: typeof log
|
log: typeof log
|
||||||
@@ -25,6 +29,12 @@ type BackgroundUpdateCheckDeps = {
|
|||||||
syncCachePackageJsonToIntent: typeof syncCachePackageJsonToIntent
|
syncCachePackageJsonToIntent: typeof syncCachePackageJsonToIntent
|
||||||
showUpdateAvailableToast: typeof showUpdateAvailableToast
|
showUpdateAvailableToast: typeof showUpdateAvailableToast
|
||||||
showAutoUpdatedToast: typeof showAutoUpdatedToast
|
showAutoUpdatedToast: typeof showAutoUpdatedToast
|
||||||
|
now: () => number
|
||||||
|
}
|
||||||
|
|
||||||
|
type LastUpdatedMarker = {
|
||||||
|
readonly version: string
|
||||||
|
readonly updatedAt: number
|
||||||
}
|
}
|
||||||
|
|
||||||
type BackgroundUpdateCheckRunner = (
|
type BackgroundUpdateCheckRunner = (
|
||||||
@@ -39,6 +49,10 @@ function getCacheWorkspaceDir(deps: BackgroundUpdateCheckDeps): string {
|
|||||||
|
|
||||||
const defaultDeps: BackgroundUpdateCheckDeps = {
|
const defaultDeps: BackgroundUpdateCheckDeps = {
|
||||||
existsSync,
|
existsSync,
|
||||||
|
mkdirSync,
|
||||||
|
readFileSync,
|
||||||
|
writeFileSync,
|
||||||
|
dirname,
|
||||||
join,
|
join,
|
||||||
runBunInstallWithDetails,
|
runBunInstallWithDetails,
|
||||||
log,
|
log,
|
||||||
@@ -52,12 +66,54 @@ const defaultDeps: BackgroundUpdateCheckDeps = {
|
|||||||
syncCachePackageJsonToIntent,
|
syncCachePackageJsonToIntent,
|
||||||
showUpdateAvailableToast,
|
showUpdateAvailableToast,
|
||||||
showAutoUpdatedToast,
|
showAutoUpdatedToast,
|
||||||
|
now: () => Date.now(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LAST_UPDATED_MARKER_FILE = ".last-updated"
|
||||||
|
const RECENT_INSTALL_WINDOW_MS = 5 * 60 * 1000
|
||||||
|
|
||||||
function getPinnedVersionToastMessage(latestVersion: string): string {
|
function getPinnedVersionToastMessage(latestVersion: string): string {
|
||||||
return `Update available: ${latestVersion} (version pinned, update manually)`
|
return `Update available: ${latestVersion} (version pinned, update manually)`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getLastUpdatedMarkerPath(deps: BackgroundUpdateCheckDeps): string {
|
||||||
|
return deps.join(deps.getOpenCodeCacheDir(), LAST_UPDATED_MARKER_FILE)
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseLastUpdatedMarker(rawMarker: string): LastUpdatedMarker | null {
|
||||||
|
const marker: unknown = JSON.parse(rawMarker)
|
||||||
|
if (typeof marker !== "object" || marker === null) return null
|
||||||
|
if (!("version" in marker) || typeof marker.version !== "string") return null
|
||||||
|
if (!("updatedAt" in marker) || typeof marker.updatedAt !== "number") return null
|
||||||
|
return { version: marker.version, updatedAt: marker.updatedAt }
|
||||||
|
}
|
||||||
|
|
||||||
|
function wasRecentlyInstalled(version: string, deps: BackgroundUpdateCheckDeps): boolean {
|
||||||
|
try {
|
||||||
|
const markerPath = getLastUpdatedMarkerPath(deps)
|
||||||
|
if (!deps.existsSync(markerPath)) return false
|
||||||
|
|
||||||
|
const marker = parseLastUpdatedMarker(deps.readFileSync(markerPath, "utf-8"))
|
||||||
|
if (!marker) return false
|
||||||
|
if (marker.version !== version) return false
|
||||||
|
|
||||||
|
return deps.now() - marker.updatedAt < RECENT_INSTALL_WINDOW_MS
|
||||||
|
} catch (err) {
|
||||||
|
deps.log("[auto-update-checker] Failed to read last update marker:", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function writeLastUpdatedMarker(version: string, deps: BackgroundUpdateCheckDeps): void {
|
||||||
|
try {
|
||||||
|
const markerPath = getLastUpdatedMarkerPath(deps)
|
||||||
|
deps.mkdirSync(deps.dirname(markerPath), { recursive: true })
|
||||||
|
deps.writeFileSync(markerPath, JSON.stringify({ version, updatedAt: deps.now() }, null, 2))
|
||||||
|
} catch (err) {
|
||||||
|
deps.log("[auto-update-checker] Failed to write last update marker:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resolves the active install workspace.
|
* Resolves the active install workspace.
|
||||||
* Same logic as doctor check: prefer config-dir if installed, fall back to cache-dir.
|
* Same logic as doctor check: prefer config-dir if installed, fall back to cache-dir.
|
||||||
@@ -153,6 +209,11 @@ export function createBackgroundUpdateCheckRunner(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (wasRecentlyInstalled(latestVersion, deps)) {
|
||||||
|
deps.log(`[auto-update-checker] Skipping update check; ${latestVersion} was installed recently`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
deps.log(`[auto-update-checker] Update available (${channel}): ${currentVersion} → ${latestVersion}`)
|
deps.log(`[auto-update-checker] Update available (${channel}): ${currentVersion} → ${latestVersion}`)
|
||||||
|
|
||||||
if (!autoUpdate) {
|
if (!autoUpdate) {
|
||||||
@@ -186,6 +247,7 @@ export function createBackgroundUpdateCheckRunner(
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writeLastUpdatedMarker(latestVersion, deps)
|
||||||
await deps.showAutoUpdatedToast(ctx, currentVersion, latestVersion)
|
await deps.showAutoUpdatedToast(ctx, currentVersion, latestVersion)
|
||||||
deps.log(`[auto-update-checker] Update installed: ${currentVersion} → ${latestVersion}`)
|
deps.log(`[auto-update-checker] Update installed: ${currentVersion} → ${latestVersion}`)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import type { PluginInput } from "@opencode-ai/plugin"
|
import type { PluginInput } from "@opencode-ai/plugin"
|
||||||
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
|
import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test"
|
||||||
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs"
|
||||||
import { join } from "node:path"
|
import { join } from "node:path"
|
||||||
|
|
||||||
import type { PluginEntryInfo } from "../auto-update-checker/checker"
|
import type { PluginEntryInfo } from "../auto-update-checker/checker"
|
||||||
@@ -191,4 +191,25 @@ describe("workspace resolution", () => {
|
|||||||
expect(mockRunBunInstallWithDetails.mock.calls[0]?.[0]?.workspaceDir).toBe(TEST_CONFIG_DIR)
|
expect(mockRunBunInstallWithDetails.mock.calls[0]?.[0]?.workspaceDir).toBe(TEST_CONFIG_DIR)
|
||||||
expect(mockRunBunInstallWithDetails.mock.calls[1]?.[0]?.workspaceDir).toBe(TEST_CACHE_WORKSPACE_DIR)
|
expect(mockRunBunInstallWithDetails.mock.calls[1]?.[0]?.workspaceDir).toBe(TEST_CACHE_WORKSPACE_DIR)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
it("#given same version was just installed #when next startup checks again #then it skips reinstall loop", async () => {
|
||||||
|
// #given
|
||||||
|
const firstRun = await createRunner()
|
||||||
|
mkdirSync(TEST_CACHE_WORKSPACE_DIR, { recursive: true })
|
||||||
|
writeFileSync(join(TEST_CACHE_WORKSPACE_DIR, "package.json"), JSON.stringify({ dependencies: { [PACKAGE_NAME]: "3.4.0" } }, null, 2))
|
||||||
|
|
||||||
|
// #when
|
||||||
|
await firstRun(mockCtx, true, getToastMessage)
|
||||||
|
const markerPath = join(TEST_CACHE_DIR, ".last-updated")
|
||||||
|
const secondRun = await createRunner()
|
||||||
|
await secondRun(mockCtx, true, getToastMessage)
|
||||||
|
|
||||||
|
// #then
|
||||||
|
expect(readFileSync(markerPath, "utf-8")).toContain('"version": "3.5.0"')
|
||||||
|
expect(mockRunBunInstallWithDetails).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockSyncCachePackageJsonToIntent).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockInvalidatePackage).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockShowAutoUpdatedToast).toHaveBeenCalledTimes(1)
|
||||||
|
expect(mockShowUpdateAvailableToast).not.toHaveBeenCalled()
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user