- #3124: Session tools now merge SDK and file-backed sessions for SQLite backend - #3125: Cache priming fixed for OpenCode >=1.3.14 empty workspace - #3127: Activity-based progress detection prevents infinite compaction on Kimi/Minimax All 29 new tests pass, 4885 total tests passing.
This commit is contained in:
@@ -2,6 +2,7 @@ import { describe, test, expect, beforeEach, afterEach } from "bun:test"
|
||||
import * as fs from "node:fs"
|
||||
import * as path from "node:path"
|
||||
import * as os from "node:os"
|
||||
import { PACKAGE_NAME } from "../constants"
|
||||
import { updatePinnedVersion, revertPinnedVersion } from "./pinned-version-updater"
|
||||
|
||||
describe("pinned-version-updater", () => {
|
||||
@@ -21,18 +22,18 @@ describe("pinned-version-updater", () => {
|
||||
test("updates pinned version in config", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["oh-my-openagent@3.1.8"],
|
||||
plugin: [`${PACKAGE_NAME}@3.1.8`],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = updatePinnedVersion(configPath, "oh-my-openagent@3.1.8", "3.4.0")
|
||||
const result = updatePinnedVersion(configPath, `${PACKAGE_NAME}@3.1.8`, "3.4.0")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(true)
|
||||
const updated = fs.readFileSync(configPath, "utf-8")
|
||||
expect(updated).toContain("oh-my-openagent@3.4.0")
|
||||
expect(updated).not.toContain("oh-my-openagent@3.1.8")
|
||||
expect(updated).toContain(`${PACKAGE_NAME}@3.4.0`)
|
||||
expect(updated).not.toContain(`${PACKAGE_NAME}@3.1.8`)
|
||||
})
|
||||
|
||||
test("returns false when entry not found", () => {
|
||||
@@ -43,7 +44,7 @@ describe("pinned-version-updater", () => {
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = updatePinnedVersion(configPath, "oh-my-openagent@3.1.8", "3.4.0")
|
||||
const result = updatePinnedVersion(configPath, `${PACKAGE_NAME}@3.1.8`, "3.4.0")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(false)
|
||||
@@ -55,7 +56,7 @@ describe("pinned-version-updater", () => {
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = updatePinnedVersion(configPath, "oh-my-openagent@3.1.8", "3.4.0")
|
||||
const result = updatePinnedVersion(configPath, `${PACKAGE_NAME}@3.1.8`, "3.4.0")
|
||||
|
||||
//#then
|
||||
expect(result).toBe(false)
|
||||
@@ -66,46 +67,46 @@ describe("pinned-version-updater", () => {
|
||||
test("reverts from failed version back to original entry", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["oh-my-openagent@3.4.0"],
|
||||
plugin: [`${PACKAGE_NAME}@3.4.0`],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-openagent@3.1.8")
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", `${PACKAGE_NAME}@3.1.8`)
|
||||
|
||||
//#then
|
||||
expect(result).toBe(true)
|
||||
const reverted = fs.readFileSync(configPath, "utf-8")
|
||||
expect(reverted).toContain("oh-my-openagent@3.1.8")
|
||||
expect(reverted).not.toContain("oh-my-openagent@3.4.0")
|
||||
expect(reverted).toContain(`${PACKAGE_NAME}@3.1.8`)
|
||||
expect(reverted).not.toContain(`${PACKAGE_NAME}@3.4.0`)
|
||||
})
|
||||
|
||||
test("reverts to unpinned entry", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["oh-my-openagent@3.4.0"],
|
||||
plugin: [`${PACKAGE_NAME}@3.4.0`],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-openagent")
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", PACKAGE_NAME)
|
||||
|
||||
//#then
|
||||
expect(result).toBe(true)
|
||||
const reverted = fs.readFileSync(configPath, "utf-8")
|
||||
expect(reverted).toContain('"oh-my-openagent"')
|
||||
expect(reverted).not.toContain("oh-my-openagent@3.4.0")
|
||||
expect(reverted).toContain(`"${PACKAGE_NAME}"`)
|
||||
expect(reverted).not.toContain(`${PACKAGE_NAME}@3.4.0`)
|
||||
})
|
||||
|
||||
test("returns false when failed version not found", () => {
|
||||
//#given
|
||||
const config = JSON.stringify({
|
||||
plugin: ["oh-my-openagent@3.1.8"],
|
||||
plugin: [`${PACKAGE_NAME}@3.1.8`],
|
||||
})
|
||||
fs.writeFileSync(configPath, config)
|
||||
|
||||
//#when
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", "oh-my-openagent@3.1.8")
|
||||
const result = revertPinnedVersion(configPath, "3.4.0", `${PACKAGE_NAME}@3.1.8`)
|
||||
|
||||
//#then
|
||||
expect(result).toBe(false)
|
||||
@@ -116,18 +117,18 @@ describe("pinned-version-updater", () => {
|
||||
test("config returns to original state after update + revert", () => {
|
||||
//#given
|
||||
const originalConfig = JSON.stringify({
|
||||
plugin: ["oh-my-openagent@3.1.8"],
|
||||
plugin: [`${PACKAGE_NAME}@3.1.8`],
|
||||
})
|
||||
fs.writeFileSync(configPath, originalConfig)
|
||||
|
||||
//#when
|
||||
updatePinnedVersion(configPath, "oh-my-openagent@3.1.8", "3.4.0")
|
||||
revertPinnedVersion(configPath, "3.4.0", "oh-my-openagent@3.1.8")
|
||||
updatePinnedVersion(configPath, `${PACKAGE_NAME}@3.1.8`, "3.4.0")
|
||||
revertPinnedVersion(configPath, "3.4.0", `${PACKAGE_NAME}@3.1.8`)
|
||||
|
||||
//#then
|
||||
const finalConfig = fs.readFileSync(configPath, "utf-8")
|
||||
expect(finalConfig).toContain("oh-my-openagent@3.1.8")
|
||||
expect(finalConfig).not.toContain("oh-my-openagent@3.4.0")
|
||||
expect(finalConfig).toContain(`${PACKAGE_NAME}@3.1.8`)
|
||||
expect(finalConfig).not.toContain(`${PACKAGE_NAME}@3.4.0`)
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
@@ -2,6 +2,7 @@ import { afterEach, beforeEach, describe, expect, test } from "bun:test"
|
||||
import * as fs from "node:fs"
|
||||
import * as os from "node:os"
|
||||
import * as path from "node:path"
|
||||
import { PACKAGE_NAME } from "../constants"
|
||||
import { findPluginEntry } from "./plugin-entry"
|
||||
|
||||
describe("findPluginEntry", () => {
|
||||
@@ -21,7 +22,7 @@ describe("findPluginEntry", () => {
|
||||
|
||||
test("returns unpinned for bare package name", () => {
|
||||
// #given plugin is configured without a tag
|
||||
fs.writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent"] }))
|
||||
fs.writeFileSync(configPath, JSON.stringify({ plugin: [PACKAGE_NAME] }))
|
||||
|
||||
// #when plugin entry is detected
|
||||
const pluginInfo = findPluginEntry(temporaryDirectory)
|
||||
@@ -34,7 +35,7 @@ describe("findPluginEntry", () => {
|
||||
|
||||
test("returns unpinned for latest dist-tag", () => {
|
||||
// #given plugin is configured with latest dist-tag
|
||||
fs.writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent@latest"] }))
|
||||
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@latest`] }))
|
||||
|
||||
// #when plugin entry is detected
|
||||
const pluginInfo = findPluginEntry(temporaryDirectory)
|
||||
@@ -47,7 +48,7 @@ describe("findPluginEntry", () => {
|
||||
|
||||
test("returns unpinned for beta dist-tag", () => {
|
||||
// #given plugin is configured with beta dist-tag
|
||||
fs.writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent@beta"] }))
|
||||
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@beta`] }))
|
||||
|
||||
// #when plugin entry is detected
|
||||
const pluginInfo = findPluginEntry(temporaryDirectory)
|
||||
@@ -60,7 +61,7 @@ describe("findPluginEntry", () => {
|
||||
|
||||
test("returns pinned for explicit semver", () => {
|
||||
// #given plugin is configured with explicit version
|
||||
fs.writeFileSync(configPath, JSON.stringify({ plugin: ["oh-my-openagent@3.5.2"] }))
|
||||
fs.writeFileSync(configPath, JSON.stringify({ plugin: [`${PACKAGE_NAME}@3.5.2`] }))
|
||||
|
||||
// #when plugin entry is detected
|
||||
const pluginInfo = findPluginEntry(temporaryDirectory)
|
||||
|
||||
@@ -11,7 +11,7 @@ interface CachePackageJson {
|
||||
|
||||
export interface SyncResult {
|
||||
synced: boolean
|
||||
error: "file_not_found" | "plugin_not_in_deps" | "parse_error" | "write_error" | null
|
||||
error: "parse_error" | "write_error" | null
|
||||
message?: string
|
||||
}
|
||||
|
||||
@@ -32,12 +32,33 @@ function getIntentVersion(pluginInfo: PluginEntryInfo): string {
|
||||
return pluginInfo.pinnedVersion
|
||||
}
|
||||
|
||||
function writeCachePackageJson(
|
||||
cachePackageJsonPath: string,
|
||||
pkgJson: CachePackageJson,
|
||||
): SyncResult {
|
||||
const tmpPath = `${cachePackageJsonPath}.${crypto.randomUUID()}`
|
||||
try {
|
||||
fs.mkdirSync(path.dirname(cachePackageJsonPath), { recursive: true })
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(pkgJson, null, 2))
|
||||
fs.renameSync(tmpPath, cachePackageJsonPath)
|
||||
return { synced: true, error: null }
|
||||
} catch (err) {
|
||||
log("[auto-update-checker] Failed to write cache package.json:", err)
|
||||
safeUnlink(tmpPath)
|
||||
return { synced: false, error: "write_error", message: "Failed to write cache package.json" }
|
||||
}
|
||||
}
|
||||
|
||||
export function syncCachePackageJsonToIntent(pluginInfo: PluginEntryInfo): SyncResult {
|
||||
const cachePackageJsonPath = path.join(CACHE_DIR, "package.json")
|
||||
const intentVersion = getIntentVersion(pluginInfo)
|
||||
|
||||
if (!fs.existsSync(cachePackageJsonPath)) {
|
||||
log("[auto-update-checker] Cache package.json not found, nothing to sync")
|
||||
return { synced: false, error: "file_not_found", message: "Cache package.json not found" }
|
||||
log("[auto-update-checker] Cache package.json missing, creating workspace package.json", { intentVersion })
|
||||
return {
|
||||
...writeCachePackageJson(cachePackageJsonPath, { dependencies: { [PACKAGE_NAME]: intentVersion } }),
|
||||
message: `Created cache package.json with: ${intentVersion}`,
|
||||
}
|
||||
}
|
||||
|
||||
let content: string
|
||||
@@ -58,12 +79,21 @@ export function syncCachePackageJsonToIntent(pluginInfo: PluginEntryInfo): SyncR
|
||||
}
|
||||
|
||||
if (!pkgJson || !pkgJson.dependencies?.[PACKAGE_NAME]) {
|
||||
log("[auto-update-checker] Plugin not in cache package.json dependencies, nothing to sync")
|
||||
return { synced: false, error: "plugin_not_in_deps", message: "Plugin not in cache package.json dependencies" }
|
||||
log("[auto-update-checker] Plugin missing from cache package.json dependencies, adding dependency", { intentVersion })
|
||||
const nextPkgJson = {
|
||||
...(pkgJson ?? {}),
|
||||
dependencies: {
|
||||
...(pkgJson?.dependencies ?? {}),
|
||||
[PACKAGE_NAME]: intentVersion,
|
||||
},
|
||||
}
|
||||
return {
|
||||
...writeCachePackageJson(cachePackageJsonPath, nextPkgJson),
|
||||
message: `Added ${PACKAGE_NAME}: ${intentVersion}`,
|
||||
}
|
||||
}
|
||||
|
||||
const currentVersion = pkgJson.dependencies[PACKAGE_NAME]
|
||||
const intentVersion = getIntentVersion(pluginInfo)
|
||||
|
||||
if (currentVersion === intentVersion) {
|
||||
log("[auto-update-checker] Cache package.json already matches intent:", intentVersion)
|
||||
@@ -84,15 +114,8 @@ export function syncCachePackageJsonToIntent(pluginInfo: PluginEntryInfo): SyncR
|
||||
}
|
||||
|
||||
pkgJson.dependencies[PACKAGE_NAME] = intentVersion
|
||||
|
||||
const tmpPath = `${cachePackageJsonPath}.${crypto.randomUUID()}`
|
||||
try {
|
||||
fs.writeFileSync(tmpPath, JSON.stringify(pkgJson, null, 2))
|
||||
fs.renameSync(tmpPath, cachePackageJsonPath)
|
||||
return { synced: true, error: null, message: `Updated: "${currentVersion}" → "${intentVersion}"` }
|
||||
} catch (err) {
|
||||
log("[auto-update-checker] Failed to write cache package.json:", err)
|
||||
safeUnlink(tmpPath)
|
||||
return { synced: false, error: "write_error", message: "Failed to write cache package.json" }
|
||||
return {
|
||||
...writeCachePackageJson(cachePackageJsonPath, pkgJson),
|
||||
message: `Updated: "${currentVersion}" → "${intentVersion}"`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,9 +6,9 @@ describe("auto-update-checker constants", () => {
|
||||
it("uses the OpenCode cache directory for installed package metadata", async () => {
|
||||
const { CACHE_DIR, INSTALLED_PACKAGE_JSON, PACKAGE_NAME } = await import(`./constants?test=${Date.now()}`)
|
||||
|
||||
expect(CACHE_DIR).toBe(getOpenCodeCacheDir())
|
||||
expect(CACHE_DIR).toBe(join(getOpenCodeCacheDir(), "packages"))
|
||||
expect(INSTALLED_PACKAGE_JSON).toBe(
|
||||
join(getOpenCodeCacheDir(), "node_modules", PACKAGE_NAME, "package.json")
|
||||
join(getOpenCodeCacheDir(), "packages", "node_modules", PACKAGE_NAME, "package.json")
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -7,8 +7,9 @@ export const PACKAGE_NAME = "oh-my-openagent"
|
||||
export const NPM_REGISTRY_URL = `https://registry.npmjs.org/-/package/${PACKAGE_NAME}/dist-tags`
|
||||
export const NPM_FETCH_TIMEOUT = 5000
|
||||
|
||||
export const CACHE_DIR = getOpenCodeCacheDir()
|
||||
export const VERSION_FILE = path.join(CACHE_DIR, "version")
|
||||
export const CACHE_ROOT_DIR = getOpenCodeCacheDir()
|
||||
export const CACHE_DIR = path.join(CACHE_ROOT_DIR, "packages")
|
||||
export const VERSION_FILE = path.join(CACHE_ROOT_DIR, "version")
|
||||
|
||||
export function getWindowsAppdataDir(): string | null {
|
||||
if (process.platform !== "win32") return null
|
||||
|
||||
@@ -33,6 +33,10 @@ type BackgroundUpdateCheckRunner = (
|
||||
getToastMessage: (isUpdate: boolean, latestVersion?: string) => string,
|
||||
) => Promise<void>
|
||||
|
||||
function getCacheWorkspaceDir(deps: BackgroundUpdateCheckDeps): string {
|
||||
return deps.join(deps.getOpenCodeCacheDir(), "packages")
|
||||
}
|
||||
|
||||
const defaultDeps: BackgroundUpdateCheckDeps = {
|
||||
existsSync,
|
||||
join,
|
||||
@@ -60,7 +64,7 @@ function getPinnedVersionToastMessage(latestVersion: string): string {
|
||||
*/
|
||||
function resolveActiveInstallWorkspace(deps: BackgroundUpdateCheckDeps): string {
|
||||
const configPaths = deps.getOpenCodeConfigPaths({ binary: "opencode" })
|
||||
const cacheDir = deps.getOpenCodeCacheDir()
|
||||
const cacheDir = getCacheWorkspaceDir(deps)
|
||||
|
||||
const configInstallPath = deps.join(configPaths.configDir, "node_modules", PACKAGE_NAME, "package.json")
|
||||
const cacheInstallPath = deps.join(cacheDir, "node_modules", PACKAGE_NAME, "package.json")
|
||||
@@ -76,6 +80,12 @@ function resolveActiveInstallWorkspace(deps: BackgroundUpdateCheckDeps): string
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
const cachePackageJsonPath = deps.join(cacheDir, "package.json")
|
||||
if (deps.existsSync(cachePackageJsonPath)) {
|
||||
deps.log(`[auto-update-checker] Active workspace: cache-dir (${cacheDir}, package.json present)`)
|
||||
return cacheDir
|
||||
}
|
||||
|
||||
// Default to config-dir if neither exists (matches doctor behavior)
|
||||
deps.log(`[auto-update-checker] Active workspace: config-dir (default, no install detected)`)
|
||||
return configPaths.configDir
|
||||
@@ -95,6 +105,19 @@ async function runBunInstallSafe(workspaceDir: string, deps: BackgroundUpdateChe
|
||||
}
|
||||
}
|
||||
|
||||
async function primeCacheWorkspace(
|
||||
activeWorkspace: string,
|
||||
deps: BackgroundUpdateCheckDeps,
|
||||
): Promise<boolean> {
|
||||
const cacheWorkspace = getCacheWorkspaceDir(deps)
|
||||
if (activeWorkspace === cacheWorkspace) {
|
||||
return true
|
||||
}
|
||||
|
||||
deps.log(`[auto-update-checker] Priming cache workspace after install: ${cacheWorkspace}`)
|
||||
return runBunInstallSafe(cacheWorkspace, deps)
|
||||
}
|
||||
|
||||
export function createBackgroundUpdateCheckRunner(
|
||||
overrides: Partial<BackgroundUpdateCheckDeps> = {},
|
||||
): BackgroundUpdateCheckRunner {
|
||||
@@ -156,6 +179,13 @@ export function createBackgroundUpdateCheckRunner(
|
||||
const installSuccess = await runBunInstallSafe(activeWorkspace, deps)
|
||||
|
||||
if (installSuccess) {
|
||||
const cachePrimed = await primeCacheWorkspace(activeWorkspace, deps)
|
||||
if (!cachePrimed) {
|
||||
await deps.showUpdateAvailableToast(ctx, latestVersion, getToastMessage)
|
||||
deps.log("[auto-update-checker] cache workspace priming failed after install")
|
||||
return
|
||||
}
|
||||
|
||||
await deps.showAutoUpdatedToast(ctx, currentVersion, latestVersion)
|
||||
deps.log(`[auto-update-checker] Update installed: ${currentVersion} → ${latestVersion}`)
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user