import { describe, test, expect, afterEach } from "bun:test"
import * as fs from "fs"
import * as path from "path"
import {
AGENT_NAME_MAP,
HOOK_NAME_MAP,
migrateAgentNames,
migrateHookNames,
migrateConfigFile,
migrateAgentConfigToCategory,
shouldDeleteAgentConfig,
} from "./migration"
describe("migrateAgentNames", () => {
test("migrates legacy OmO names to lowercase", () => {
const agents = {
omo: { model: "anthropic/claude-opus-4-5" },
OmO: { temperature: 0.5 },
"OmO-Plan": { prompt: "custom prompt" },
}
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["sisyphus"]).toEqual({ temperature: 0.5 })
expect(migrated["prometheus"]).toEqual({ prompt: "custom prompt" })
expect(migrated["omo"]).toBeUndefined()
expect(migrated["OmO"]).toBeUndefined()
expect(migrated["OmO-Plan"]).toBeUndefined()
})
test("preserves current agent names unchanged", () => {
const agents = {
oracle: { model: "openai/gpt-5.2" },
librarian: { model: "google/gemini-3-flash" },
explore: { model: "opencode/gpt-5-nano" },
}
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(false)
expect(migrated["oracle"]).toEqual({ model: "openai/gpt-5.2" })
expect(migrated["librarian"]).toEqual({ model: "google/gemini-3-flash" })
expect(migrated["explore"]).toEqual({ model: "opencode/gpt-5-nano" })
})
test("handles case-insensitive migration", () => {
const agents = {
SISYPHUS: { model: "test" },
"planner-sisyphus": { prompt: "test" },
"Orchestrator-Sisyphus": { model: "openai/gpt-5.2" },
}
const { migrated, changed } = migrateAgentNames(agents)
expect(migrated["sisyphus"]).toEqual({ model: "test" })
expect(migrated["prometheus"]).toEqual({ prompt: "test" })
expect(migrated["atlas"]).toEqual({ model: "openai/gpt-5.2" })
})
test("passes through unknown agent names unchanged", () => {
const agents = {
"custom-agent": { model: "custom/model" },
}
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(false)
expect(migrated["custom-agent"]).toEqual({ model: "custom/model" })
})
test("migrates orchestrator-sisyphus to atlas", () => {
const agents = {
"orchestrator-sisyphus": { model: "anthropic/claude-opus-4-5" },
}
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["atlas"]).toEqual({ model: "anthropic/claude-opus-4-5" })
expect(migrated["orchestrator-sisyphus"]).toBeUndefined()
})
test("migrates lowercase atlas to atlas", () => {
const agents = {
atlas: { model: "anthropic/claude-opus-4-5" },
}
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(false)
expect(migrated["atlas"]).toEqual({ model: "anthropic/claude-opus-4-5" })
})
test("migrates Sisyphus variants to lowercase", () => {
const agents = { "Sisyphus": { model: "test" } }
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["sisyphus"]).toEqual({ model: "test" })
expect(migrated["Sisyphus"]).toBeUndefined()
})
test("migrates omo key to sisyphus", () => {
const agents = { "omo": { model: "test" } }
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["sisyphus"]).toEqual({ model: "test" })
expect(migrated["omo"]).toBeUndefined()
})
test("migrates Atlas variants to lowercase", () => {
const agents = { "Atlas": { model: "test" } }
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["atlas"]).toEqual({ model: "test" })
expect(migrated["Atlas"]).toBeUndefined()
})
test("migrates Prometheus variants to lowercase", () => {
const agents = { "Prometheus (Planner)": { model: "test" } }
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["prometheus"]).toEqual({ model: "test" })
expect(migrated["Prometheus (Planner)"]).toBeUndefined()
})
test("migrates Metis variants to lowercase", () => {
const agents = { "Metis (Plan Consultant)": { model: "test" } }
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["metis"]).toEqual({ model: "test" })
expect(migrated["Metis (Plan Consultant)"]).toBeUndefined()
})
test("migrates Momus variants to lowercase", () => {
const agents = { "Momus (Plan Reviewer)": { model: "test" } }
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["momus"]).toEqual({ model: "test" })
expect(migrated["Momus (Plan Reviewer)"]).toBeUndefined()
})
test("migrates Sisyphus-Junior to lowercase", () => {
const agents = { "Sisyphus-Junior": { model: "test" } }
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(true)
expect(migrated["sisyphus-junior"]).toEqual({ model: "test" })
expect(migrated["Sisyphus-Junior"]).toBeUndefined()
})
test("preserves lowercase passthrough", () => {
const agents = { "oracle": { model: "test" } }
const { migrated, changed } = migrateAgentNames(agents)
expect(changed).toBe(false)
expect(migrated["oracle"]).toEqual({ model: "test" })
})
})
describe("migrateHookNames", () => {
test("migrates anthropic-auto-compact to anthropic-context-window-limit-recovery", () => {
const hooks = ["anthropic-auto-compact", "comment-checker"]
const { migrated, changed, removed } = migrateHookNames(hooks)
expect(changed).toBe(true)
expect(migrated).toContain("anthropic-context-window-limit-recovery")
expect(migrated).toContain("comment-checker")
expect(migrated).not.toContain("anthropic-auto-compact")
expect(removed).toEqual([])
})
test("preserves current hook names unchanged", () => {
const hooks = [
"anthropic-context-window-limit-recovery",
"todo-continuation-enforcer",
"session-recovery",
]
const { migrated, changed, removed } = migrateHookNames(hooks)
expect(changed).toBe(false)
expect(migrated).toEqual(hooks)
expect(removed).toEqual([])
})
test("handles empty hooks array", () => {
const hooks: string[] = []
const { migrated, changed, removed } = migrateHookNames(hooks)
expect(changed).toBe(false)
expect(migrated).toEqual([])
expect(removed).toEqual([])
})
test("migrates multiple legacy hook names", () => {
const hooks = ["anthropic-auto-compact"]
const { migrated, changed } = migrateHookNames(hooks)
expect(changed).toBe(true)
expect(migrated).toEqual(["anthropic-context-window-limit-recovery"])
})
test("migrates sisyphus-orchestrator to atlas", () => {
const hooks = ["sisyphus-orchestrator", "comment-checker"]
const { migrated, changed, removed } = migrateHookNames(hooks)
expect(changed).toBe(true)
expect(migrated).toContain("atlas")
expect(migrated).toContain("comment-checker")
expect(migrated).not.toContain("sisyphus-orchestrator")
expect(removed).toEqual([])
})
test("removes obsolete hooks and returns them in removed array", () => {
const hooks = ["preemptive-compaction", "empty-message-sanitizer", "comment-checker"]
const { migrated, changed, removed } = migrateHookNames(hooks)
expect(changed).toBe(true)
expect(migrated).toEqual(["comment-checker"])
expect(removed).toContain("preemptive-compaction")
expect(removed).toContain("empty-message-sanitizer")
expect(removed).toHaveLength(2)
})
test("handles mixed migration and removal", () => {
const hooks = ["anthropic-auto-compact", "preemptive-compaction", "sisyphus-orchestrator"]
const { migrated, changed, removed } = migrateHookNames(hooks)
expect(changed).toBe(true)
expect(migrated).toContain("anthropic-context-window-limit-recovery")
expect(migrated).toContain("atlas")
expect(migrated).not.toContain("preemptive-compaction")
expect(removed).toEqual(["preemptive-compaction"])
})
})
describe("migrateConfigFile", () => {
const testConfigPath = "/tmp/nonexistent-path-for-test.json"
test("migrates omo_agent to sisyphus_agent", () => {
const rawConfig: Record<string, unknown> = {
omo_agent: { disabled: false },
}
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(true)
expect(rawConfig.sisyphus_agent).toEqual({ disabled: false })
expect(rawConfig.omo_agent).toBeUndefined()
})
test("migrates legacy agent names in agents object", () => {
const rawConfig: Record<string, unknown> = {
agents: {
omo: { model: "test" },
OmO: { temperature: 0.5 },
},
}
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(true)
const agents = rawConfig.agents as Record<string, unknown>
expect(agents["sisyphus"]).toBeDefined()
})
test("migrates legacy hook names in disabled_hooks", () => {
const rawConfig: Record<string, unknown> = {
disabled_hooks: ["anthropic-auto-compact", "comment-checker"],
}
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(true)
expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery")
expect(rawConfig.disabled_hooks).not.toContain("anthropic-auto-compact")
})
test("does not write if no migration needed", () => {
const rawConfig: Record<string, unknown> = {
sisyphus_agent: { disabled: false },
agents: {
sisyphus: { model: "test" },
},
disabled_hooks: ["anthropic-context-window-limit-recovery"],
}
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(false)
})
test("handles migration of all legacy items together", () => {
const rawConfig: Record<string, unknown> = {
omo_agent: { disabled: false },
agents: {
omo: { model: "test" },
"OmO-Plan": { prompt: "custom" },
},
disabled_hooks: ["anthropic-auto-compact"],
}
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(true)
expect(rawConfig.sisyphus_agent).toEqual({ disabled: false })
expect(rawConfig.omo_agent).toBeUndefined()
const agents = rawConfig.agents as Record<string, unknown>
expect(agents["sisyphus"]).toBeDefined()
expect(agents["prometheus"]).toBeDefined()
expect(rawConfig.disabled_hooks).toContain("anthropic-context-window-limit-recovery")
})
})
describe("migration maps", () => {
test("AGENT_NAME_MAP contains all expected legacy mappings", () => {
expect(AGENT_NAME_MAP["omo"]).toBe("sisyphus")
expect(AGENT_NAME_MAP["OmO"]).toBe("sisyphus")
expect(AGENT_NAME_MAP["OmO-Plan"]).toBe("prometheus")
expect(AGENT_NAME_MAP["omo-plan"]).toBe("prometheus")
expect(AGENT_NAME_MAP["Planner-Sisyphus"]).toBe("prometheus")
expect(AGENT_NAME_MAP["plan-consultant"]).toBe("metis")
})
test("HOOK_NAME_MAP contains anthropic-auto-compact migration", () => {
expect(HOOK_NAME_MAP["anthropic-auto-compact"]).toBe("anthropic-context-window-limit-recovery")
})
})
describe("migrateAgentConfigToCategory", () => {
test("migrates model to category when mapping exists", () => {
const config = {
model: "google/gemini-3-pro",
temperature: 0.5,
top_p: 0.9,
}
const { migrated, changed } = migrateAgentConfigToCategory(config)
expect(changed).toBe(true)
expect(migrated.category).toBe("visual-engineering")
expect(migrated.model).toBeUndefined()
expect(migrated.temperature).toBe(0.5)
expect(migrated.top_p).toBe(0.9)
})
test("does not migrate when model is not in map", () => {
const config = {
model: "custom/model",
temperature: 0.5,
}
const { migrated, changed } = migrateAgentConfigToCategory(config)
expect(changed).toBe(false)
expect(migrated).toEqual(config)
})
test("does not migrate when model is not a string", () => {
const config = {
model: { name: "test" },
temperature: 0.5,
}
const { migrated, changed } = migrateAgentConfigToCategory(config)
expect(changed).toBe(false)
expect(migrated).toEqual(config)
})
test("handles all mapped models correctly", () => {
const configs = [
{ model: "google/gemini-3-pro" },
{ model: "google/gemini-3-flash" },
{ model: "openai/gpt-5.2" },
{ model: "anthropic/claude-haiku-4-5" },
{ model: "anthropic/claude-opus-4-5" },
{ model: "anthropic/claude-sonnet-4-5" },
]
const expectedCategories = ["visual-engineering", "writing", "ultrabrain", "quick", "unspecified-high", "unspecified-low"]
const results = configs.map(migrateAgentConfigToCategory)
results.forEach((result, index) => {
expect(result.changed).toBe(true)
expect(result.migrated.category).toBe(expectedCategories[index])
expect(result.migrated.model).toBeUndefined()
})
})
test("preserves non-model fields during migration", () => {
const config = {
model: "openai/gpt-5.2",
temperature: 0.1,
top_p: 0.95,
maxTokens: 4096,
prompt_append: "custom instruction",
}
const { migrated } = migrateAgentConfigToCategory(config)
expect(migrated.category).toBe("ultrabrain")
expect(migrated.temperature).toBe(0.1)
expect(migrated.top_p).toBe(0.95)
expect(migrated.maxTokens).toBe(4096)
expect(migrated.prompt_append).toBe("custom instruction")
})
})
describe("shouldDeleteAgentConfig", () => {
test("returns true when config only has category field", () => {
const config = { category: "visual-engineering" }
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
expect(shouldDelete).toBe(true)
})
test("returns false when category does not exist", () => {
const config = { category: "unknown" }
const shouldDelete = shouldDeleteAgentConfig(config, "unknown")
expect(shouldDelete).toBe(false)
})
test("returns true when all fields match category defaults", () => {
const config = {
category: "visual-engineering",
model: "google/gemini-3-pro",
}
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
expect(shouldDelete).toBe(true)
})
test("returns false when fields differ from category defaults", () => {
const config = {
category: "visual-engineering",
model: "anthropic/claude-opus-4-5",
}
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
expect(shouldDelete).toBe(false)
})
test("handles different categories with their defaults", () => {
const configs = [
{ category: "ultrabrain" },
{ category: "quick" },
{ category: "unspecified-high" },
{ category: "unspecified-low" },
]
const results = configs.map((config) => shouldDeleteAgentConfig(config, config.category as string))
results.forEach((result) => {
expect(result).toBe(true)
})
})
test("returns false when additional fields are present", () => {
const config = {
category: "visual-engineering",
temperature: 0.7,
custom_field: "value",
}
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
expect(shouldDelete).toBe(false)
})
test("handles complex config with multiple overrides", () => {
const config = {
category: "visual-engineering",
temperature: 0.5,
top_p: 0.8,
prompt_append: "custom prompt",
}
const shouldDelete = shouldDeleteAgentConfig(config, "visual-engineering")
expect(shouldDelete).toBe(false)
})
})
describe("migrateConfigFile with backup", () => {
const cleanupPaths: string[] = []
afterEach(() => {
cleanupPaths.forEach((p) => {
try {
fs.unlinkSync(p)
} catch {
}
})
})
test("creates backup file with timestamp when legacy migration needed", () => {
const testConfigPath = "/tmp/test-config-migration.json"
const testConfigContent = globalThis.JSON.stringify({ agents: { omo: { model: "test" } } }, null, 2)
const rawConfig: Record<string, unknown> = {
agents: {
omo: { model: "test" },
},
}
fs.writeFileSync(testConfigPath, testConfigContent)
cleanupPaths.push(testConfigPath)
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(true)
const dir = path.dirname(testConfigPath)
const basename = path.basename(testConfigPath)
const files = fs.readdirSync(dir)
const backupFiles = files.filter((f) => f.startsWith(`${basename}.bak.`))
expect(backupFiles.length).toBeGreaterThan(0)
const backupFile = backupFiles[0]
const backupPath = path.join(dir, backupFile)
cleanupPaths.push(backupPath)
expect(backupFile).toMatch(/test-config-migration\.json\.bak\.\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}/)
const backupContent = fs.readFileSync(backupPath, "utf-8")
expect(backupContent).toBe(testConfigContent)
})
test("preserves model setting without auto-conversion to category", () => {
const testConfigPath = "/tmp/test-config-preserve-model.json"
const rawConfig: Record<string, unknown> = {
agents: {
"multimodal-looker": { model: "anthropic/claude-haiku-4-5" },
oracle: { model: "openai/gpt-5.2" },
"my-custom-agent": { model: "google/gemini-3-pro" },
},
}
fs.writeFileSync(testConfigPath, globalThis.JSON.stringify(rawConfig, null, 2))
cleanupPaths.push(testConfigPath)
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(false)
const agents = rawConfig.agents as Record<string, Record<string, unknown>>
expect(agents["multimodal-looker"].model).toBe("anthropic/claude-haiku-4-5")
expect(agents.oracle.model).toBe("openai/gpt-5.2")
expect(agents["my-custom-agent"].model).toBe("google/gemini-3-pro")
})
test("preserves category setting when explicitly set", () => {
const testConfigPath = "/tmp/test-config-preserve-category.json"
const rawConfig: Record<string, unknown> = {
agents: {
"multimodal-looker": { category: "quick" },
oracle: { category: "ultrabrain" },
},
}
fs.writeFileSync(testConfigPath, globalThis.JSON.stringify(rawConfig, null, 2))
cleanupPaths.push(testConfigPath)
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(false)
const agents = rawConfig.agents as Record<string, Record<string, unknown>>
expect(agents["multimodal-looker"].category).toBe("quick")
expect(agents.oracle.category).toBe("ultrabrain")
})
test("does not write when no migration needed", () => {
const testConfigPath = "/tmp/test-config-no-migration.json"
const rawConfig: Record<string, unknown> = {
agents: {
sisyphus: { model: "test" },
},
}
fs.writeFileSync(testConfigPath, globalThis.JSON.stringify({ agents: { sisyphus: { model: "test" } } }, null, 2))
cleanupPaths.push(testConfigPath)
const dir = path.dirname(testConfigPath)
const basename = path.basename(testConfigPath)
const existingFiles = fs.readdirSync(dir)
const existingBackups = existingFiles.filter((f) => f.startsWith(`${basename}.bak.`))
existingBackups.forEach((f) => {
const backupPath = path.join(dir, f)
try {
fs.unlinkSync(backupPath)
cleanupPaths.splice(cleanupPaths.indexOf(backupPath), 1)
} catch {
}
})
const needsWrite = migrateConfigFile(testConfigPath, rawConfig)
expect(needsWrite).toBe(false)
const files = fs.readdirSync(dir)
const backupFiles = files.filter((f) => f.startsWith(`${basename}.bak.`))
expect(backupFiles.length).toBe(0)
})
})