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
This commit is contained in:
@@ -0,0 +1,206 @@
|
||||
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()
|
||||
})
|
||||
})
|
||||
})
|
||||
Reference in New Issue
Block a user