* style(tests): normalize BDD comments from '// #given' to '// given' - Replace 4,668 Python-style BDD comments across 107 test files - Patterns changed: // #given -> // given, // #when -> // when, // #then -> // then - Also handles no-space variants: //#given -> // given * fix(rules-injector): prefer output.metadata.filePath over output.title - Extract file path resolution to dedicated output-path.ts module - Prefer metadata.filePath which contains actual file path - Fall back to output.title only when metadata unavailable - Fixes issue where rules weren't injected when tool output title was a label * feat(slashcommand): add optional user_message parameter - Add user_message optional parameter for command arguments - Model can now call: command='publish' user_message='patch' - Improves error messages with clearer format guidance - Helps LLMs understand correct parameter usage * feat(hooks): restore compaction-context-injector hook - Restore hook deleted in cbbc7bd0 for session compaction context - Injects 7 mandatory sections: User Requests, Final Goal, Work Completed, Remaining Tasks, Active Working Context, MUST NOT Do, Agent Verification State - Re-register in hooks/index.ts and main plugin entry * refactor(background-agent): split manager.ts into focused modules - Extract constants.ts for TTL values and internal types (52 lines) - Extract state.ts for TaskStateManager class (204 lines) - Extract spawner.ts for task creation logic (244 lines) - Extract result-handler.ts for completion handling (265 lines) - Reduce manager.ts from 1377 to 755 lines (45% reduction) - Maintain backward compatible exports * refactor(agents): split prometheus-prompt.ts into subdirectory - Move 1196-line prometheus-prompt.ts to prometheus/ subdirectory - Organize prompt sections into separate files for maintainability - Update agents/index.ts exports * refactor(delegate-task): split tools.ts into focused modules - Extract categories.ts for category definitions and routing - Extract executor.ts for task execution logic - Extract helpers.ts for utility functions - Extract prompt-builder.ts for prompt construction - Reduce tools.ts complexity with cleaner separation of concerns * refactor(builtin-skills): split skills.ts into individual skill files - Move each skill to dedicated file in skills/ subdirectory - Create barrel export for backward compatibility - Improve maintainability with focused skill modules * chore: update import paths and lockfile - Update prometheus import path after refactor - Update bun.lock * fix(tests): complete BDD comment normalization - Fix remaining #when/#then patterns missed by initial sed - Affected: state.test.ts, events.test.ts --------- Co-authored-by: justsisyphus <justsisyphus@users.noreply.github.com>
412 lines
14 KiB
TypeScript
412 lines
14 KiB
TypeScript
import { describe, expect, test, mock, beforeEach, afterEach } from "bun:test"
|
|
|
|
import { ANTIGRAVITY_PROVIDER_CONFIG, getPluginNameWithVersion, fetchNpmDistTags, generateOmoConfig } from "./config-manager"
|
|
import type { InstallConfig } from "./types"
|
|
|
|
describe("getPluginNameWithVersion", () => {
|
|
const originalFetch = globalThis.fetch
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch
|
|
})
|
|
|
|
test("returns @latest when current version matches latest tag", async () => {
|
|
// given npm dist-tags with latest=2.14.0
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
|
} as Response)
|
|
) as unknown as typeof fetch
|
|
|
|
// when current version is 2.14.0
|
|
const result = await getPluginNameWithVersion("2.14.0")
|
|
|
|
// then should use @latest tag
|
|
expect(result).toBe("oh-my-opencode@latest")
|
|
})
|
|
|
|
test("returns @beta when current version matches beta tag", async () => {
|
|
// given npm dist-tags with beta=3.0.0-beta.3
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
|
} as Response)
|
|
) as unknown as typeof fetch
|
|
|
|
// when current version is 3.0.0-beta.3
|
|
const result = await getPluginNameWithVersion("3.0.0-beta.3")
|
|
|
|
// then should use @beta tag
|
|
expect(result).toBe("oh-my-opencode@beta")
|
|
})
|
|
|
|
test("returns @next when current version matches next tag", async () => {
|
|
// given npm dist-tags with next=3.1.0-next.1
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3", next: "3.1.0-next.1" }),
|
|
} as Response)
|
|
) as unknown as typeof fetch
|
|
|
|
// when current version is 3.1.0-next.1
|
|
const result = await getPluginNameWithVersion("3.1.0-next.1")
|
|
|
|
// then should use @next tag
|
|
expect(result).toBe("oh-my-opencode@next")
|
|
})
|
|
|
|
test("returns pinned version when no tag matches", async () => {
|
|
// given npm dist-tags with beta=3.0.0-beta.3
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
|
} as Response)
|
|
) as unknown as typeof fetch
|
|
|
|
// when current version is old beta 3.0.0-beta.2
|
|
const result = await getPluginNameWithVersion("3.0.0-beta.2")
|
|
|
|
// then should pin to specific version
|
|
expect(result).toBe("oh-my-opencode@3.0.0-beta.2")
|
|
})
|
|
|
|
test("returns pinned version when fetch fails", async () => {
|
|
// given network failure
|
|
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
|
|
|
// when current version is 3.0.0-beta.3
|
|
const result = await getPluginNameWithVersion("3.0.0-beta.3")
|
|
|
|
// then should fall back to pinned version
|
|
expect(result).toBe("oh-my-opencode@3.0.0-beta.3")
|
|
})
|
|
|
|
test("returns pinned version when npm returns non-ok response", async () => {
|
|
// given npm returns 404
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: false,
|
|
status: 404,
|
|
} as Response)
|
|
) as unknown as typeof fetch
|
|
|
|
// when current version is 2.14.0
|
|
const result = await getPluginNameWithVersion("2.14.0")
|
|
|
|
// then should fall back to pinned version
|
|
expect(result).toBe("oh-my-opencode@2.14.0")
|
|
})
|
|
|
|
test("prioritizes latest over other tags when version matches multiple", async () => {
|
|
// given version matches both latest and beta (during release promotion)
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ beta: "3.0.0", latest: "3.0.0", next: "3.1.0-alpha.1" }),
|
|
} as Response)
|
|
) as unknown as typeof fetch
|
|
|
|
// when current version matches both
|
|
const result = await getPluginNameWithVersion("3.0.0")
|
|
|
|
// then should prioritize @latest
|
|
expect(result).toBe("oh-my-opencode@latest")
|
|
})
|
|
})
|
|
|
|
describe("fetchNpmDistTags", () => {
|
|
const originalFetch = globalThis.fetch
|
|
|
|
afterEach(() => {
|
|
globalThis.fetch = originalFetch
|
|
})
|
|
|
|
test("returns dist-tags on success", async () => {
|
|
// given npm returns dist-tags
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: true,
|
|
json: () => Promise.resolve({ latest: "2.14.0", beta: "3.0.0-beta.3" }),
|
|
} as Response)
|
|
) as unknown as typeof fetch
|
|
|
|
// when fetching dist-tags
|
|
const result = await fetchNpmDistTags("oh-my-opencode")
|
|
|
|
// then should return the tags
|
|
expect(result).toEqual({ latest: "2.14.0", beta: "3.0.0-beta.3" })
|
|
})
|
|
|
|
test("returns null on network failure", async () => {
|
|
// given network failure
|
|
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
|
|
|
|
// when fetching dist-tags
|
|
const result = await fetchNpmDistTags("oh-my-opencode")
|
|
|
|
// then should return null
|
|
expect(result).toBeNull()
|
|
})
|
|
|
|
test("returns null on non-ok response", async () => {
|
|
// given npm returns 404
|
|
globalThis.fetch = mock(() =>
|
|
Promise.resolve({
|
|
ok: false,
|
|
status: 404,
|
|
} as Response)
|
|
) as unknown as typeof fetch
|
|
|
|
// when fetching dist-tags
|
|
const result = await fetchNpmDistTags("oh-my-opencode")
|
|
|
|
// then should return null
|
|
expect(result).toBeNull()
|
|
})
|
|
})
|
|
|
|
describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
|
|
test("all models include full spec (limit + modalities + Antigravity label)", () => {
|
|
const google = (ANTIGRAVITY_PROVIDER_CONFIG as any).google
|
|
expect(google).toBeTruthy()
|
|
|
|
const models = google.models as Record<string, any>
|
|
expect(models).toBeTruthy()
|
|
|
|
const required = [
|
|
"antigravity-gemini-3-pro",
|
|
"antigravity-gemini-3-flash",
|
|
"antigravity-claude-sonnet-4-5",
|
|
"antigravity-claude-sonnet-4-5-thinking",
|
|
"antigravity-claude-opus-4-5-thinking",
|
|
]
|
|
|
|
for (const key of required) {
|
|
const model = models[key]
|
|
expect(model).toBeTruthy()
|
|
expect(typeof model.name).toBe("string")
|
|
expect(model.name.includes("(Antigravity)")).toBe(true)
|
|
|
|
expect(model.limit).toBeTruthy()
|
|
expect(typeof model.limit.context).toBe("number")
|
|
expect(typeof model.limit.output).toBe("number")
|
|
|
|
expect(model.modalities).toBeTruthy()
|
|
expect(Array.isArray(model.modalities.input)).toBe(true)
|
|
expect(Array.isArray(model.modalities.output)).toBe(true)
|
|
}
|
|
})
|
|
|
|
test("Gemini models have variant definitions", () => {
|
|
// given the antigravity provider config
|
|
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
|
|
|
// when checking Gemini Pro variants
|
|
const pro = models["antigravity-gemini-3-pro"]
|
|
// then should have low and high variants
|
|
expect(pro.variants).toBeTruthy()
|
|
expect(pro.variants.low).toBeTruthy()
|
|
expect(pro.variants.high).toBeTruthy()
|
|
|
|
// when checking Gemini Flash variants
|
|
const flash = models["antigravity-gemini-3-flash"]
|
|
// then should have minimal, low, medium, high variants
|
|
expect(flash.variants).toBeTruthy()
|
|
expect(flash.variants.minimal).toBeTruthy()
|
|
expect(flash.variants.low).toBeTruthy()
|
|
expect(flash.variants.medium).toBeTruthy()
|
|
expect(flash.variants.high).toBeTruthy()
|
|
})
|
|
|
|
test("Claude thinking models have variant definitions", () => {
|
|
// given the antigravity provider config
|
|
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
|
|
|
|
// when checking Claude thinking variants
|
|
const sonnetThinking = models["antigravity-claude-sonnet-4-5-thinking"]
|
|
const opusThinking = models["antigravity-claude-opus-4-5-thinking"]
|
|
|
|
// then both should have low and max variants
|
|
for (const model of [sonnetThinking, opusThinking]) {
|
|
expect(model.variants).toBeTruthy()
|
|
expect(model.variants.low).toBeTruthy()
|
|
expect(model.variants.max).toBeTruthy()
|
|
}
|
|
})
|
|
})
|
|
|
|
describe("generateOmoConfig - model fallback system", () => {
|
|
test("generates native sonnet models when Claude standard subscription", () => {
|
|
// given user has Claude standard subscription (not max20)
|
|
const config: InstallConfig = {
|
|
hasClaude: true,
|
|
isMax20: false,
|
|
hasOpenAI: false,
|
|
hasGemini: false,
|
|
hasCopilot: false,
|
|
hasOpencodeZen: false,
|
|
hasZaiCodingPlan: false,
|
|
hasKimiForCoding: false,
|
|
}
|
|
|
|
// when generating config
|
|
const result = generateOmoConfig(config)
|
|
|
|
// then should use native anthropic sonnet (cost-efficient for standard plan)
|
|
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
|
expect(result.agents).toBeDefined()
|
|
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-sonnet-4-5")
|
|
})
|
|
|
|
test("generates native opus models when Claude max20 subscription", () => {
|
|
// given user has Claude max20 subscription
|
|
const config: InstallConfig = {
|
|
hasClaude: true,
|
|
isMax20: true,
|
|
hasOpenAI: false,
|
|
hasGemini: false,
|
|
hasCopilot: false,
|
|
hasOpencodeZen: false,
|
|
hasZaiCodingPlan: false,
|
|
hasKimiForCoding: false,
|
|
}
|
|
|
|
// when generating config
|
|
const result = generateOmoConfig(config)
|
|
|
|
// then should use native anthropic opus (max power for max20 plan)
|
|
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
|
})
|
|
|
|
test("uses github-copilot sonnet fallback when only copilot available", () => {
|
|
// given user has only copilot (no max plan)
|
|
const config: InstallConfig = {
|
|
hasClaude: false,
|
|
isMax20: false,
|
|
hasOpenAI: false,
|
|
hasGemini: false,
|
|
hasCopilot: true,
|
|
hasOpencodeZen: false,
|
|
hasZaiCodingPlan: false,
|
|
hasKimiForCoding: false,
|
|
}
|
|
|
|
// when generating config
|
|
const result = generateOmoConfig(config)
|
|
|
|
// then should use github-copilot sonnet models (copilot fallback)
|
|
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("github-copilot/claude-sonnet-4.5")
|
|
})
|
|
|
|
test("uses ultimate fallback when no providers configured", () => {
|
|
// given user has no providers
|
|
const config: InstallConfig = {
|
|
hasClaude: false,
|
|
isMax20: false,
|
|
hasOpenAI: false,
|
|
hasGemini: false,
|
|
hasCopilot: false,
|
|
hasOpencodeZen: false,
|
|
hasZaiCodingPlan: false,
|
|
hasKimiForCoding: false,
|
|
}
|
|
|
|
// when generating config
|
|
const result = generateOmoConfig(config)
|
|
|
|
// then should use ultimate fallback for all agents
|
|
expect(result.$schema).toBe("https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json")
|
|
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("opencode/glm-4.7-free")
|
|
})
|
|
|
|
test("uses zai-coding-plan/glm-4.7 for librarian when Z.ai available", () => {
|
|
// given user has Z.ai and Claude max20
|
|
const config: InstallConfig = {
|
|
hasClaude: true,
|
|
isMax20: true,
|
|
hasOpenAI: false,
|
|
hasGemini: false,
|
|
hasCopilot: false,
|
|
hasOpencodeZen: false,
|
|
hasZaiCodingPlan: true,
|
|
hasKimiForCoding: false,
|
|
}
|
|
|
|
// when generating config
|
|
const result = generateOmoConfig(config)
|
|
|
|
// then librarian should use zai-coding-plan/glm-4.7
|
|
expect((result.agents as Record<string, { model: string }>).librarian.model).toBe("zai-coding-plan/glm-4.7")
|
|
// then other agents should use native opus (max20 plan)
|
|
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("anthropic/claude-opus-4-5")
|
|
})
|
|
|
|
test("uses native OpenAI models when only ChatGPT available", () => {
|
|
// given user has only ChatGPT subscription
|
|
const config: InstallConfig = {
|
|
hasClaude: false,
|
|
isMax20: false,
|
|
hasOpenAI: true,
|
|
hasGemini: false,
|
|
hasCopilot: false,
|
|
hasOpencodeZen: false,
|
|
hasZaiCodingPlan: false,
|
|
hasKimiForCoding: false,
|
|
}
|
|
|
|
// when generating config
|
|
const result = generateOmoConfig(config)
|
|
|
|
// then Sisyphus should use native OpenAI (fallback within native tier)
|
|
expect((result.agents as Record<string, { model: string }>).sisyphus.model).toBe("openai/gpt-5.2")
|
|
// then Oracle should use native OpenAI (first fallback entry)
|
|
expect((result.agents as Record<string, { model: string }>).oracle.model).toBe("openai/gpt-5.2")
|
|
// then multimodal-looker should use native OpenAI (fallback within native tier)
|
|
expect((result.agents as Record<string, { model: string }>)["multimodal-looker"].model).toBe("openai/gpt-5.2")
|
|
})
|
|
|
|
test("uses haiku for explore when Claude max20", () => {
|
|
// given user has Claude max20
|
|
const config: InstallConfig = {
|
|
hasClaude: true,
|
|
isMax20: true,
|
|
hasOpenAI: false,
|
|
hasGemini: false,
|
|
hasCopilot: false,
|
|
hasOpencodeZen: false,
|
|
hasZaiCodingPlan: false,
|
|
hasKimiForCoding: false,
|
|
}
|
|
|
|
// when generating config
|
|
const result = generateOmoConfig(config)
|
|
|
|
// then explore should use haiku (max20 plan uses Claude quota)
|
|
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("anthropic/claude-haiku-4-5")
|
|
})
|
|
|
|
test("uses haiku for explore regardless of max20 flag", () => {
|
|
// given user has Claude but not max20
|
|
const config: InstallConfig = {
|
|
hasClaude: true,
|
|
isMax20: false,
|
|
hasOpenAI: false,
|
|
hasGemini: false,
|
|
hasCopilot: false,
|
|
hasOpencodeZen: false,
|
|
hasZaiCodingPlan: false,
|
|
hasKimiForCoding: false,
|
|
}
|
|
|
|
// when generating config
|
|
const result = generateOmoConfig(config)
|
|
|
|
// then explore should use haiku (isMax20 doesn't affect explore anymore)
|
|
expect((result.agents as Record<string, { model: string }>).explore.model).toBe("anthropic/claude-haiku-4-5")
|
|
})
|
|
})
|