Files
oh-my-opencode/src/hooks/legacy-plugin-toast/hook.test.ts
T
YeonGyu-Kim f03c6700d4 fix(hooks): reuse shared legacy plugin migration helper
Replace the legacy toast regex writer with the shared atomic helper and route legacy plugin call sites through small wrappers so existing mock.module tests stop leaking across the suite.

Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
2026-04-04 01:32:59 +09:00

207 lines
5.9 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"
import type { MigrationResult } from "./auto-migrate-runner"
const mockCheckForLegacyPluginEntry = mock(() => ({
hasLegacyEntry: false,
hasCanonicalEntry: false,
legacyEntries: [] as string[],
}))
const mockAutoMigrate = mock((): MigrationResult => ({
migrated: false,
from: null,
to: null,
configPath: null,
}))
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const mockShowToast = mock((_arg: any) => Promise.resolve())
const mockLog = mock(() => {})
mock.module("../../shared/legacy-plugin-warning", () => ({
checkForLegacyPluginEntry: mockCheckForLegacyPluginEntry,
}))
mock.module("../../shared/logger", () => ({
log: mockLog,
}))
mock.module("./auto-migrate-runner", () => ({
autoMigrateLegacyPluginEntry: mockAutoMigrate,
}))
afterAll(() => {
mock.restore()
})
function createMockCtx() {
return {
client: {
tui: { showToast: mockShowToast },
},
directory: "/tmp/test",
} as never
}
function createEvent(type: string, parentID?: string) {
return {
event: {
type,
properties: parentID ? { info: { parentID } } : { info: {} },
},
}
}
async function importFreshModule() {
return import(`./hook?t=${Date.now()}-${Math.random()}`)
}
describe("createLegacyPluginToastHook", () => {
beforeEach(() => {
mockCheckForLegacyPluginEntry.mockReset()
mockAutoMigrate.mockReset()
mockShowToast.mockReset()
mockLog.mockReset()
mockCheckForLegacyPluginEntry.mockReturnValue({
hasLegacyEntry: false,
hasCanonicalEntry: true,
legacyEntries: [],
})
mockAutoMigrate.mockReturnValue({ migrated: false, from: null, to: null, configPath: null })
mockShowToast.mockResolvedValue(undefined)
})
describe("#given no legacy entry exists", () => {
it("#then does not show a toast", async () => {
// given
const { createLegacyPluginToastHook } = await importFreshModule()
const hook = createLegacyPluginToastHook(createMockCtx())
// when
await hook.event(createEvent("session.created"))
// then
expect(mockShowToast).not.toHaveBeenCalled()
})
})
describe("#given legacy entry exists and migration succeeds", () => {
it("#then shows success toast", async () => {
// given
mockCheckForLegacyPluginEntry.mockReturnValue({
hasLegacyEntry: true,
hasCanonicalEntry: false,
legacyEntries: ["oh-my-opencode"],
})
mockAutoMigrate.mockReturnValue({
migrated: true,
from: "oh-my-opencode",
to: "oh-my-openagent",
configPath: "/tmp/opencode.json",
})
const { createLegacyPluginToastHook } = await importFreshModule()
const hook = createLegacyPluginToastHook(createMockCtx())
// when
await hook.event(createEvent("session.created"))
// then
expect(mockShowToast).toHaveBeenCalledTimes(1)
const toastArg = mockShowToast.mock.calls[0][0] as { body: { variant: string } }
expect(toastArg.body.variant).toBe("success")
})
})
describe("#given legacy entry exists but migration fails", () => {
it("#then shows warning toast", async () => {
// given
mockCheckForLegacyPluginEntry.mockReturnValue({
hasLegacyEntry: true,
hasCanonicalEntry: false,
legacyEntries: ["oh-my-opencode"],
})
mockAutoMigrate.mockReturnValue({
migrated: false,
from: null,
to: null,
configPath: "/tmp/opencode.json",
})
const { createLegacyPluginToastHook } = await importFreshModule()
const hook = createLegacyPluginToastHook(createMockCtx())
// when
await hook.event(createEvent("session.created"))
// then
expect(mockShowToast).toHaveBeenCalledTimes(1)
const toastArg2 = mockShowToast.mock.calls[0][0] as { body: { variant: string } }
expect(toastArg2.body.variant).toBe("warning")
})
})
describe("#given session.created fires twice", () => {
it("#then only fires once (once-guard)", async () => {
// given
mockCheckForLegacyPluginEntry.mockReturnValue({
hasLegacyEntry: true,
hasCanonicalEntry: false,
legacyEntries: ["oh-my-opencode"],
})
mockAutoMigrate.mockReturnValue({
migrated: true,
from: "oh-my-opencode",
to: "oh-my-openagent",
configPath: "/tmp/opencode.json",
})
const { createLegacyPluginToastHook } = await importFreshModule()
const hook = createLegacyPluginToastHook(createMockCtx())
// when
await hook.event(createEvent("session.created"))
await hook.event(createEvent("session.created"))
// then
expect(mockShowToast).toHaveBeenCalledTimes(1)
})
})
describe("#given a non-session.created event fires", () => {
it("#then does nothing", async () => {
// given
mockCheckForLegacyPluginEntry.mockReturnValue({
hasLegacyEntry: true,
hasCanonicalEntry: false,
legacyEntries: ["oh-my-opencode"],
})
const { createLegacyPluginToastHook } = await importFreshModule()
const hook = createLegacyPluginToastHook(createMockCtx())
// when
await hook.event(createEvent("session.deleted"))
// then
expect(mockCheckForLegacyPluginEntry).not.toHaveBeenCalled()
})
})
describe("#given session.created from a subagent (has parentID)", () => {
it("#then skips the check", async () => {
// given
mockCheckForLegacyPluginEntry.mockReturnValue({
hasLegacyEntry: true,
hasCanonicalEntry: false,
legacyEntries: ["oh-my-opencode"],
})
const { createLegacyPluginToastHook } = await importFreshModule()
const hook = createLegacyPluginToastHook(createMockCtx())
// when
await hook.event(createEvent("session.created", "parent-session-id"))
// then
expect(mockCheckForLegacyPluginEntry).not.toHaveBeenCalled()
})
})
})