From 2fb1604e3f79f1baa571a5e45536de502fe7dd58 Mon Sep 17 00:00:00 2001 From: YeonGyu-Kim Date: Sat, 4 Apr 2026 18:56:40 +0900 Subject: [PATCH] Remove isolated mock test directories (_auc-mocks-*) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Delete background-update-check, cache, hook, workspace-resolution, and sync-package-json isolated test directories - These have been renamed/reorganized to zauc-mocks-* pattern 🤖 GENERATED WITH ASSISTANCE OF OhMyOpenCode --- .../background-update-check.test.ts | 329 ----------------- src/hooks/_auc-mocks-cache/cache.test.ts | 102 ----- src/hooks/_auc-mocks-hook/hook.test.ts | 249 ------------- .../workspace-resolution.test.ts | 239 ------------ .../_auc-sync-mocks/sync-package-json.test.ts | 349 ------------------ 5 files changed, 1268 deletions(-) delete mode 100644 src/hooks/_auc-mocks-bg/background-update-check.test.ts delete mode 100644 src/hooks/_auc-mocks-cache/cache.test.ts delete mode 100644 src/hooks/_auc-mocks-hook/hook.test.ts delete mode 100644 src/hooks/_auc-mocks-ws/workspace-resolution.test.ts delete mode 100644 src/hooks/_auc-sync-mocks/sync-package-json.test.ts diff --git a/src/hooks/_auc-mocks-bg/background-update-check.test.ts b/src/hooks/_auc-mocks-bg/background-update-check.test.ts deleted file mode 100644 index a85be6637..000000000 --- a/src/hooks/_auc-mocks-bg/background-update-check.test.ts +++ /dev/null @@ -1,329 +0,0 @@ -import type { PluginInput } from "@opencode-ai/plugin" -import { beforeEach, describe, expect, it, mock } from "bun:test" - -type PluginEntry = { - entry: string - isPinned: boolean - pinnedVersion: string | null - configPath: string -} - -type ToastMessageGetter = (isUpdate: boolean, version?: string) => string - -function createPluginEntry(overrides?: Partial): PluginEntry { - return { - entry: "oh-my-opencode@3.4.0", - isPinned: false, - pinnedVersion: null, - configPath: "/test/opencode.json", - ...overrides, - } -} - -const mockFindPluginEntry = mock((_directory: string): PluginEntry | null => createPluginEntry()) -const mockGetCachedVersion = mock((): string | null => "3.4.0") -const mockGetLatestVersion = mock(async (): Promise => "3.5.0") -const mockExtractChannel = mock(() => "latest") -const mockInvalidatePackage = mock(() => {}) -const mockRunBunInstallWithDetails = mock(async () => ({ success: true })) -const mockShowUpdateAvailableToast = mock( - async (_ctx: PluginInput, _latestVersion: string, _getToastMessage: ToastMessageGetter): Promise => {} -) -const mockShowAutoUpdatedToast = mock( - async (_ctx: PluginInput, _fromVersion: string, _toVersion: string): Promise => {} -) - -const mockSyncCachePackageJsonToIntent = mock(() => false) - -let importCounter = 0 - -async function importFreshBackgroundUpdateCheck(): Promise { - mock.module("../auto-update-checker/checker", () => ({ - findPluginEntry: mockFindPluginEntry, - getCachedVersion: mockGetCachedVersion, - getLatestVersion: mockGetLatestVersion, - revertPinnedVersion: mock(() => false), - syncCachePackageJsonToIntent: mockSyncCachePackageJsonToIntent, - })) - mock.module("../auto-update-checker/version-channel", () => ({ extractChannel: mockExtractChannel })) - mock.module("../auto-update-checker/cache", () => ({ invalidatePackage: mockInvalidatePackage })) - mock.module("../../cli/config-manager", () => ({ runBunInstallWithDetails: mockRunBunInstallWithDetails })) - mock.module("../auto-update-checker/hook/update-toasts", () => ({ - showUpdateAvailableToast: mockShowUpdateAvailableToast, - showAutoUpdatedToast: mockShowAutoUpdatedToast, - })) - mock.module("../../shared/logger", () => ({ log: () => {} })) - - const backgroundUpdateCheckModule = await import(`../auto-update-checker/hook/background-update-check?test=${importCounter++}`) - mock.restore() - return backgroundUpdateCheckModule -} - -describe("runBackgroundUpdateCheck", () => { - const mockCtx = { directory: "/test" } as PluginInput - const getToastMessage: ToastMessageGetter = (isUpdate, version) => - isUpdate ? `Update to ${version}` : "Up to date" - - beforeEach(() => { - mockFindPluginEntry.mockReset() - mockGetCachedVersion.mockReset() - mockGetLatestVersion.mockReset() - mockExtractChannel.mockReset() - mockInvalidatePackage.mockReset() - mockRunBunInstallWithDetails.mockReset() - mockShowUpdateAvailableToast.mockReset() - mockShowAutoUpdatedToast.mockReset() - mockSyncCachePackageJsonToIntent.mockReset() - - mockFindPluginEntry.mockReturnValue(createPluginEntry()) - mockGetCachedVersion.mockReturnValue("3.4.0") - mockGetLatestVersion.mockResolvedValue("3.5.0") - mockExtractChannel.mockReturnValue("latest") - mockRunBunInstallWithDetails.mockResolvedValue({ success: true }) - mockSyncCachePackageJsonToIntent.mockReturnValue({ synced: true, error: null }) - }) - - describe("#given no plugin entry found", () => { - it("returns early without showing any toast", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockFindPluginEntry.mockReturnValue(null) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockFindPluginEntry).toHaveBeenCalledTimes(1) - expect(mockShowUpdateAvailableToast).not.toHaveBeenCalled() - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - }) - }) - - describe("#given no version available", () => { - it("returns early when neither cached nor pinned version exists", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockFindPluginEntry.mockReturnValue(createPluginEntry({ entry: "oh-my-opencode" })) - mockGetCachedVersion.mockReturnValue(null) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockGetCachedVersion).toHaveBeenCalledTimes(1) - expect(mockGetLatestVersion).not.toHaveBeenCalled() - expect(mockShowUpdateAvailableToast).not.toHaveBeenCalled() - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) - - describe("#given latest version fetch fails", () => { - it("returns early without toasts", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockGetLatestVersion.mockResolvedValue(null) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockGetLatestVersion).toHaveBeenCalledWith("latest") - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - expect(mockShowUpdateAvailableToast).not.toHaveBeenCalled() - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) - - describe("#given already on latest version", () => { - it("returns early without any action", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockGetCachedVersion.mockReturnValue("3.4.0") - mockGetLatestVersion.mockResolvedValue("3.4.0") - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockGetLatestVersion).toHaveBeenCalledTimes(1) - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - expect(mockShowUpdateAvailableToast).not.toHaveBeenCalled() - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) - - describe("#given update available with autoUpdate disabled", () => { - it("shows update notification but does not install", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - const autoUpdate = false - //#when - await runBackgroundUpdateCheck(mockCtx, autoUpdate, getToastMessage) - //#then - expect(mockShowUpdateAvailableToast).toHaveBeenCalledWith(mockCtx, "3.5.0", getToastMessage) - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) - - describe("#given user has pinned a specific version", () => { - it("shows pinned-version toast without auto-updating", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockFindPluginEntry.mockReturnValue(createPluginEntry({ isPinned: true, pinnedVersion: "3.4.0" })) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockShowUpdateAvailableToast).toHaveBeenCalledTimes(1) - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - - it("toast message mentions version pinned", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - let capturedToastMessage: ToastMessageGetter | undefined - mockFindPluginEntry.mockReturnValue(createPluginEntry({ isPinned: true, pinnedVersion: "3.4.0" })) - mockShowUpdateAvailableToast.mockImplementation( - async (_ctx: PluginInput, _latestVersion: string, toastMessage: ToastMessageGetter) => { - capturedToastMessage = toastMessage - } - ) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockShowUpdateAvailableToast).toHaveBeenCalledTimes(1) - expect(capturedToastMessage).toBeDefined() - if (!capturedToastMessage) { - throw new Error("toast message callback missing") - } - const message = capturedToastMessage(true, "3.5.0") - expect(message).toContain("version pinned") - expect(message).not.toBe("Update to 3.5.0") - }) - }) - - describe("#given unpinned with auto-update and install succeeds", () => { - it("syncs cache, invalidates, installs, and shows auto-updated toast", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockRunBunInstallWithDetails.mockResolvedValue({ success: true }) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockSyncCachePackageJsonToIntent).toHaveBeenCalledTimes(1) - expect(mockInvalidatePackage).toHaveBeenCalledTimes(1) - expect(mockRunBunInstallWithDetails).toHaveBeenCalledTimes(1) - expect(mockShowAutoUpdatedToast).toHaveBeenCalledWith(mockCtx, "3.4.0", "3.5.0") - expect(mockShowUpdateAvailableToast).not.toHaveBeenCalled() - }) - - it("syncs before invalidate and install (correct order)", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - const callOrder: string[] = [] - mockSyncCachePackageJsonToIntent.mockImplementation(() => { - callOrder.push("sync") - return { synced: true, error: null } - }) - mockInvalidatePackage.mockImplementation(() => { - callOrder.push("invalidate") - }) - mockRunBunInstallWithDetails.mockImplementation(async () => { - callOrder.push("install") - return { success: true } - }) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(callOrder).toEqual(["sync", "invalidate", "install"]) - }) - }) - - describe("#given unpinned with auto-update and install fails", () => { - it("falls back to notification-only toast", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockRunBunInstallWithDetails.mockResolvedValue({ success: false }) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockRunBunInstallWithDetails).toHaveBeenCalledTimes(1) - expect(mockShowUpdateAvailableToast).toHaveBeenCalledWith(mockCtx, "3.5.0", getToastMessage) - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) - - describe("#given sync fails with file_not_found", () => { - it("aborts update and shows notification-only toast", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockSyncCachePackageJsonToIntent.mockReturnValue({ - synced: false, - error: "file_not_found", - message: "Cache package.json not found", - }) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockSyncCachePackageJsonToIntent).toHaveBeenCalledTimes(1) - expect(mockInvalidatePackage).not.toHaveBeenCalled() - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - expect(mockShowUpdateAvailableToast).toHaveBeenCalledWith(mockCtx, "3.5.0", getToastMessage) - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) - - describe("#given sync fails with plugin_not_in_deps", () => { - it("aborts update and shows notification-only toast", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockSyncCachePackageJsonToIntent.mockReturnValue({ - synced: false, - error: "plugin_not_in_deps", - message: "Plugin not in cache package.json dependencies", - }) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockSyncCachePackageJsonToIntent).toHaveBeenCalledTimes(1) - expect(mockInvalidatePackage).not.toHaveBeenCalled() - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - expect(mockShowUpdateAvailableToast).toHaveBeenCalledWith(mockCtx, "3.5.0", getToastMessage) - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) - - describe("#given sync fails with parse_error", () => { - it("aborts update and shows notification-only toast", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockSyncCachePackageJsonToIntent.mockReturnValue({ - synced: false, - error: "parse_error", - message: "Failed to parse cache package.json (malformed JSON)", - }) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockSyncCachePackageJsonToIntent).toHaveBeenCalledTimes(1) - expect(mockInvalidatePackage).not.toHaveBeenCalled() - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - expect(mockShowUpdateAvailableToast).toHaveBeenCalledWith(mockCtx, "3.5.0", getToastMessage) - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) - - describe("#given sync fails with write_error", () => { - it("aborts update and shows notification-only toast", async () => { - //#given - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mockSyncCachePackageJsonToIntent.mockReturnValue({ - synced: false, - error: "write_error", - message: "Failed to write cache package.json", - }) - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - //#then - expect(mockSyncCachePackageJsonToIntent).toHaveBeenCalledTimes(1) - expect(mockInvalidatePackage).not.toHaveBeenCalled() - expect(mockRunBunInstallWithDetails).not.toHaveBeenCalled() - expect(mockShowUpdateAvailableToast).toHaveBeenCalledWith(mockCtx, "3.5.0", getToastMessage) - expect(mockShowAutoUpdatedToast).not.toHaveBeenCalled() - }) - }) -}) diff --git a/src/hooks/_auc-mocks-cache/cache.test.ts b/src/hooks/_auc-mocks-cache/cache.test.ts deleted file mode 100644 index 1ad5b8285..000000000 --- a/src/hooks/_auc-mocks-cache/cache.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" -import { join } from "node:path" - -const TEST_CACHE_DIR = join(import.meta.dir, "__test-cache__") -const TEST_OPENCODE_CACHE_DIR = join(TEST_CACHE_DIR, "opencode") -const TEST_USER_CONFIG_DIR = "/tmp/opencode-config" - -let importCounter = 0 - -async function importFreshCacheModule(): Promise { - mock.module("../auto-update-checker/constants", () => ({ - CACHE_DIR: TEST_OPENCODE_CACHE_DIR, - USER_CONFIG_DIR: TEST_USER_CONFIG_DIR, - PACKAGE_NAME: "oh-my-opencode", - NPM_REGISTRY_URL: "https://registry.npmjs.org/-/package/oh-my-opencode/dist-tags", - NPM_FETCH_TIMEOUT: 5000, - VERSION_FILE: join(TEST_OPENCODE_CACHE_DIR, "version"), - USER_OPENCODE_CONFIG: join(TEST_USER_CONFIG_DIR, "opencode.json"), - USER_OPENCODE_CONFIG_JSONC: join(TEST_USER_CONFIG_DIR, "opencode.jsonc"), - INSTALLED_PACKAGE_JSON: join(TEST_OPENCODE_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"), - getWindowsAppdataDir: () => null, - })) - - mock.module("../../shared/logger", () => ({ - log: () => {}, - })) - - const cacheModule = await import(`../auto-update-checker/cache?test=${importCounter++}`) - mock.restore() - return cacheModule -} - -function resetTestCache(): void { - if (existsSync(TEST_CACHE_DIR)) { - rmSync(TEST_CACHE_DIR, { recursive: true, force: true }) - } - - mkdirSync(join(TEST_OPENCODE_CACHE_DIR, "node_modules", "oh-my-opencode"), { recursive: true }) - writeFileSync( - join(TEST_OPENCODE_CACHE_DIR, "package.json"), - JSON.stringify({ dependencies: { "oh-my-opencode": "latest", other: "1.0.0" } }, null, 2) - ) - writeFileSync( - join(TEST_OPENCODE_CACHE_DIR, "bun.lock"), - JSON.stringify( - { - workspaces: { - "": { - dependencies: { "oh-my-opencode": "latest", other: "1.0.0" }, - }, - }, - packages: { - "oh-my-opencode": {}, - other: {}, - }, - }, - null, - 2 - ) - ) - writeFileSync( - join(TEST_OPENCODE_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"), - '{"name":"oh-my-opencode"}' - ) -} - -describe("invalidatePackage", () => { - beforeEach(() => { - resetTestCache() - }) - - afterEach(() => { - if (existsSync(TEST_CACHE_DIR)) { - rmSync(TEST_CACHE_DIR, { recursive: true, force: true }) - } - }) - - it("invalidates the installed package from the OpenCode cache directory", async () => { - const { invalidatePackage } = await importFreshCacheModule() - - const result = invalidatePackage() - - expect(result).toBe(true) - expect(existsSync(join(TEST_OPENCODE_CACHE_DIR, "node_modules", "oh-my-opencode"))).toBe(false) - - const packageJson = JSON.parse(readFileSync(join(TEST_OPENCODE_CACHE_DIR, "package.json"), "utf-8")) as { - dependencies?: Record - } - expect(packageJson.dependencies?.["oh-my-opencode"]).toBe("latest") - expect(packageJson.dependencies?.other).toBe("1.0.0") - - const bunLock = JSON.parse(readFileSync(join(TEST_OPENCODE_CACHE_DIR, "bun.lock"), "utf-8")) as { - workspaces?: { ""?: { dependencies?: Record } } - packages?: Record - } - expect(bunLock.workspaces?.[""]?.dependencies?.["oh-my-opencode"]).toBe("latest") - expect(bunLock.workspaces?.[""]?.dependencies?.other).toBe("1.0.0") - expect(bunLock.packages?.["oh-my-opencode"]).toBeUndefined() - expect(bunLock.packages?.other).toEqual({}) - }) -}) diff --git a/src/hooks/_auc-mocks-hook/hook.test.ts b/src/hooks/_auc-mocks-hook/hook.test.ts deleted file mode 100644 index 5a673f88c..000000000 --- a/src/hooks/_auc-mocks-hook/hook.test.ts +++ /dev/null @@ -1,249 +0,0 @@ -import { afterAll, afterEach, beforeEach, describe, expect, it, mock } from "bun:test" - -const mockShowConfigErrorsIfAny = mock(async () => {}) -const mockShowModelCacheWarningIfNeeded = mock(async () => {}) -const mockUpdateAndShowConnectedProvidersCacheStatus = mock(async () => {}) -const mockRefreshModelCapabilitiesOnStartup = mock(async () => {}) -const mockShowLocalDevToast = mock(async () => {}) -const mockShowVersionToast = mock(async () => {}) -const mockRunBackgroundUpdateCheck = mock(async () => {}) -const mockGetCachedVersion = mock(() => "3.6.0") -const mockGetLocalDevVersion = mock<(directory: string) => string | null>(() => null) - -mock.module("../auto-update-checker/hook/config-errors-toast", () => ({ - showConfigErrorsIfAny: mockShowConfigErrorsIfAny, -})) - -mock.module("../auto-update-checker/hook/model-cache-warning", () => ({ - showModelCacheWarningIfNeeded: mockShowModelCacheWarningIfNeeded, -})) - -mock.module("../auto-update-checker/hook/connected-providers-status", () => ({ - updateAndShowConnectedProvidersCacheStatus: - mockUpdateAndShowConnectedProvidersCacheStatus, -})) - -mock.module("../auto-update-checker/hook/model-capabilities-status", () => ({ - refreshModelCapabilitiesOnStartup: mockRefreshModelCapabilitiesOnStartup, -})) - -mock.module("../auto-update-checker/hook/startup-toasts", () => ({ - showLocalDevToast: mockShowLocalDevToast, - showVersionToast: mockShowVersionToast, -})) - -mock.module("../auto-update-checker/hook/background-update-check", () => ({ - runBackgroundUpdateCheck: mockRunBackgroundUpdateCheck, -})) - -mock.module("../auto-update-checker/checker", () => ({ - getCachedVersion: mockGetCachedVersion, - getLocalDevVersion: mockGetLocalDevVersion, -})) - -mock.module("../../shared/logger", () => ({ - log: () => {}, -})) - -afterAll(() => { - mock.restore() -}) - -type HookFactory = typeof import("../auto-update-checker/hook").createAutoUpdateCheckerHook - -async function importFreshHookFactory(): Promise { - const hookModule = await import(`../auto-update-checker/hook?test-${Date.now()}-${Math.random()}`) - return hookModule.createAutoUpdateCheckerHook -} - -function createPluginInput() { - return { - directory: "/test", - client: {} as never, - } as never -} - -async function flushScheduledWork(): Promise { - await new Promise((resolve) => { - setTimeout(resolve, 0) - }) - await Promise.resolve() - await Promise.resolve() -} - -function runSessionCreatedEvent( - hook: ReturnType, - properties?: { info?: { parentID?: string } } -): void { - hook.event({ - event: { - type: "session.created", - properties, - }, - }) -} - -beforeEach(() => { - mockShowConfigErrorsIfAny.mockClear() - mockShowModelCacheWarningIfNeeded.mockClear() - mockUpdateAndShowConnectedProvidersCacheStatus.mockClear() - mockRefreshModelCapabilitiesOnStartup.mockClear() - mockShowLocalDevToast.mockClear() - mockShowVersionToast.mockClear() - mockRunBackgroundUpdateCheck.mockClear() - mockGetCachedVersion.mockClear() - mockGetLocalDevVersion.mockClear() - - mockGetCachedVersion.mockReturnValue("3.6.0") - mockGetLocalDevVersion.mockReturnValue(null) -}) - -afterEach(() => { - delete process.env.OPENCODE_CLI_RUN_MODE -}) - -describe("createAutoUpdateCheckerHook", () => { - it("skips startup toasts and checks in CLI run mode", async () => { - //#given - CLI run mode enabled - process.env.OPENCODE_CLI_RUN_MODE = "true" - const createAutoUpdateCheckerHook = await importFreshHookFactory() - - const hook = createAutoUpdateCheckerHook(createPluginInput(), { - showStartupToast: true, - isSisyphusEnabled: true, - autoUpdate: true, - }) - - //#when - session.created event arrives - runSessionCreatedEvent(hook, { info: { parentID: undefined } }) - await flushScheduledWork() - - //#then - no update checker side effects run - expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled() - expect(mockShowModelCacheWarningIfNeeded).not.toHaveBeenCalled() - expect(mockUpdateAndShowConnectedProvidersCacheStatus).not.toHaveBeenCalled() - expect(mockRefreshModelCapabilitiesOnStartup).not.toHaveBeenCalled() - expect(mockShowLocalDevToast).not.toHaveBeenCalled() - expect(mockShowVersionToast).not.toHaveBeenCalled() - expect(mockRunBackgroundUpdateCheck).not.toHaveBeenCalled() - }) - - it("runs all startup checks on normal session.created", async () => { - //#given - normal mode and no local dev version - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) - - //#when - session.created event arrives on primary session - runSessionCreatedEvent(hook) - await flushScheduledWork() - - //#then - startup checks, toast, and background check run - expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1) - expect(mockUpdateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1) - expect(mockRefreshModelCapabilitiesOnStartup).toHaveBeenCalledTimes(1) - expect(mockShowModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1) - expect(mockShowVersionToast).toHaveBeenCalledTimes(1) - expect(mockRunBackgroundUpdateCheck).toHaveBeenCalledTimes(1) - }) - - it("ignores subagent sessions (parentID present)", async () => { - //#given - a subagent session with parentID - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) - - //#when - session.created event contains parentID - runSessionCreatedEvent(hook, { info: { parentID: "parent-123" } }) - await flushScheduledWork() - - //#then - no startup actions run - expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled() - expect(mockUpdateAndShowConnectedProvidersCacheStatus).not.toHaveBeenCalled() - expect(mockRefreshModelCapabilitiesOnStartup).not.toHaveBeenCalled() - expect(mockShowModelCacheWarningIfNeeded).not.toHaveBeenCalled() - expect(mockShowLocalDevToast).not.toHaveBeenCalled() - expect(mockShowVersionToast).not.toHaveBeenCalled() - expect(mockRunBackgroundUpdateCheck).not.toHaveBeenCalled() - }) - - it("runs only once (hasChecked guard)", async () => { - //#given - one hook instance in normal mode - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) - - //#when - session.created event is fired twice - runSessionCreatedEvent(hook) - runSessionCreatedEvent(hook) - await flushScheduledWork() - - //#then - side effects execute only once - expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1) - expect(mockUpdateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1) - expect(mockRefreshModelCapabilitiesOnStartup).toHaveBeenCalledTimes(1) - expect(mockShowModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1) - expect(mockShowVersionToast).toHaveBeenCalledTimes(1) - expect(mockRunBackgroundUpdateCheck).toHaveBeenCalledTimes(1) - }) - - it("shows localDevToast when local dev version exists", async () => { - //#given - local dev version is present - mockGetLocalDevVersion.mockReturnValue("3.6.0-dev") - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) - - //#when - session.created event arrives - runSessionCreatedEvent(hook) - await flushScheduledWork() - - //#then - local dev toast is shown and background check is skipped - expect(mockShowConfigErrorsIfAny).toHaveBeenCalledTimes(1) - expect(mockUpdateAndShowConnectedProvidersCacheStatus).toHaveBeenCalledTimes(1) - expect(mockRefreshModelCapabilitiesOnStartup).toHaveBeenCalledTimes(1) - expect(mockShowModelCacheWarningIfNeeded).toHaveBeenCalledTimes(1) - expect(mockShowLocalDevToast).toHaveBeenCalledTimes(1) - expect(mockShowVersionToast).not.toHaveBeenCalled() - expect(mockRunBackgroundUpdateCheck).not.toHaveBeenCalled() - }) - - it("ignores non-session.created events", async () => { - //#given - a hook instance in normal mode - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput()) - - //#when - a non-session.created event arrives - hook.event({ - event: { - type: "session.deleted", - }, - }) - await flushScheduledWork() - - //#then - no startup actions run - expect(mockShowConfigErrorsIfAny).not.toHaveBeenCalled() - expect(mockUpdateAndShowConnectedProvidersCacheStatus).not.toHaveBeenCalled() - expect(mockRefreshModelCapabilitiesOnStartup).not.toHaveBeenCalled() - expect(mockShowModelCacheWarningIfNeeded).not.toHaveBeenCalled() - expect(mockShowLocalDevToast).not.toHaveBeenCalled() - expect(mockShowVersionToast).not.toHaveBeenCalled() - expect(mockRunBackgroundUpdateCheck).not.toHaveBeenCalled() - }) - - it("passes correct toast message with sisyphus enabled", async () => { - //#given - sisyphus mode enabled - const createAutoUpdateCheckerHook = await importFreshHookFactory() - const hook = createAutoUpdateCheckerHook(createPluginInput(), { - isSisyphusEnabled: true, - }) - - //#when - session.created event arrives - runSessionCreatedEvent(hook) - await flushScheduledWork() - - //#then - startup toast includes sisyphus wording - expect(mockShowVersionToast).toHaveBeenCalledTimes(1) - expect(mockShowVersionToast).toHaveBeenCalledWith( - expect.anything(), - "3.6.0", - expect.stringContaining("Sisyphus") - ) - }) -}) diff --git a/src/hooks/_auc-mocks-ws/workspace-resolution.test.ts b/src/hooks/_auc-mocks-ws/workspace-resolution.test.ts deleted file mode 100644 index bf459655d..000000000 --- a/src/hooks/_auc-mocks-ws/workspace-resolution.test.ts +++ /dev/null @@ -1,239 +0,0 @@ -import type { PluginInput } from "@opencode-ai/plugin" -import { afterEach, beforeEach, describe, expect, it, mock, spyOn } from "bun:test" -import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs" -import { join } from "node:path" -import * as shared from "../../shared" - -type PluginEntry = { - entry: string - isPinned: boolean - pinnedVersion: string | null - configPath: string -} - -type ToastMessageGetter = (isUpdate: boolean, version?: string) => string - -function createPluginEntry(overrides?: Partial): PluginEntry { - return { - entry: "oh-my-opencode@3.4.0", - isPinned: false, - pinnedVersion: null, - configPath: "/test/opencode.json", - ...overrides, - } -} - -const TEST_DIR = join(import.meta.dir, "__test-workspace-resolution__") -const TEST_CACHE_DIR = join(TEST_DIR, "cache") -const TEST_CONFIG_DIR = join(TEST_DIR, "config") - -const mockFindPluginEntry = mock((_directory: string): PluginEntry | null => createPluginEntry()) -const mockGetCachedVersion = mock((): string | null => "3.4.0") -const mockGetLatestVersion = mock(async (): Promise => "3.5.0") -const mockExtractChannel = mock(() => "latest") -const mockInvalidatePackage = mock(() => {}) -const mockShowUpdateAvailableToast = mock( - async (_ctx: PluginInput, _latestVersion: string, _getToastMessage: ToastMessageGetter): Promise => {} -) -const mockShowAutoUpdatedToast = mock( - async (_ctx: PluginInput, _fromVersion: string, _toVersion: string): Promise => {} -) -const mockSyncCachePackageJsonToIntent = mock(() => ({ synced: true, error: null })) - -const mockRunBunInstallWithDetails = mock( - async (opts?: { outputMode?: string; workspaceDir?: string }) => { - return { success: true } - } -) - -let importCounter = 0 -let getOpenCodeCacheDirSpy: { mockRestore: () => void } | undefined -let getOpenCodeConfigPathsSpy: { mockRestore: () => void } | undefined - -async function importFreshBackgroundUpdateCheck(): Promise { - mock.module("../auto-update-checker/checker", () => ({ - findPluginEntry: mockFindPluginEntry, - getCachedVersion: mockGetCachedVersion, - getLatestVersion: mockGetLatestVersion, - revertPinnedVersion: mock(() => false), - syncCachePackageJsonToIntent: mockSyncCachePackageJsonToIntent, - })) - mock.module("../auto-update-checker/version-channel", () => ({ extractChannel: mockExtractChannel })) - mock.module("../auto-update-checker/cache", () => ({ invalidatePackage: mockInvalidatePackage })) - mock.module("../../cli/config-manager", () => ({ - runBunInstallWithDetails: mockRunBunInstallWithDetails, - })) - mock.module("../auto-update-checker/hook/update-toasts", () => ({ - showUpdateAvailableToast: mockShowUpdateAvailableToast, - showAutoUpdatedToast: mockShowAutoUpdatedToast, - })) - mock.module("../../shared/logger", () => ({ log: () => {} })) - getOpenCodeCacheDirSpy = spyOn(shared, "getOpenCodeCacheDir").mockReturnValue(TEST_CACHE_DIR) - getOpenCodeConfigPathsSpy = spyOn(shared, "getOpenCodeConfigPaths").mockReturnValue({ - configDir: TEST_CONFIG_DIR, - configJson: join(TEST_CONFIG_DIR, "opencode.json"), - configJsonc: join(TEST_CONFIG_DIR, "opencode.jsonc"), - packageJson: join(TEST_CONFIG_DIR, "package.json"), - omoConfig: join(TEST_CONFIG_DIR, "oh-my-opencode.json"), - }) - - mock.module("../auto-update-checker/constants", () => ({ - PACKAGE_NAME: "oh-my-opencode", - CACHE_DIR: TEST_CACHE_DIR, - USER_CONFIG_DIR: TEST_CONFIG_DIR, - NPM_REGISTRY_URL: "https://registry.npmjs.org/-/package/oh-my-opencode/dist-tags", - NPM_FETCH_TIMEOUT: 5000, - VERSION_FILE: join(TEST_CACHE_DIR, "version"), - USER_OPENCODE_CONFIG: join(TEST_CONFIG_DIR, "opencode.json"), - USER_OPENCODE_CONFIG_JSONC: join(TEST_CONFIG_DIR, "opencode.jsonc"), - INSTALLED_PACKAGE_JSON: join(TEST_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"), - getWindowsAppdataDir: () => null, - })) - - mock.module("../../shared/data-path", () => ({ - getDataDir: () => join(TEST_DIR, "data"), - getOpenCodeStorageDir: () => join(TEST_DIR, "data", "opencode", "storage"), - getCacheDir: () => TEST_DIR, - getOmoOpenCodeCacheDir: () => join(TEST_DIR, "oh-my-opencode"), - getOpenCodeCacheDir: () => TEST_CACHE_DIR, - })) - mock.module("../../shared/opencode-config-dir", () => ({ - getOpenCodeConfigDir: () => TEST_CONFIG_DIR, - getOpenCodeConfigPaths: () => ({ - configDir: TEST_CONFIG_DIR, - configJson: join(TEST_CONFIG_DIR, "opencode.json"), - configJsonc: join(TEST_CONFIG_DIR, "opencode.jsonc"), - packageJson: join(TEST_CONFIG_DIR, "package.json"), - omoConfig: join(TEST_CONFIG_DIR, "oh-my-opencode.json"), - }), - })) - - const backgroundUpdateCheckModule = await import(`../auto-update-checker/hook/background-update-check?test=${importCounter++}`) - return backgroundUpdateCheckModule -} - -describe("workspace resolution", () => { - const mockCtx = { directory: "/test" } as PluginInput - const getToastMessage: ToastMessageGetter = (isUpdate, version) => - isUpdate ? `Update to ${version}` : "Up to date" - - beforeEach(() => { - // Setup test directories - if (existsSync(TEST_DIR)) { - rmSync(TEST_DIR, { recursive: true, force: true }) - } - mkdirSync(TEST_DIR, { recursive: true }) - - mockFindPluginEntry.mockReset() - mockGetCachedVersion.mockReset() - mockGetLatestVersion.mockReset() - mockExtractChannel.mockReset() - mockInvalidatePackage.mockReset() - mockRunBunInstallWithDetails.mockReset() - mockShowUpdateAvailableToast.mockReset() - mockShowAutoUpdatedToast.mockReset() - - mockFindPluginEntry.mockReturnValue(createPluginEntry()) - mockGetCachedVersion.mockReturnValue("3.4.0") - mockGetLatestVersion.mockResolvedValue("3.5.0") - mockExtractChannel.mockReturnValue("latest") - // Note: Don't use mockResolvedValue here - it overrides the function that captures args - mockSyncCachePackageJsonToIntent.mockReturnValue({ synced: true, error: null }) - }) - - afterEach(() => { - getOpenCodeCacheDirSpy?.mockRestore() - getOpenCodeConfigPathsSpy?.mockRestore() - getOpenCodeCacheDirSpy = undefined - getOpenCodeConfigPathsSpy = undefined - mock.restore() - if (existsSync(TEST_DIR)) { - rmSync(TEST_DIR, { recursive: true, force: true }) - } - }) - - describe("#given config-dir install exists but cache-dir does not", () => { - it("installs to config-dir, not cache-dir", async () => { - //#given - config-dir has installation, cache-dir does not - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mkdirSync(join(TEST_CONFIG_DIR, "node_modules", "oh-my-opencode"), { recursive: true }) - writeFileSync( - join(TEST_CONFIG_DIR, "package.json"), - JSON.stringify({ dependencies: { "oh-my-opencode": "3.4.0" } }, null, 2) - ) - writeFileSync( - join(TEST_CONFIG_DIR, "node_modules", "oh-my-opencode", "package.json"), - JSON.stringify({ name: "oh-my-opencode", version: "3.4.0" }, null, 2) - ) - - // cache-dir should NOT exist - expect(existsSync(TEST_CACHE_DIR)).toBe(false) - - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - - //#then - install should be called with config-dir - const mockCalls = mockRunBunInstallWithDetails.mock.calls - expect(mockCalls[0][0]?.workspaceDir).toBe(TEST_CONFIG_DIR) - }) - }) - - describe("#given both config-dir and cache-dir exist", () => { - it("prefers config-dir over cache-dir", async () => { - //#given - both directories have installations - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mkdirSync(join(TEST_CONFIG_DIR, "node_modules", "oh-my-opencode"), { recursive: true }) - writeFileSync( - join(TEST_CONFIG_DIR, "package.json"), - JSON.stringify({ dependencies: { "oh-my-opencode": "3.4.0" } }, null, 2) - ) - writeFileSync( - join(TEST_CONFIG_DIR, "node_modules", "oh-my-opencode", "package.json"), - JSON.stringify({ name: "oh-my-opencode", version: "3.4.0" }, null, 2) - ) - - mkdirSync(join(TEST_CACHE_DIR, "node_modules", "oh-my-opencode"), { recursive: true }) - writeFileSync( - join(TEST_CACHE_DIR, "package.json"), - JSON.stringify({ dependencies: { "oh-my-opencode": "3.4.0" } }, null, 2) - ) - writeFileSync( - join(TEST_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"), - JSON.stringify({ name: "oh-my-opencode", version: "3.4.0" }, null, 2) - ) - - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - - //#then - install should prefer config-dir - const mockCalls2 = mockRunBunInstallWithDetails.mock.calls - expect(mockCalls2[0][0]?.workspaceDir).toBe(TEST_CONFIG_DIR) - }) - }) - - describe("#given only cache-dir install exists", () => { - it("falls back to cache-dir", async () => { - //#given - only cache-dir has installation - const { runBackgroundUpdateCheck } = await importFreshBackgroundUpdateCheck() - mkdirSync(join(TEST_CACHE_DIR, "node_modules", "oh-my-opencode"), { recursive: true }) - writeFileSync( - join(TEST_CACHE_DIR, "package.json"), - JSON.stringify({ dependencies: { "oh-my-opencode": "3.4.0" } }, null, 2) - ) - writeFileSync( - join(TEST_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"), - JSON.stringify({ name: "oh-my-opencode", version: "3.4.0" }, null, 2) - ) - - // config-dir should NOT exist - expect(existsSync(TEST_CONFIG_DIR)).toBe(false) - - //#when - await runBackgroundUpdateCheck(mockCtx, true, getToastMessage) - - //#then - install should fall back to cache-dir - const mockCalls3 = mockRunBunInstallWithDetails.mock.calls - expect(mockCalls3[0][0]?.workspaceDir).toBe(TEST_CACHE_DIR) - }) - }) -}) diff --git a/src/hooks/_auc-sync-mocks/sync-package-json.test.ts b/src/hooks/_auc-sync-mocks/sync-package-json.test.ts deleted file mode 100644 index b8f596f7e..000000000 --- a/src/hooks/_auc-sync-mocks/sync-package-json.test.ts +++ /dev/null @@ -1,349 +0,0 @@ -import { afterEach, beforeEach, describe, expect, it, mock } from "bun:test" -import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" -import { join } from "node:path" -import type { PluginEntryInfo } from "../auto-update-checker/checker/plugin-entry" - -const TEST_CACHE_DIR = join(import.meta.dir, "__test-sync-cache__") - -let importCounter = 0 - -async function importFreshSyncPackageJsonModule(): Promise { - mock.module("../auto-update-checker/constants", () => ({ - CACHE_DIR: TEST_CACHE_DIR, - PACKAGE_NAME: "oh-my-opencode", - NPM_REGISTRY_URL: "https://registry.npmjs.org/-/package/oh-my-opencode/dist-tags", - NPM_FETCH_TIMEOUT: 5000, - VERSION_FILE: join(TEST_CACHE_DIR, "version"), - USER_CONFIG_DIR: "/tmp/opencode-config", - USER_OPENCODE_CONFIG: "/tmp/opencode-config/opencode.json", - USER_OPENCODE_CONFIG_JSONC: "/tmp/opencode-config/opencode.jsonc", - INSTALLED_PACKAGE_JSON: join(TEST_CACHE_DIR, "node_modules", "oh-my-opencode", "package.json"), - getWindowsAppdataDir: () => null, - })) - - mock.module("../../shared/logger", () => ({ - log: () => {}, - })) - - const syncPackageJsonModule = await import(`../auto-update-checker/checker/sync-package-json?test=${importCounter++}`) - mock.restore() - return syncPackageJsonModule -} - -function resetTestCache(currentVersion = "3.10.0"): void { - if (existsSync(TEST_CACHE_DIR)) { - rmSync(TEST_CACHE_DIR, { recursive: true, force: true }) - } - - mkdirSync(TEST_CACHE_DIR, { recursive: true }) - writeFileSync( - join(TEST_CACHE_DIR, "package.json"), - JSON.stringify({ dependencies: { "oh-my-opencode": currentVersion, other: "1.0.0" } }, null, 2) - ) -} - -function cleanupTestCache(): void { - if (existsSync(TEST_CACHE_DIR)) { - rmSync(TEST_CACHE_DIR, { recursive: true, force: true }) - } -} - -function readCachePackageJsonVersion(): string | undefined { - const content = readFileSync(join(TEST_CACHE_DIR, "package.json"), "utf-8") - const pkg = JSON.parse(content) as { dependencies?: Record } - return pkg.dependencies?.["oh-my-opencode"] -} - -describe("syncCachePackageJsonToIntent", () => { - beforeEach(() => { - resetTestCache() - }) - - afterEach(() => { - cleanupTestCache() - }) - - describe("#given cache package.json with pinned semver version", () => { - describe("#when opencode.json intent is latest tag", () => { - it("#then updates package.json to use latest", async () => { - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@latest", - isPinned: false, - pinnedVersion: "latest", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(true) - expect(result.error).toBeNull() - expect(readCachePackageJsonVersion()).toBe("latest") - }) - }) - - describe("#when opencode.json intent is next tag", () => { - it("#then updates package.json to use next", async () => { - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@next", - isPinned: false, - pinnedVersion: "next", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(true) - expect(result.error).toBeNull() - expect(readCachePackageJsonVersion()).toBe("next") - }) - }) - - describe("#when opencode.json has no version (implies latest)", () => { - it("#then updates package.json to use latest", async () => { - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode", - isPinned: false, - pinnedVersion: null, - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(true) - expect(result.error).toBeNull() - expect(readCachePackageJsonVersion()).toBe("latest") - }) - }) - }) - - describe("#given cache package.json already matches intent", () => { - it("#then returns synced false with no error", async () => { - resetTestCache("latest") - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@latest", - isPinned: false, - pinnedVersion: "latest", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(false) - expect(result.error).toBeNull() - expect(readCachePackageJsonVersion()).toBe("latest") - }) - }) - - describe("#given cache package.json does not exist", () => { - it("#then returns file_not_found error", async () => { - cleanupTestCache() - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@latest", - isPinned: false, - pinnedVersion: "latest", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(false) - expect(result.error).toBe("file_not_found") - }) - }) - - describe("#given plugin not in cache package.json dependencies", () => { - it("#then returns plugin_not_in_deps error", async () => { - cleanupTestCache() - mkdirSync(TEST_CACHE_DIR, { recursive: true }) - writeFileSync( - join(TEST_CACHE_DIR, "package.json"), - JSON.stringify({ dependencies: { other: "1.0.0" } }, null, 2) - ) - - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@latest", - isPinned: false, - pinnedVersion: "latest", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(false) - expect(result.error).toBe("plugin_not_in_deps") - }) - }) - - describe("#given user explicitly changed from one semver to another", () => { - it("#then updates package.json to new version", async () => { - resetTestCache("3.9.0") - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@3.10.0", - isPinned: true, - pinnedVersion: "3.10.0", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(true) - expect(result.error).toBeNull() - expect(readCachePackageJsonVersion()).toBe("3.10.0") - }) - }) - - describe("#given cache package.json with other dependencies", () => { - it("#then other dependencies are preserved when updating plugin version", async () => { - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@latest", - isPinned: false, - pinnedVersion: "latest", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(true) - expect(result.error).toBeNull() - - const content = readFileSync(join(TEST_CACHE_DIR, "package.json"), "utf-8") - const pkg = JSON.parse(content) as { dependencies?: Record } - expect(pkg.dependencies?.["other"]).toBe("1.0.0") - }) - }) - - describe("#given malformed JSON in cache package.json", () => { - it("#then returns parse_error", async () => { - cleanupTestCache() - mkdirSync(TEST_CACHE_DIR, { recursive: true }) - writeFileSync(join(TEST_CACHE_DIR, "package.json"), "{ invalid json }") - - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@latest", - isPinned: false, - pinnedVersion: "latest", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(false) - expect(result.error).toBe("parse_error") - }) - }) - - describe("#given write permission denied", () => { - it("#then returns write_error", async () => { - cleanupTestCache() - mkdirSync(TEST_CACHE_DIR, { recursive: true }) - writeFileSync( - join(TEST_CACHE_DIR, "package.json"), - JSON.stringify({ dependencies: { "oh-my-opencode": "3.10.0" } }, null, 2) - ) - - const fs = await import("node:fs") - const originalWriteFileSync = fs.writeFileSync - const originalRenameSync = fs.renameSync - - mock.module("node:fs", () => ({ - ...fs, - writeFileSync: mock(() => { - throw new Error("EACCES: permission denied") - }), - renameSync: fs.renameSync, - })) - - try { - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@latest", - isPinned: false, - pinnedVersion: "latest", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(false) - expect(result.error).toBe("write_error") - } finally { - mock.module("node:fs", () => ({ - ...fs, - writeFileSync: originalWriteFileSync, - renameSync: originalRenameSync, - })) - } - }) - }) - - describe("#given rename fails after successful write", () => { - it("#then returns write_error and cleans up temp file", async () => { - cleanupTestCache() - mkdirSync(TEST_CACHE_DIR, { recursive: true }) - writeFileSync( - join(TEST_CACHE_DIR, "package.json"), - JSON.stringify({ dependencies: { "oh-my-opencode": "3.10.0" } }, null, 2) - ) - - const fs = await import("node:fs") - const originalWriteFileSync = fs.writeFileSync - const originalRenameSync = fs.renameSync - - let tempFilePath: string | null = null - - mock.module("node:fs", () => ({ - ...fs, - writeFileSync: mock((path: string, data: string) => { - tempFilePath = path - return originalWriteFileSync(path, data) - }), - renameSync: mock(() => { - throw new Error("EXDEV: cross-device link not permitted") - }), - })) - - try { - const { syncCachePackageJsonToIntent } = await importFreshSyncPackageJsonModule() - - const pluginInfo: PluginEntryInfo = { - entry: "oh-my-opencode@latest", - isPinned: false, - pinnedVersion: "latest", - configPath: "/tmp/opencode.json", - } - - const result = syncCachePackageJsonToIntent(pluginInfo) - - expect(result.synced).toBe(false) - expect(result.error).toBe("write_error") - expect(tempFilePath).not.toBeNull() - expect(existsSync(tempFilePath!)).toBe(false) - } finally { - mock.module("node:fs", () => ({ - ...fs, - writeFileSync: originalWriteFileSync, - renameSync: originalRenameSync, - })) - } - }) - }) -})