Files
oh-my-openagent/src/hooks/legacy-plugin-toast/hook.test.ts
YeonGyu-Kim 6a733c9dde fix(#2823): auto-migrate legacy plugin name and warn users at startup
- logLegacyPluginStartupWarning now emits console.warn (visible to user,
  not just log file) when oh-my-opencode is detected in opencode.json
- Auto-migrates opencode.json plugin entry from oh-my-opencode to
  oh-my-openagent (with backup)
- plugin-config.ts: add console.warn when loading legacy config filename
- test: 10 tests covering migration, console output, edge cases
2026-03-27 15:40:04 +09:00

207 lines
5.9 KiB
TypeScript

import { afterAll, beforeEach, describe, expect, it, mock } from "bun:test"
import type { MigrationResult } from "./auto-migrate"
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", () => ({
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()
})
})
})