refactor: major codebase cleanup - BDD comments, file splitting, bug fixes (#1350)

* 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>
This commit is contained in:
YeonGyu-Kim
2026-02-01 16:47:50 +09:00
committed by GitHub
parent c83150d9ea
commit f146aeff0f
145 changed files with 10307 additions and 9562 deletions
+65 -65
View File
@@ -11,7 +11,7 @@ describe("getPluginNameWithVersion", () => {
})
test("returns @latest when current version matches latest tag", async () => {
// #given npm dist-tags with latest=2.14.0
// given npm dist-tags with latest=2.14.0
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
@@ -19,15 +19,15 @@ describe("getPluginNameWithVersion", () => {
} as Response)
) as unknown as typeof fetch
// #when current version is 2.14.0
// when current version is 2.14.0
const result = await getPluginNameWithVersion("2.14.0")
// #then should use @latest tag
// 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
// given npm dist-tags with beta=3.0.0-beta.3
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
@@ -35,15 +35,15 @@ describe("getPluginNameWithVersion", () => {
} as Response)
) as unknown as typeof fetch
// #when current version is 3.0.0-beta.3
// when current version is 3.0.0-beta.3
const result = await getPluginNameWithVersion("3.0.0-beta.3")
// #then should use @beta tag
// 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
// given npm dist-tags with next=3.1.0-next.1
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
@@ -51,15 +51,15 @@ describe("getPluginNameWithVersion", () => {
} as Response)
) as unknown as typeof fetch
// #when current version is 3.1.0-next.1
// when current version is 3.1.0-next.1
const result = await getPluginNameWithVersion("3.1.0-next.1")
// #then should use @next tag
// 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
// given npm dist-tags with beta=3.0.0-beta.3
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
@@ -67,26 +67,26 @@ describe("getPluginNameWithVersion", () => {
} as Response)
) as unknown as typeof fetch
// #when current version is old beta 3.0.0-beta.2
// 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
// 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
// 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
// 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
// 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
// given npm returns 404
globalThis.fetch = mock(() =>
Promise.resolve({
ok: false,
@@ -94,15 +94,15 @@ describe("getPluginNameWithVersion", () => {
} as Response)
) as unknown as typeof fetch
// #when current version is 2.14.0
// when current version is 2.14.0
const result = await getPluginNameWithVersion("2.14.0")
// #then should fall back to pinned version
// 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)
// given version matches both latest and beta (during release promotion)
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
@@ -110,10 +110,10 @@ describe("getPluginNameWithVersion", () => {
} as Response)
) as unknown as typeof fetch
// #when current version matches both
// when current version matches both
const result = await getPluginNameWithVersion("3.0.0")
// #then should prioritize @latest
// then should prioritize @latest
expect(result).toBe("oh-my-opencode@latest")
})
})
@@ -126,7 +126,7 @@ describe("fetchNpmDistTags", () => {
})
test("returns dist-tags on success", async () => {
// #given npm returns dist-tags
// given npm returns dist-tags
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
@@ -134,26 +134,26 @@ describe("fetchNpmDistTags", () => {
} as Response)
) as unknown as typeof fetch
// #when fetching dist-tags
// when fetching dist-tags
const result = await fetchNpmDistTags("oh-my-opencode")
// #then should return the tags
// 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
// given network failure
globalThis.fetch = mock(() => Promise.reject(new Error("Network error"))) as unknown as typeof fetch
// #when fetching dist-tags
// when fetching dist-tags
const result = await fetchNpmDistTags("oh-my-opencode")
// #then should return null
// then should return null
expect(result).toBeNull()
})
test("returns null on non-ok response", async () => {
// #given npm returns 404
// given npm returns 404
globalThis.fetch = mock(() =>
Promise.resolve({
ok: false,
@@ -161,10 +161,10 @@ describe("fetchNpmDistTags", () => {
} as Response)
) as unknown as typeof fetch
// #when fetching dist-tags
// when fetching dist-tags
const result = await fetchNpmDistTags("oh-my-opencode")
// #then should return null
// then should return null
expect(result).toBeNull()
})
})
@@ -202,19 +202,19 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
})
test("Gemini models have variant definitions", () => {
// #given the antigravity provider config
// given the antigravity provider config
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
// #when checking Gemini Pro variants
// when checking Gemini Pro variants
const pro = models["antigravity-gemini-3-pro"]
// #then should have low and high variants
// 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
// when checking Gemini Flash variants
const flash = models["antigravity-gemini-3-flash"]
// #then should have minimal, low, medium, high variants
// then should have minimal, low, medium, high variants
expect(flash.variants).toBeTruthy()
expect(flash.variants.minimal).toBeTruthy()
expect(flash.variants.low).toBeTruthy()
@@ -223,14 +223,14 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
})
test("Claude thinking models have variant definitions", () => {
// #given the antigravity provider config
// given the antigravity provider config
const models = (ANTIGRAVITY_PROVIDER_CONFIG as any).google.models as Record<string, any>
// #when checking Claude thinking variants
// 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
// then both should have low and max variants
for (const model of [sonnetThinking, opusThinking]) {
expect(model.variants).toBeTruthy()
expect(model.variants.low).toBeTruthy()
@@ -241,7 +241,7 @@ describe("config-manager ANTIGRAVITY_PROVIDER_CONFIG", () => {
describe("generateOmoConfig - model fallback system", () => {
test("generates native sonnet models when Claude standard subscription", () => {
// #given user has Claude standard subscription (not max20)
// given user has Claude standard subscription (not max20)
const config: InstallConfig = {
hasClaude: true,
isMax20: false,
@@ -253,17 +253,17 @@ describe("generateOmoConfig - model fallback system", () => {
hasKimiForCoding: false,
}
// #when generating config
// when generating config
const result = generateOmoConfig(config)
// #then should use native anthropic sonnet (cost-efficient for standard plan)
// 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
// given user has Claude max20 subscription
const config: InstallConfig = {
hasClaude: true,
isMax20: true,
@@ -275,15 +275,15 @@ describe("generateOmoConfig - model fallback system", () => {
hasKimiForCoding: false,
}
// #when generating config
// when generating config
const result = generateOmoConfig(config)
// #then should use native anthropic opus (max power for max20 plan)
// 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)
// given user has only copilot (no max plan)
const config: InstallConfig = {
hasClaude: false,
isMax20: false,
@@ -295,15 +295,15 @@ describe("generateOmoConfig - model fallback system", () => {
hasKimiForCoding: false,
}
// #when generating config
// when generating config
const result = generateOmoConfig(config)
// #then should use github-copilot sonnet models (copilot fallback)
// 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
// given user has no providers
const config: InstallConfig = {
hasClaude: false,
isMax20: false,
@@ -315,16 +315,16 @@ describe("generateOmoConfig - model fallback system", () => {
hasKimiForCoding: false,
}
// #when generating config
// when generating config
const result = generateOmoConfig(config)
// #then should use ultimate fallback for all agents
// 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
// given user has Z.ai and Claude max20
const config: InstallConfig = {
hasClaude: true,
isMax20: true,
@@ -336,17 +336,17 @@ describe("generateOmoConfig - model fallback system", () => {
hasKimiForCoding: false,
}
// #when generating config
// when generating config
const result = generateOmoConfig(config)
// #then librarian should use zai-coding-plan/glm-4.7
// 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)
// 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
// given user has only ChatGPT subscription
const config: InstallConfig = {
hasClaude: false,
isMax20: false,
@@ -358,19 +358,19 @@ describe("generateOmoConfig - model fallback system", () => {
hasKimiForCoding: false,
}
// #when generating config
// when generating config
const result = generateOmoConfig(config)
// #then Sisyphus should use native OpenAI (fallback within native tier)
// 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)
// 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)
// 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
// given user has Claude max20
const config: InstallConfig = {
hasClaude: true,
isMax20: true,
@@ -382,15 +382,15 @@ describe("generateOmoConfig - model fallback system", () => {
hasKimiForCoding: false,
}
// #when generating config
// when generating config
const result = generateOmoConfig(config)
// #then explore should use haiku (max20 plan uses Claude quota)
// 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
// given user has Claude but not max20
const config: InstallConfig = {
hasClaude: true,
isMax20: false,
@@ -402,10 +402,10 @@ describe("generateOmoConfig - model fallback system", () => {
hasKimiForCoding: false,
}
// #when generating config
// when generating config
const result = generateOmoConfig(config)
// #then explore should use haiku (isMax20 doesn't affect explore anymore)
// 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")
})
})
+24 -24
View File
@@ -4,19 +4,19 @@ import * as auth from "./auth"
describe("auth check", () => {
describe("getAuthProviderInfo", () => {
it("returns anthropic as always available", () => {
// #given anthropic provider
// #when getting info
// given anthropic provider
// when getting info
const info = auth.getAuthProviderInfo("anthropic")
// #then should show plugin installed (builtin)
// then should show plugin installed (builtin)
expect(info.id).toBe("anthropic")
expect(info.pluginInstalled).toBe(true)
})
it("returns correct name for each provider", () => {
// #given each provider
// #when getting info
// #then should have correct names
// given each provider
// when getting info
// then should have correct names
expect(auth.getAuthProviderInfo("anthropic").name).toContain("Claude")
expect(auth.getAuthProviderInfo("openai").name).toContain("ChatGPT")
expect(auth.getAuthProviderInfo("google").name).toContain("Gemini")
@@ -31,7 +31,7 @@ describe("auth check", () => {
})
it("returns pass when plugin installed", async () => {
// #given plugin installed
// given plugin installed
getInfoSpy = spyOn(auth, "getAuthProviderInfo").mockReturnValue({
id: "anthropic",
name: "Anthropic (Claude)",
@@ -39,15 +39,15 @@ describe("auth check", () => {
configured: true,
})
// #when checking
// when checking
const result = await auth.checkAuthProvider("anthropic")
// #then should pass
// then should pass
expect(result.status).toBe("pass")
})
it("returns skip when plugin not installed", async () => {
// #given plugin not installed
// given plugin not installed
getInfoSpy = spyOn(auth, "getAuthProviderInfo").mockReturnValue({
id: "openai",
name: "OpenAI (ChatGPT)",
@@ -55,10 +55,10 @@ describe("auth check", () => {
configured: false,
})
// #when checking
// when checking
const result = await auth.checkAuthProvider("openai")
// #then should skip
// then should skip
expect(result.status).toBe("skip")
expect(result.message).toContain("not installed")
})
@@ -66,11 +66,11 @@ describe("auth check", () => {
describe("checkAnthropicAuth", () => {
it("returns a check result", async () => {
// #given
// #when checking anthropic
// given
// when checking anthropic
const result = await auth.checkAnthropicAuth()
// #then should return valid result
// then should return valid result
expect(result.name).toBeDefined()
expect(["pass", "fail", "warn", "skip"]).toContain(result.status)
})
@@ -78,11 +78,11 @@ describe("auth check", () => {
describe("checkOpenAIAuth", () => {
it("returns a check result", async () => {
// #given
// #when checking openai
// given
// when checking openai
const result = await auth.checkOpenAIAuth()
// #then should return valid result
// then should return valid result
expect(result.name).toBeDefined()
expect(["pass", "fail", "warn", "skip"]).toContain(result.status)
})
@@ -90,11 +90,11 @@ describe("auth check", () => {
describe("checkGoogleAuth", () => {
it("returns a check result", async () => {
// #given
// #when checking google
// given
// when checking google
const result = await auth.checkGoogleAuth()
// #then should return valid result
// then should return valid result
expect(result.name).toBeDefined()
expect(["pass", "fail", "warn", "skip"]).toContain(result.status)
})
@@ -102,11 +102,11 @@ describe("auth check", () => {
describe("getAuthCheckDefinitions", () => {
it("returns definitions for all three providers", () => {
// #given
// #when getting definitions
// given
// when getting definitions
const defs = auth.getAuthCheckDefinitions()
// #then should have 3 definitions
// then should have 3 definitions
expect(defs.length).toBe(3)
expect(defs.every((d) => d.category === "authentication")).toBe(true)
})
+18 -18
View File
@@ -4,11 +4,11 @@ import * as config from "./config"
describe("config check", () => {
describe("validateConfig", () => {
it("returns valid: false for non-existent file", () => {
// #given non-existent file path
// #when validating
// given non-existent file path
// when validating
const result = config.validateConfig("/non/existent/path.json")
// #then should indicate invalid
// then should indicate invalid
expect(result.valid).toBe(false)
expect(result.errors.length).toBeGreaterThan(0)
})
@@ -16,11 +16,11 @@ describe("config check", () => {
describe("getConfigInfo", () => {
it("returns exists: false when no config found", () => {
// #given no config file exists
// #when getting config info
// given no config file exists
// when getting config info
const info = config.getConfigInfo()
// #then should handle gracefully
// then should handle gracefully
expect(typeof info.exists).toBe("boolean")
expect(typeof info.valid).toBe("boolean")
})
@@ -34,7 +34,7 @@ describe("config check", () => {
})
it("returns pass when no config exists (uses defaults)", async () => {
// #given no config file
// given no config file
getInfoSpy = spyOn(config, "getConfigInfo").mockReturnValue({
exists: false,
path: null,
@@ -43,16 +43,16 @@ describe("config check", () => {
errors: [],
})
// #when checking validity
// when checking validity
const result = await config.checkConfigValidity()
// #then should pass with default message
// then should pass with default message
expect(result.status).toBe("pass")
expect(result.message).toContain("default")
})
it("returns pass when config is valid", async () => {
// #given valid config
// given valid config
getInfoSpy = spyOn(config, "getConfigInfo").mockReturnValue({
exists: true,
path: "/home/user/.config/opencode/oh-my-opencode.json",
@@ -61,16 +61,16 @@ describe("config check", () => {
errors: [],
})
// #when checking validity
// when checking validity
const result = await config.checkConfigValidity()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("JSON")
})
it("returns fail when config has validation errors", async () => {
// #given invalid config
// given invalid config
getInfoSpy = spyOn(config, "getConfigInfo").mockReturnValue({
exists: true,
path: "/home/user/.config/opencode/oh-my-opencode.json",
@@ -79,10 +79,10 @@ describe("config check", () => {
errors: ["agents.oracle: Invalid model format"],
})
// #when checking validity
// when checking validity
const result = await config.checkConfigValidity()
// #then should fail with errors
// then should fail with errors
expect(result.status).toBe("fail")
expect(result.details?.some((d) => d.includes("Error"))).toBe(true)
})
@@ -90,11 +90,11 @@ describe("config check", () => {
describe("getConfigCheckDefinition", () => {
it("returns valid check definition", () => {
// #given
// #when getting definition
// given
// when getting definition
const def = config.getConfigCheckDefinition()
// #then should have required properties
// then should have required properties
expect(def.id).toBe("config-validation")
expect(def.category).toBe("configuration")
expect(def.critical).toBe(false)
+24 -24
View File
@@ -4,11 +4,11 @@ import * as deps from "./dependencies"
describe("dependencies check", () => {
describe("checkAstGrepCli", () => {
it("returns dependency info", async () => {
// #given
// #when checking ast-grep cli
// given
// when checking ast-grep cli
const info = await deps.checkAstGrepCli()
// #then should return valid info
// then should return valid info
expect(info.name).toBe("AST-Grep CLI")
expect(info.required).toBe(false)
expect(typeof info.installed).toBe("boolean")
@@ -17,11 +17,11 @@ describe("dependencies check", () => {
describe("checkAstGrepNapi", () => {
it("returns dependency info", async () => {
// #given
// #when checking ast-grep napi
// given
// when checking ast-grep napi
const info = await deps.checkAstGrepNapi()
// #then should return valid info
// then should return valid info
expect(info.name).toBe("AST-Grep NAPI")
expect(info.required).toBe(false)
expect(typeof info.installed).toBe("boolean")
@@ -30,11 +30,11 @@ describe("dependencies check", () => {
describe("checkCommentChecker", () => {
it("returns dependency info", async () => {
// #given
// #when checking comment checker
// given
// when checking comment checker
const info = await deps.checkCommentChecker()
// #then should return valid info
// then should return valid info
expect(info.name).toBe("Comment Checker")
expect(info.required).toBe(false)
expect(typeof info.installed).toBe("boolean")
@@ -49,7 +49,7 @@ describe("dependencies check", () => {
})
it("returns pass when installed", async () => {
// #given ast-grep installed
// given ast-grep installed
checkSpy = spyOn(deps, "checkAstGrepCli").mockResolvedValue({
name: "AST-Grep CLI",
required: false,
@@ -58,16 +58,16 @@ describe("dependencies check", () => {
path: "/usr/local/bin/sg",
})
// #when checking
// when checking
const result = await deps.checkDependencyAstGrepCli()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("0.25.0")
})
it("returns warn when not installed", async () => {
// #given ast-grep not installed
// given ast-grep not installed
checkSpy = spyOn(deps, "checkAstGrepCli").mockResolvedValue({
name: "AST-Grep CLI",
required: false,
@@ -77,10 +77,10 @@ describe("dependencies check", () => {
installHint: "Install: npm install -g @ast-grep/cli",
})
// #when checking
// when checking
const result = await deps.checkDependencyAstGrepCli()
// #then should warn (optional)
// then should warn (optional)
expect(result.status).toBe("warn")
expect(result.message).toContain("optional")
})
@@ -94,7 +94,7 @@ describe("dependencies check", () => {
})
it("returns pass when installed", async () => {
// #given napi installed
// given napi installed
checkSpy = spyOn(deps, "checkAstGrepNapi").mockResolvedValue({
name: "AST-Grep NAPI",
required: false,
@@ -103,10 +103,10 @@ describe("dependencies check", () => {
path: null,
})
// #when checking
// when checking
const result = await deps.checkDependencyAstGrepNapi()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
})
})
@@ -119,7 +119,7 @@ describe("dependencies check", () => {
})
it("returns warn when not installed", async () => {
// #given comment checker not installed
// given comment checker not installed
checkSpy = spyOn(deps, "checkCommentChecker").mockResolvedValue({
name: "Comment Checker",
required: false,
@@ -129,21 +129,21 @@ describe("dependencies check", () => {
installHint: "Hook will be disabled if not available",
})
// #when checking
// when checking
const result = await deps.checkDependencyCommentChecker()
// #then should warn
// then should warn
expect(result.status).toBe("warn")
})
})
describe("getDependencyCheckDefinitions", () => {
it("returns definitions for all dependencies", () => {
// #given
// #when getting definitions
// given
// when getting definitions
const defs = deps.getDependencyCheckDefinitions()
// #then should have 3 definitions
// then should have 3 definitions
expect(defs.length).toBe(3)
expect(defs.every((d) => d.category === "dependencies")).toBe(true)
expect(defs.every((d) => d.critical === false)).toBe(true)
+12 -12
View File
@@ -68,7 +68,7 @@ describe("gh cli check", () => {
})
it("returns warn when gh is not installed", async () => {
// #given gh not installed
// given gh not installed
getInfoSpy = spyOn(gh, "getGhCliInfo").mockResolvedValue({
installed: false,
version: null,
@@ -79,17 +79,17 @@ describe("gh cli check", () => {
error: null,
})
// #when checking
// when checking
const result = await gh.checkGhCli()
// #then should warn (optional)
// then should warn (optional)
expect(result.status).toBe("warn")
expect(result.message).toContain("Not installed")
expect(result.details).toContain("Install: https://cli.github.com/")
})
it("returns warn when gh is installed but not authenticated", async () => {
// #given gh installed but not authenticated
// given gh installed but not authenticated
getInfoSpy = spyOn(gh, "getGhCliInfo").mockResolvedValue({
installed: true,
version: "2.40.0",
@@ -100,10 +100,10 @@ describe("gh cli check", () => {
error: "not logged in",
})
// #when checking
// when checking
const result = await gh.checkGhCli()
// #then should warn about auth
// then should warn about auth
expect(result.status).toBe("warn")
expect(result.message).toContain("2.40.0")
expect(result.message).toContain("not authenticated")
@@ -111,7 +111,7 @@ describe("gh cli check", () => {
})
it("returns pass when gh is installed and authenticated", async () => {
// #given gh installed and authenticated
// given gh installed and authenticated
getInfoSpy = spyOn(gh, "getGhCliInfo").mockResolvedValue({
installed: true,
version: "2.40.0",
@@ -122,10 +122,10 @@ describe("gh cli check", () => {
error: null,
})
// #when checking
// when checking
const result = await gh.checkGhCli()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("2.40.0")
expect(result.message).toContain("octocat")
@@ -136,11 +136,11 @@ describe("gh cli check", () => {
describe("getGhCliCheckDefinition", () => {
it("returns correct check definition", () => {
// #given
// #when getting definition
// given
// when getting definition
const def = gh.getGhCliCheckDefinition()
// #then should have correct properties
// then should have correct properties
expect(def.id).toBe("gh-cli")
expect(def.name).toBe("GitHub CLI")
expect(def.category).toBe("tools")
+24 -24
View File
@@ -5,11 +5,11 @@ import type { LspServerInfo } from "../types"
describe("lsp check", () => {
describe("getLspServersInfo", () => {
it("returns array of server info", async () => {
// #given
// #when getting servers info
// given
// when getting servers info
const servers = await lsp.getLspServersInfo()
// #then should return array with expected structure
// then should return array with expected structure
expect(Array.isArray(servers)).toBe(true)
servers.forEach((s) => {
expect(s.id).toBeDefined()
@@ -19,14 +19,14 @@ describe("lsp check", () => {
})
it("does not spawn 'which' command (windows compatibility)", async () => {
// #given
// given
const spawnSpy = spyOn(Bun, "spawn")
try {
// #when getting servers info
// when getting servers info
await lsp.getLspServersInfo()
// #then should not spawn which
// then should not spawn which
const calls = spawnSpy.mock.calls
const whichCalls = calls.filter((c) => Array.isArray(c) && Array.isArray(c[0]) && c[0][0] === "which")
expect(whichCalls.length).toBe(0)
@@ -38,29 +38,29 @@ describe("lsp check", () => {
describe("getLspServerStats", () => {
it("counts installed servers correctly", () => {
// #given servers with mixed installation status
// given servers with mixed installation status
const servers = [
{ id: "ts", installed: true, extensions: [".ts"], source: "builtin" as const },
{ id: "py", installed: false, extensions: [".py"], source: "builtin" as const },
{ id: "go", installed: true, extensions: [".go"], source: "builtin" as const },
]
// #when getting stats
// when getting stats
const stats = lsp.getLspServerStats(servers)
// #then should count correctly
// then should count correctly
expect(stats.installed).toBe(2)
expect(stats.total).toBe(3)
})
it("handles empty array", () => {
// #given no servers
// given no servers
const servers: LspServerInfo[] = []
// #when getting stats
// when getting stats
const stats = lsp.getLspServerStats(servers)
// #then should return zeros
// then should return zeros
expect(stats.installed).toBe(0)
expect(stats.total).toBe(0)
})
@@ -74,46 +74,46 @@ describe("lsp check", () => {
})
it("returns warn when no servers installed", async () => {
// #given no servers installed
// given no servers installed
getServersSpy = spyOn(lsp, "getLspServersInfo").mockResolvedValue([
{ id: "typescript-language-server", installed: false, extensions: [".ts"], source: "builtin" },
{ id: "pyright", installed: false, extensions: [".py"], source: "builtin" },
])
// #when checking
// when checking
const result = await lsp.checkLspServers()
// #then should warn
// then should warn
expect(result.status).toBe("warn")
expect(result.message).toContain("No LSP servers")
})
it("returns pass when servers installed", async () => {
// #given some servers installed
// given some servers installed
getServersSpy = spyOn(lsp, "getLspServersInfo").mockResolvedValue([
{ id: "typescript-language-server", installed: true, extensions: [".ts"], source: "builtin" },
{ id: "pyright", installed: false, extensions: [".py"], source: "builtin" },
])
// #when checking
// when checking
const result = await lsp.checkLspServers()
// #then should pass with count
// then should pass with count
expect(result.status).toBe("pass")
expect(result.message).toContain("1/2")
})
it("lists installed and missing servers in details", async () => {
// #given mixed installation
// given mixed installation
getServersSpy = spyOn(lsp, "getLspServersInfo").mockResolvedValue([
{ id: "typescript-language-server", installed: true, extensions: [".ts"], source: "builtin" },
{ id: "pyright", installed: false, extensions: [".py"], source: "builtin" },
])
// #when checking
// when checking
const result = await lsp.checkLspServers()
// #then should list both
// then should list both
expect(result.details?.some((d) => d.includes("Installed"))).toBe(true)
expect(result.details?.some((d) => d.includes("Not found"))).toBe(true)
})
@@ -121,11 +121,11 @@ describe("lsp check", () => {
describe("getLspCheckDefinition", () => {
it("returns valid check definition", () => {
// #given
// #when getting definition
// given
// when getting definition
const def = lsp.getLspCheckDefinition()
// #then should have required properties
// then should have required properties
expect(def.id).toBe("lsp-servers")
expect(def.category).toBe("tools")
expect(def.critical).toBe(false)
+18 -18
View File
@@ -4,11 +4,11 @@ import * as mcpOauth from "./mcp-oauth"
describe("mcp-oauth check", () => {
describe("getMcpOAuthCheckDefinition", () => {
it("returns check definition with correct properties", () => {
// #given
// #when getting definition
// given
// when getting definition
const def = mcpOauth.getMcpOAuthCheckDefinition()
// #then should have correct structure
// then should have correct structure
expect(def.id).toBe("mcp-oauth-tokens")
expect(def.name).toBe("MCP OAuth Tokens")
expect(def.category).toBe("tools")
@@ -25,19 +25,19 @@ describe("mcp-oauth check", () => {
})
it("returns skip when no tokens stored", async () => {
// #given no OAuth tokens configured
// given no OAuth tokens configured
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue(null)
// #when checking OAuth tokens
// when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should skip
// then should skip
expect(result.status).toBe("skip")
expect(result.message).toContain("No OAuth")
})
it("returns pass when all tokens valid", async () => {
// #given valid tokens with future expiry (expiresAt is in epoch seconds)
// given valid tokens with future expiry (expiresAt is in epoch seconds)
const futureTime = Math.floor(Date.now() / 1000) + 3600
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
"example.com/resource1": {
@@ -50,17 +50,17 @@ describe("mcp-oauth check", () => {
},
})
// #when checking OAuth tokens
// when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("2")
expect(result.message).toContain("valid")
})
it("returns warn when some tokens expired", async () => {
// #given mix of valid and expired tokens (expiresAt is in epoch seconds)
// given mix of valid and expired tokens (expiresAt is in epoch seconds)
const futureTime = Math.floor(Date.now() / 1000) + 3600
const pastTime = Math.floor(Date.now() / 1000) - 3600
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
@@ -74,10 +74,10 @@ describe("mcp-oauth check", () => {
},
})
// #when checking OAuth tokens
// when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should warn
// then should warn
expect(result.status).toBe("warn")
expect(result.message).toContain("1")
expect(result.message).toContain("expired")
@@ -87,23 +87,23 @@ describe("mcp-oauth check", () => {
})
it("returns pass when tokens have no expiry", async () => {
// #given tokens without expiry info
// given tokens without expiry info
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
"example.com/resource1": {
accessToken: "token1",
},
})
// #when checking OAuth tokens
// when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should pass (no expiry = assume valid)
// then should pass (no expiry = assume valid)
expect(result.status).toBe("pass")
expect(result.message).toContain("1")
})
it("includes token details in output", async () => {
// #given multiple tokens
// given multiple tokens
const futureTime = Math.floor(Date.now() / 1000) + 3600
readStoreSpy = spyOn(mcpOauth, "readTokenStore").mockReturnValue({
"api.example.com/v1": {
@@ -116,10 +116,10 @@ describe("mcp-oauth check", () => {
},
})
// #when checking OAuth tokens
// when checking OAuth tokens
const result = await mcpOauth.checkMcpOAuthTokens()
// #then should list tokens in details
// then should list tokens in details
expect(result.details).toBeDefined()
expect(result.details?.length).toBeGreaterThan(0)
expect(
+24 -24
View File
@@ -4,11 +4,11 @@ import * as mcp from "./mcp"
describe("mcp check", () => {
describe("getBuiltinMcpInfo", () => {
it("returns builtin servers", () => {
// #given
// #when getting builtin info
// given
// when getting builtin info
const servers = mcp.getBuiltinMcpInfo()
// #then should include expected servers
// then should include expected servers
expect(servers.length).toBe(2)
expect(servers.every((s) => s.type === "builtin")).toBe(true)
expect(servers.every((s) => s.enabled === true)).toBe(true)
@@ -19,33 +19,33 @@ describe("mcp check", () => {
describe("getUserMcpInfo", () => {
it("returns empty array when no user config", () => {
// #given no user config exists
// #when getting user info
// given no user config exists
// when getting user info
const servers = mcp.getUserMcpInfo()
// #then should return array (may be empty)
// then should return array (may be empty)
expect(Array.isArray(servers)).toBe(true)
})
})
describe("checkBuiltinMcpServers", () => {
it("returns pass with server count", async () => {
// #given
// #when checking builtin servers
// given
// when checking builtin servers
const result = await mcp.checkBuiltinMcpServers()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("2")
expect(result.message).toContain("enabled")
})
it("lists enabled servers in details", async () => {
// #given
// #when checking builtin servers
// given
// when checking builtin servers
const result = await mcp.checkBuiltinMcpServers()
// #then should list servers
// then should list servers
expect(result.details?.some((d) => d.includes("context7"))).toBe(true)
expect(result.details?.some((d) => d.includes("grep_app"))).toBe(true)
})
@@ -59,41 +59,41 @@ describe("mcp check", () => {
})
it("returns skip when no user config", async () => {
// #given no user servers
// given no user servers
getUserSpy = spyOn(mcp, "getUserMcpInfo").mockReturnValue([])
// #when checking
// when checking
const result = await mcp.checkUserMcpServers()
// #then should skip
// then should skip
expect(result.status).toBe("skip")
expect(result.message).toContain("No user MCP")
})
it("returns pass when valid user servers", async () => {
// #given valid user servers
// given valid user servers
getUserSpy = spyOn(mcp, "getUserMcpInfo").mockReturnValue([
{ id: "custom-mcp", type: "user", enabled: true, valid: true },
])
// #when checking
// when checking
const result = await mcp.checkUserMcpServers()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("1")
})
it("returns warn when servers have issues", async () => {
// #given invalid server config
// given invalid server config
getUserSpy = spyOn(mcp, "getUserMcpInfo").mockReturnValue([
{ id: "bad-mcp", type: "user", enabled: true, valid: false, error: "Missing command" },
])
// #when checking
// when checking
const result = await mcp.checkUserMcpServers()
// #then should warn
// then should warn
expect(result.status).toBe("warn")
expect(result.details?.some((d) => d.includes("Invalid"))).toBe(true)
})
@@ -101,11 +101,11 @@ describe("mcp check", () => {
describe("getMcpCheckDefinitions", () => {
it("returns definitions for builtin and user", () => {
// #given
// #when getting definitions
// given
// when getting definitions
const defs = mcp.getMcpCheckDefinitions()
// #then should have 2 definitions
// then should have 2 definitions
expect(defs.length).toBe(2)
expect(defs.every((d) => d.category === "tools")).toBe(true)
expect(defs.map((d) => d.id)).toContain("mcp-builtin")
+19 -19
View File
@@ -2,16 +2,16 @@ import { describe, it, expect, beforeEach, afterEach, spyOn, mock } from "bun:te
describe("model-resolution check", () => {
describe("getModelResolutionInfo", () => {
// #given: Model requirements are defined in model-requirements.ts
// #when: Getting model resolution info
// #then: Returns info for all agents and categories with their provider chains
// given: Model requirements are defined in model-requirements.ts
// when: Getting model resolution info
// then: Returns info for all agents and categories with their provider chains
it("returns agent requirements with provider chains", async () => {
const { getModelResolutionInfo } = await import("./model-resolution")
const info = getModelResolutionInfo()
// #then: Should have agent entries
// then: Should have agent entries
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
expect(sisyphus).toBeDefined()
expect(sisyphus!.requirement.fallbackChain[0]?.model).toBe("claude-opus-4-5")
@@ -24,7 +24,7 @@ describe("model-resolution check", () => {
const info = getModelResolutionInfo()
// #then: Should have category entries
// then: Should have category entries
const visual = info.categories.find((c) => c.name === "visual-engineering")
expect(visual).toBeDefined()
expect(visual!.requirement.fallbackChain[0]?.model).toBe("gemini-3-pro")
@@ -33,14 +33,14 @@ describe("model-resolution check", () => {
})
describe("getModelResolutionInfoWithOverrides", () => {
// #given: User has overrides in oh-my-opencode.json
// #when: Getting resolution info with config
// #then: Shows user override in Step 1 position
// given: User has overrides in oh-my-opencode.json
// when: Getting resolution info with config
// then: Shows user override in Step 1 position
it("shows user override for agent when configured", async () => {
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
// #given: User has override for oracle agent
// given: User has override for oracle agent
const mockConfig = {
agents: {
oracle: { model: "anthropic/claude-opus-4-5" },
@@ -49,7 +49,7 @@ describe("model-resolution check", () => {
const info = getModelResolutionInfoWithOverrides(mockConfig)
// #then: Oracle should show the override
// then: Oracle should show the override
const oracle = info.agents.find((a) => a.name === "oracle")
expect(oracle).toBeDefined()
expect(oracle!.userOverride).toBe("anthropic/claude-opus-4-5")
@@ -59,7 +59,7 @@ describe("model-resolution check", () => {
it("shows user override for category when configured", async () => {
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
// #given: User has override for visual-engineering category
// given: User has override for visual-engineering category
const mockConfig = {
categories: {
"visual-engineering": { model: "openai/gpt-5.2" },
@@ -68,7 +68,7 @@ describe("model-resolution check", () => {
const info = getModelResolutionInfoWithOverrides(mockConfig)
// #then: visual-engineering should show the override
// then: visual-engineering should show the override
const visual = info.categories.find((c) => c.name === "visual-engineering")
expect(visual).toBeDefined()
expect(visual!.userOverride).toBe("openai/gpt-5.2")
@@ -78,12 +78,12 @@ describe("model-resolution check", () => {
it("shows provider fallback when no override exists", async () => {
const { getModelResolutionInfoWithOverrides } = await import("./model-resolution")
// #given: No overrides configured
// given: No overrides configured
const mockConfig = {}
const info = getModelResolutionInfoWithOverrides(mockConfig)
// #then: Should show provider fallback chain
// then: Should show provider fallback chain
const sisyphus = info.agents.find((a) => a.name === "sisyphus")
expect(sisyphus).toBeDefined()
expect(sisyphus!.userOverride).toBeUndefined()
@@ -93,16 +93,16 @@ describe("model-resolution check", () => {
})
describe("checkModelResolution", () => {
// #given: Doctor check is executed
// #when: Running the model resolution check
// #then: Returns pass with details showing resolution flow
// given: Doctor check is executed
// when: Running the model resolution check
// then: Returns pass with details showing resolution flow
it("returns pass or warn status with agent and category counts", async () => {
const { checkModelResolution } = await import("./model-resolution")
const result = await checkModelResolution()
// #then: Should pass (with cache) or warn (no cache) and show counts
// then: Should pass (with cache) or warn (no cache) and show counts
// In CI without model cache, status is "warn"; locally with cache, status is "pass"
expect(["pass", "warn"]).toContain(result.status)
expect(result.message).toMatch(/\d+ agents?, \d+ categories?/)
@@ -113,7 +113,7 @@ describe("model-resolution check", () => {
const result = await checkModelResolution()
// #then: Details should contain agent/category resolution info
// then: Details should contain agent/category resolution info
expect(result.details).toBeDefined()
expect(result.details!.length).toBeGreaterThan(0)
// Should have Available Models and Configured Models headers
+51 -51
View File
@@ -5,106 +5,106 @@ import { MIN_OPENCODE_VERSION } from "../constants"
describe("opencode check", () => {
describe("compareVersions", () => {
it("returns true when current >= minimum", () => {
// #given versions where current is greater
// #when comparing
// #then should return true
// given versions where current is greater
// when comparing
// then should return true
expect(opencode.compareVersions("1.0.200", "1.0.150")).toBe(true)
expect(opencode.compareVersions("1.1.0", "1.0.150")).toBe(true)
expect(opencode.compareVersions("2.0.0", "1.0.150")).toBe(true)
})
it("returns true when versions are equal", () => {
// #given equal versions
// #when comparing
// #then should return true
// given equal versions
// when comparing
// then should return true
expect(opencode.compareVersions("1.0.150", "1.0.150")).toBe(true)
})
it("returns false when current < minimum", () => {
// #given version below minimum
// #when comparing
// #then should return false
// given version below minimum
// when comparing
// then should return false
expect(opencode.compareVersions("1.0.100", "1.0.150")).toBe(false)
expect(opencode.compareVersions("0.9.0", "1.0.150")).toBe(false)
})
it("handles version prefixes", () => {
// #given version with v prefix
// #when comparing
// #then should strip prefix and compare correctly
// given version with v prefix
// when comparing
// then should strip prefix and compare correctly
expect(opencode.compareVersions("v1.0.200", "1.0.150")).toBe(true)
})
it("handles prerelease versions", () => {
// #given prerelease version
// #when comparing
// #then should use base version
// given prerelease version
// when comparing
// then should use base version
expect(opencode.compareVersions("1.0.200-beta.1", "1.0.150")).toBe(true)
})
})
describe("command helpers", () => {
it("selects where on Windows", () => {
// #given win32 platform
// #when selecting lookup command
// #then should use where
// given win32 platform
// when selecting lookup command
// then should use where
expect(opencode.getBinaryLookupCommand("win32")).toBe("where")
})
it("selects which on non-Windows", () => {
// #given linux platform
// #when selecting lookup command
// #then should use which
// given linux platform
// when selecting lookup command
// then should use which
expect(opencode.getBinaryLookupCommand("linux")).toBe("which")
expect(opencode.getBinaryLookupCommand("darwin")).toBe("which")
})
it("parses command output into paths", () => {
// #given raw output with multiple lines and spaces
// given raw output with multiple lines and spaces
const output = "C:\\\\bin\\\\opencode.ps1\r\nC:\\\\bin\\\\opencode.exe\n\n"
// #when parsing
// when parsing
const paths = opencode.parseBinaryPaths(output)
// #then should return trimmed, non-empty paths
// then should return trimmed, non-empty paths
expect(paths).toEqual(["C:\\\\bin\\\\opencode.ps1", "C:\\\\bin\\\\opencode.exe"])
})
it("prefers exe/cmd/bat over ps1 on Windows", () => {
// #given windows paths
// given windows paths
const paths = [
"C:\\\\bin\\\\opencode.ps1",
"C:\\\\bin\\\\opencode.cmd",
"C:\\\\bin\\\\opencode.exe",
]
// #when selecting binary
// when selecting binary
const selected = opencode.selectBinaryPath(paths, "win32")
// #then should prefer exe
// then should prefer exe
expect(selected).toBe("C:\\\\bin\\\\opencode.exe")
})
it("falls back to ps1 when it is the only Windows candidate", () => {
// #given only ps1 path
// given only ps1 path
const paths = ["C:\\\\bin\\\\opencode.ps1"]
// #when selecting binary
// when selecting binary
const selected = opencode.selectBinaryPath(paths, "win32")
// #then should return ps1 path
// then should return ps1 path
expect(selected).toBe("C:\\\\bin\\\\opencode.ps1")
})
it("builds PowerShell command for ps1 on Windows", () => {
// #given a ps1 path on Windows
// given a ps1 path on Windows
const command = opencode.buildVersionCommand(
"C:\\\\bin\\\\opencode.ps1",
"win32"
)
// #when building command
// #then should use PowerShell
// when building command
// then should use PowerShell
expect(command).toEqual([
"powershell",
"-NoProfile",
@@ -117,15 +117,15 @@ describe("opencode check", () => {
})
it("builds direct command for non-ps1 binaries", () => {
// #given an exe on Windows and a binary on linux
// given an exe on Windows and a binary on linux
const winCommand = opencode.buildVersionCommand(
"C:\\\\bin\\\\opencode.exe",
"win32"
)
const linuxCommand = opencode.buildVersionCommand("opencode", "linux")
// #when building commands
// #then should execute directly
// when building commands
// then should execute directly
expect(winCommand).toEqual(["C:\\\\bin\\\\opencode.exe", "--version"])
expect(linuxCommand).toEqual(["opencode", "--version"])
})
@@ -133,13 +133,13 @@ describe("opencode check", () => {
describe("getOpenCodeInfo", () => {
it("returns installed: false when binary not found", async () => {
// #given no opencode binary
// given no opencode binary
const spy = spyOn(opencode, "findOpenCodeBinary").mockResolvedValue(null)
// #when getting info
// when getting info
const info = await opencode.getOpenCodeInfo()
// #then should indicate not installed
// then should indicate not installed
expect(info.installed).toBe(false)
expect(info.version).toBeNull()
expect(info.path).toBeNull()
@@ -157,7 +157,7 @@ describe("opencode check", () => {
})
it("returns fail when not installed", async () => {
// #given opencode not installed
// given opencode not installed
getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({
installed: false,
version: null,
@@ -165,10 +165,10 @@ describe("opencode check", () => {
binary: null,
})
// #when checking installation
// when checking installation
const result = await opencode.checkOpenCodeInstallation()
// #then should fail with installation hint
// then should fail with installation hint
expect(result.status).toBe("fail")
expect(result.message).toContain("not installed")
expect(result.details).toBeDefined()
@@ -176,7 +176,7 @@ describe("opencode check", () => {
})
it("returns warn when version below minimum", async () => {
// #given old version installed
// given old version installed
getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({
installed: true,
version: "1.0.100",
@@ -184,17 +184,17 @@ describe("opencode check", () => {
binary: "opencode",
})
// #when checking installation
// when checking installation
const result = await opencode.checkOpenCodeInstallation()
// #then should warn about old version
// then should warn about old version
expect(result.status).toBe("warn")
expect(result.message).toContain("below minimum")
expect(result.details?.some((d) => d.includes(MIN_OPENCODE_VERSION))).toBe(true)
})
it("returns pass when properly installed", async () => {
// #given current version installed
// given current version installed
getInfoSpy = spyOn(opencode, "getOpenCodeInfo").mockResolvedValue({
installed: true,
version: "1.0.200",
@@ -202,10 +202,10 @@ describe("opencode check", () => {
binary: "opencode",
})
// #when checking installation
// when checking installation
const result = await opencode.checkOpenCodeInstallation()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("1.0.200")
})
@@ -213,11 +213,11 @@ describe("opencode check", () => {
describe("getOpenCodeCheckDefinition", () => {
it("returns valid check definition", () => {
// #given
// #when getting definition
// given
// when getting definition
const def = opencode.getOpenCodeCheckDefinition()
// #then should have required properties
// then should have required properties
expect(def.id).toBe("opencode-installation")
expect(def.category).toBe("installation")
expect(def.critical).toBe(true)
+18 -18
View File
@@ -4,9 +4,9 @@ import * as plugin from "./plugin"
describe("plugin check", () => {
describe("getPluginInfo", () => {
it("returns registered: false when config not found", () => {
// #given no config file exists
// #when getting plugin info
// #then should indicate not registered
// given no config file exists
// when getting plugin info
// then should indicate not registered
const info = plugin.getPluginInfo()
expect(typeof info.registered).toBe("boolean")
expect(typeof info.isPinned).toBe("boolean")
@@ -21,7 +21,7 @@ describe("plugin check", () => {
})
it("returns fail when config file not found", async () => {
// #given no config file
// given no config file
getInfoSpy = spyOn(plugin, "getPluginInfo").mockReturnValue({
registered: false,
configPath: null,
@@ -30,16 +30,16 @@ describe("plugin check", () => {
pinnedVersion: null,
})
// #when checking registration
// when checking registration
const result = await plugin.checkPluginRegistration()
// #then should fail with hint
// then should fail with hint
expect(result.status).toBe("fail")
expect(result.message).toContain("not found")
})
it("returns fail when plugin not registered", async () => {
// #given config exists but plugin not registered
// given config exists but plugin not registered
getInfoSpy = spyOn(plugin, "getPluginInfo").mockReturnValue({
registered: false,
configPath: "/home/user/.config/opencode/opencode.json",
@@ -48,16 +48,16 @@ describe("plugin check", () => {
pinnedVersion: null,
})
// #when checking registration
// when checking registration
const result = await plugin.checkPluginRegistration()
// #then should fail
// then should fail
expect(result.status).toBe("fail")
expect(result.message).toContain("not registered")
})
it("returns pass when plugin registered", async () => {
// #given plugin registered
// given plugin registered
getInfoSpy = spyOn(plugin, "getPluginInfo").mockReturnValue({
registered: true,
configPath: "/home/user/.config/opencode/opencode.json",
@@ -66,16 +66,16 @@ describe("plugin check", () => {
pinnedVersion: null,
})
// #when checking registration
// when checking registration
const result = await plugin.checkPluginRegistration()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("Registered")
})
it("indicates pinned version when applicable", async () => {
// #given plugin pinned to version
// given plugin pinned to version
getInfoSpy = spyOn(plugin, "getPluginInfo").mockReturnValue({
registered: true,
configPath: "/home/user/.config/opencode/opencode.json",
@@ -84,10 +84,10 @@ describe("plugin check", () => {
pinnedVersion: "2.7.0",
})
// #when checking registration
// when checking registration
const result = await plugin.checkPluginRegistration()
// #then should show pinned version
// then should show pinned version
expect(result.status).toBe("pass")
expect(result.message).toContain("pinned")
expect(result.message).toContain("2.7.0")
@@ -96,11 +96,11 @@ describe("plugin check", () => {
describe("getPluginCheckDefinition", () => {
it("returns valid check definition", () => {
// #given
// #when getting definition
// given
// when getting definition
const def = plugin.getPluginCheckDefinition()
// #then should have required properties
// then should have required properties
expect(def.id).toBe("plugin-registration")
expect(def.category).toBe("installation")
expect(def.critical).toBe(true)
+24 -24
View File
@@ -4,11 +4,11 @@ import * as version from "./version"
describe("version check", () => {
describe("getVersionInfo", () => {
it("returns version check info structure", async () => {
// #given
// #when getting version info
// given
// when getting version info
const info = await version.getVersionInfo()
// #then should have expected structure
// then should have expected structure
expect(typeof info.isUpToDate).toBe("boolean")
expect(typeof info.isLocalDev).toBe("boolean")
expect(typeof info.isPinned).toBe("boolean")
@@ -23,7 +23,7 @@ describe("version check", () => {
})
it("returns pass when in local dev mode", async () => {
// #given local dev mode
// given local dev mode
getInfoSpy = spyOn(version, "getVersionInfo").mockResolvedValue({
currentVersion: "local-dev",
latestVersion: "2.7.0",
@@ -32,16 +32,16 @@ describe("version check", () => {
isPinned: false,
})
// #when checking
// when checking
const result = await version.checkVersionStatus()
// #then should pass with dev message
// then should pass with dev message
expect(result.status).toBe("pass")
expect(result.message).toContain("local development")
})
it("returns pass when pinned", async () => {
// #given pinned version
// given pinned version
getInfoSpy = spyOn(version, "getVersionInfo").mockResolvedValue({
currentVersion: "2.6.0",
latestVersion: "2.7.0",
@@ -50,16 +50,16 @@ describe("version check", () => {
isPinned: true,
})
// #when checking
// when checking
const result = await version.checkVersionStatus()
// #then should pass with pinned message
// then should pass with pinned message
expect(result.status).toBe("pass")
expect(result.message).toContain("Pinned")
})
it("returns warn when unable to determine version", async () => {
// #given no version info
// given no version info
getInfoSpy = spyOn(version, "getVersionInfo").mockResolvedValue({
currentVersion: null,
latestVersion: "2.7.0",
@@ -68,16 +68,16 @@ describe("version check", () => {
isPinned: false,
})
// #when checking
// when checking
const result = await version.checkVersionStatus()
// #then should warn
// then should warn
expect(result.status).toBe("warn")
expect(result.message).toContain("Unable to determine")
})
it("returns warn when network error", async () => {
// #given network error
// given network error
getInfoSpy = spyOn(version, "getVersionInfo").mockResolvedValue({
currentVersion: "2.6.0",
latestVersion: null,
@@ -86,16 +86,16 @@ describe("version check", () => {
isPinned: false,
})
// #when checking
// when checking
const result = await version.checkVersionStatus()
// #then should warn
// then should warn
expect(result.status).toBe("warn")
expect(result.details?.some((d) => d.includes("network"))).toBe(true)
})
it("returns warn when update available", async () => {
// #given update available
// given update available
getInfoSpy = spyOn(version, "getVersionInfo").mockResolvedValue({
currentVersion: "2.6.0",
latestVersion: "2.7.0",
@@ -104,10 +104,10 @@ describe("version check", () => {
isPinned: false,
})
// #when checking
// when checking
const result = await version.checkVersionStatus()
// #then should warn with update info
// then should warn with update info
expect(result.status).toBe("warn")
expect(result.message).toContain("Update available")
expect(result.message).toContain("2.6.0")
@@ -115,7 +115,7 @@ describe("version check", () => {
})
it("returns pass when up to date", async () => {
// #given up to date
// given up to date
getInfoSpy = spyOn(version, "getVersionInfo").mockResolvedValue({
currentVersion: "2.7.0",
latestVersion: "2.7.0",
@@ -124,10 +124,10 @@ describe("version check", () => {
isPinned: false,
})
// #when checking
// when checking
const result = await version.checkVersionStatus()
// #then should pass
// then should pass
expect(result.status).toBe("pass")
expect(result.message).toContain("Up to date")
})
@@ -135,11 +135,11 @@ describe("version check", () => {
describe("getVersionCheckDefinition", () => {
it("returns valid check definition", () => {
// #given
// #when getting definition
// given
// when getting definition
const def = version.getVersionCheckDefinition()
// #then should have required properties
// then should have required properties
expect(def.id).toBe("version-status")
expect(def.category).toBe("updates")
expect(def.critical).toBe(false)
+3 -3
View File
@@ -3,13 +3,13 @@ import packageJson from "../../package.json" with { type: "json" }
describe("CLI version", () => {
it("reads version from package.json as valid semver", () => {
//#given
// given
const semverRegex = /^\d+\.\d+\.\d+(-[\w.]+)?$/
//#when
// when
const version = packageJson.version
//#then
// then
expect(version).toMatch(semverRegex)
expect(typeof version).toBe("string")
expect(version.length).toBeGreaterThan(0)
+16 -16
View File
@@ -17,7 +17,7 @@ describe("install CLI - binary check behavior", () => {
let getOpenCodeVersionSpy: ReturnType<typeof spyOn>
beforeEach(() => {
// #given temporary config directory
// given temporary config directory
tempDir = join(tmpdir(), `omo-test-${Date.now()}-${Math.random().toString(36).slice(2)}`)
mkdirSync(tempDir, { recursive: true })
@@ -49,7 +49,7 @@ describe("install CLI - binary check behavior", () => {
})
test("non-TUI mode: should show warning but continue when OpenCode binary not found", async () => {
// #given OpenCode binary is NOT installed
// given OpenCode binary is NOT installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
@@ -63,24 +63,24 @@ describe("install CLI - binary check behavior", () => {
zaiCodingPlan: "no",
}
// #when running install
// when running install
const exitCode = await install(args)
// #then should return success (0), not failure (1)
// then should return success (0), not failure (1)
expect(exitCode).toBe(0)
// #then should have printed a warning (not error)
// then should have printed a warning (not error)
const allCalls = mockConsoleLog.mock.calls.flat().join("\n")
expect(allCalls).toContain("[!]") // warning symbol
expect(allCalls).toContain("OpenCode")
})
test("non-TUI mode: should create opencode.json with plugin even when binary not found", async () => {
// #given OpenCode binary is NOT installed
// given OpenCode binary is NOT installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null)
// #given mock npm fetch
// given mock npm fetch
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
@@ -98,28 +98,28 @@ describe("install CLI - binary check behavior", () => {
zaiCodingPlan: "no",
}
// #when running install
// when running install
const exitCode = await install(args)
// #then should create opencode.json
// then should create opencode.json
const configPath = join(tempDir, "opencode.json")
expect(existsSync(configPath)).toBe(true)
// #then opencode.json should have plugin entry
// then opencode.json should have plugin entry
const config = JSON.parse(readFileSync(configPath, "utf-8"))
expect(config.plugin).toBeDefined()
expect(config.plugin.some((p: string) => p.includes("oh-my-opencode"))).toBe(true)
// #then exit code should be 0 (success)
// then exit code should be 0 (success)
expect(exitCode).toBe(0)
})
test("non-TUI mode: should still succeed and complete all steps when binary exists", async () => {
// #given OpenCode binary IS installed
// given OpenCode binary IS installed
isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true)
getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.0.200")
// #given mock npm fetch
// given mock npm fetch
globalThis.fetch = mock(() =>
Promise.resolve({
ok: true,
@@ -137,13 +137,13 @@ describe("install CLI - binary check behavior", () => {
zaiCodingPlan: "no",
}
// #when running install
// when running install
const exitCode = await install(args)
// #then should return success
// then should return success
expect(exitCode).toBe(0)
// #then should have printed success (OK symbol)
// then should have printed success (OK symbol)
const allCalls = mockConsoleLog.mock.calls.flat().join("\n")
expect(allCalls).toContain("[OK]")
expect(allCalls).toContain("OpenCode 1.0.200")
+96 -96
View File
@@ -20,103 +20,103 @@ function createConfig(overrides: Partial<InstallConfig> = {}): InstallConfig {
describe("generateModelConfig", () => {
describe("no providers available", () => {
test("returns ULTIMATE_FALLBACK for all agents and categories when no providers", () => {
// #given no providers are available
// given no providers are available
const config = createConfig()
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use ULTIMATE_FALLBACK for everything
// then should use ULTIMATE_FALLBACK for everything
expect(result).toMatchSnapshot()
})
})
describe("single native provider", () => {
test("uses Claude models when only Claude is available", () => {
// #given only Claude is available
// given only Claude is available
const config = createConfig({ hasClaude: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use Claude models per NATIVE_FALLBACK_CHAINS
// then should use Claude models per NATIVE_FALLBACK_CHAINS
expect(result).toMatchSnapshot()
})
test("uses Claude models with isMax20 flag", () => {
// #given Claude is available with Max 20 plan
// given Claude is available with Max 20 plan
const config = createConfig({ hasClaude: true, isMax20: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models for Sisyphus
// then should use higher capability models for Sisyphus
expect(result).toMatchSnapshot()
})
test("uses OpenAI models when only OpenAI is available", () => {
// #given only OpenAI is available
// given only OpenAI is available
const config = createConfig({ hasOpenAI: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use OpenAI models
// then should use OpenAI models
expect(result).toMatchSnapshot()
})
test("uses OpenAI models with isMax20 flag", () => {
// #given OpenAI is available with Max 20 plan
// given OpenAI is available with Max 20 plan
const config = createConfig({ hasOpenAI: true, isMax20: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
// then should use higher capability models
expect(result).toMatchSnapshot()
})
test("uses Gemini models when only Gemini is available", () => {
// #given only Gemini is available
// given only Gemini is available
const config = createConfig({ hasGemini: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use Gemini models
// then should use Gemini models
expect(result).toMatchSnapshot()
})
test("uses Gemini models with isMax20 flag", () => {
// #given Gemini is available with Max 20 plan
// given Gemini is available with Max 20 plan
const config = createConfig({ hasGemini: true, isMax20: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
// then should use higher capability models
expect(result).toMatchSnapshot()
})
})
describe("all native providers", () => {
test("uses preferred models from fallback chains when all natives available", () => {
// #given all native providers are available
// given all native providers are available
const config = createConfig({
hasClaude: true,
hasOpenAI: true,
hasGemini: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use first provider in each fallback chain
// then should use first provider in each fallback chain
expect(result).toMatchSnapshot()
})
test("uses preferred models with isMax20 flag when all natives available", () => {
// #given all native providers are available with Max 20 plan
// given all native providers are available with Max 20 plan
const config = createConfig({
hasClaude: true,
hasOpenAI: true,
@@ -124,156 +124,156 @@ describe("generateModelConfig", () => {
isMax20: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
// then should use higher capability models
expect(result).toMatchSnapshot()
})
})
describe("fallback providers", () => {
test("uses OpenCode Zen models when only OpenCode Zen is available", () => {
// #given only OpenCode Zen is available
// given only OpenCode Zen is available
const config = createConfig({ hasOpencodeZen: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use OPENCODE_ZEN_MODELS
// then should use OPENCODE_ZEN_MODELS
expect(result).toMatchSnapshot()
})
test("uses OpenCode Zen models with isMax20 flag", () => {
// #given OpenCode Zen is available with Max 20 plan
// given OpenCode Zen is available with Max 20 plan
const config = createConfig({ hasOpencodeZen: true, isMax20: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
// then should use higher capability models
expect(result).toMatchSnapshot()
})
test("uses GitHub Copilot models when only Copilot is available", () => {
// #given only GitHub Copilot is available
// given only GitHub Copilot is available
const config = createConfig({ hasCopilot: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use GITHUB_COPILOT_MODELS
// then should use GITHUB_COPILOT_MODELS
expect(result).toMatchSnapshot()
})
test("uses GitHub Copilot models with isMax20 flag", () => {
// #given GitHub Copilot is available with Max 20 plan
// given GitHub Copilot is available with Max 20 plan
const config = createConfig({ hasCopilot: true, isMax20: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
// then should use higher capability models
expect(result).toMatchSnapshot()
})
test("uses ZAI model for librarian when only ZAI is available", () => {
// #given only ZAI is available
// given only ZAI is available
const config = createConfig({ hasZaiCodingPlan: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use ZAI_MODEL for librarian
// then should use ZAI_MODEL for librarian
expect(result).toMatchSnapshot()
})
test("uses ZAI model for librarian with isMax20 flag", () => {
// #given ZAI is available with Max 20 plan
// given ZAI is available with Max 20 plan
const config = createConfig({ hasZaiCodingPlan: true, isMax20: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use ZAI_MODEL for librarian
// then should use ZAI_MODEL for librarian
expect(result).toMatchSnapshot()
})
})
describe("mixed provider scenarios", () => {
test("uses Claude + OpenCode Zen combination", () => {
// #given Claude and OpenCode Zen are available
// given Claude and OpenCode Zen are available
const config = createConfig({
hasClaude: true,
hasOpencodeZen: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should prefer Claude (native) over OpenCode Zen
// then should prefer Claude (native) over OpenCode Zen
expect(result).toMatchSnapshot()
})
test("uses OpenAI + Copilot combination", () => {
// #given OpenAI and Copilot are available
// given OpenAI and Copilot are available
const config = createConfig({
hasOpenAI: true,
hasCopilot: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should prefer OpenAI (native) over Copilot
// then should prefer OpenAI (native) over Copilot
expect(result).toMatchSnapshot()
})
test("uses Claude + ZAI combination (librarian uses ZAI)", () => {
// #given Claude and ZAI are available
// given Claude and ZAI are available
const config = createConfig({
hasClaude: true,
hasZaiCodingPlan: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then librarian should use ZAI, others use Claude
// then librarian should use ZAI, others use Claude
expect(result).toMatchSnapshot()
})
test("uses Gemini + Claude combination (explore uses Gemini)", () => {
// #given Gemini and Claude are available
// given Gemini and Claude are available
const config = createConfig({
hasGemini: true,
hasClaude: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use Gemini flash
// then explore should use Gemini flash
expect(result).toMatchSnapshot()
})
test("uses all fallback providers together", () => {
// #given all fallback providers are available
// given all fallback providers are available
const config = createConfig({
hasOpencodeZen: true,
hasCopilot: true,
hasZaiCodingPlan: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should prefer OpenCode Zen, but librarian uses ZAI
// then should prefer OpenCode Zen, but librarian uses ZAI
expect(result).toMatchSnapshot()
})
test("uses all providers together", () => {
// #given all providers are available
// given all providers are available
const config = createConfig({
hasClaude: true,
hasOpenAI: true,
@@ -283,15 +283,15 @@ describe("generateModelConfig", () => {
hasZaiCodingPlan: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should prefer native providers, librarian uses ZAI
// then should prefer native providers, librarian uses ZAI
expect(result).toMatchSnapshot()
})
test("uses all providers with isMax20 flag", () => {
// #given all providers are available with Max 20 plan
// given all providers are available with Max 20 plan
const config = createConfig({
hasClaude: true,
hasOpenAI: true,
@@ -302,131 +302,131 @@ describe("generateModelConfig", () => {
isMax20: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should use higher capability models
// then should use higher capability models
expect(result).toMatchSnapshot()
})
})
describe("explore agent special cases", () => {
test("explore uses gpt-5-nano when only Gemini available (no Claude)", () => {
// #given only Gemini is available (no Claude)
// given only Gemini is available (no Claude)
const config = createConfig({ hasGemini: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use gpt-5-nano (Claude haiku not available)
// then explore should use gpt-5-nano (Claude haiku not available)
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
})
test("explore uses Claude haiku when Claude available", () => {
// #given Claude is available
// given Claude is available
const config = createConfig({ hasClaude: true, isMax20: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use claude-haiku-4-5
// then explore should use claude-haiku-4-5
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
})
test("explore uses Claude haiku regardless of isMax20 flag", () => {
// #given Claude is available without Max 20 plan
// given Claude is available without Max 20 plan
const config = createConfig({ hasClaude: true, isMax20: false })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use claude-haiku-4-5 (isMax20 doesn't affect explore)
// then explore should use claude-haiku-4-5 (isMax20 doesn't affect explore)
expect(result.agents?.explore?.model).toBe("anthropic/claude-haiku-4-5")
})
test("explore uses gpt-5-nano when only OpenAI available", () => {
// #given only OpenAI is available
// given only OpenAI is available
const config = createConfig({ hasOpenAI: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use gpt-5-nano (fallback)
// then explore should use gpt-5-nano (fallback)
expect(result.agents?.explore?.model).toBe("opencode/gpt-5-nano")
})
test("explore uses gpt-5-mini when only Copilot available", () => {
// #given only Copilot is available
// given only Copilot is available
const config = createConfig({ hasCopilot: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then explore should use gpt-5-mini (Copilot fallback)
// then explore should use gpt-5-mini (Copilot fallback)
expect(result.agents?.explore?.model).toBe("github-copilot/gpt-5-mini")
})
})
describe("Sisyphus agent special cases", () => {
test("Sisyphus uses sisyphus-high capability when isMax20 is true", () => {
// #given Claude is available with Max 20 plan
// given Claude is available with Max 20 plan
const config = createConfig({ hasClaude: true, isMax20: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then Sisyphus should use opus (sisyphus-high)
// then Sisyphus should use opus (sisyphus-high)
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-opus-4-5")
})
test("Sisyphus uses sisyphus-low capability when isMax20 is false", () => {
// #given Claude is available without Max 20 plan
// given Claude is available without Max 20 plan
const config = createConfig({ hasClaude: true, isMax20: false })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then Sisyphus should use sonnet (sisyphus-low)
// then Sisyphus should use sonnet (sisyphus-low)
expect(result.agents?.sisyphus?.model).toBe("anthropic/claude-sonnet-4-5")
})
})
describe("librarian agent special cases", () => {
test("librarian uses ZAI when ZAI is available regardless of other providers", () => {
// #given ZAI and Claude are available
// given ZAI and Claude are available
const config = createConfig({
hasClaude: true,
hasZaiCodingPlan: true,
})
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then librarian should use ZAI_MODEL
// then librarian should use ZAI_MODEL
expect(result.agents?.librarian?.model).toBe("zai-coding-plan/glm-4.7")
})
test("librarian uses claude-sonnet when ZAI not available but Claude is", () => {
// #given only Claude is available (no ZAI)
// given only Claude is available (no ZAI)
const config = createConfig({ hasClaude: true })
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then librarian should use claude-sonnet-4-5 (third in fallback chain after ZAI and opencode/glm)
// then librarian should use claude-sonnet-4-5 (third in fallback chain after ZAI and opencode/glm)
expect(result.agents?.librarian?.model).toBe("anthropic/claude-sonnet-4-5")
})
})
describe("schema URL", () => {
test("always includes correct schema URL", () => {
// #given any config
// given any config
const config = createConfig()
// #when generateModelConfig is called
// when generateModelConfig is called
const result = generateModelConfig(config)
// #then should include correct schema URL
// then should include correct schema URL
expect(result.$schema).toBe(
"https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json"
)
+21 -21
View File
@@ -30,20 +30,20 @@ const createMockContext = (overrides: {
describe("checkCompletionConditions", () => {
it("returns true when no todos and no children", async () => {
// #given
// given
spyOn(console, "log").mockImplementation(() => {})
const ctx = createMockContext()
const { checkCompletionConditions } = await import("./completion")
// #when
// when
const result = await checkCompletionConditions(ctx)
// #then
// then
expect(result).toBe(true)
})
it("returns false when incomplete todos exist", async () => {
// #given
// given
spyOn(console, "log").mockImplementation(() => {})
const ctx = createMockContext({
todo: [
@@ -53,15 +53,15 @@ describe("checkCompletionConditions", () => {
})
const { checkCompletionConditions } = await import("./completion")
// #when
// when
const result = await checkCompletionConditions(ctx)
// #then
// then
expect(result).toBe(false)
})
it("returns true when all todos completed or cancelled", async () => {
// #given
// given
spyOn(console, "log").mockImplementation(() => {})
const ctx = createMockContext({
todo: [
@@ -71,15 +71,15 @@ describe("checkCompletionConditions", () => {
})
const { checkCompletionConditions } = await import("./completion")
// #when
// when
const result = await checkCompletionConditions(ctx)
// #then
// then
expect(result).toBe(true)
})
it("returns false when child session is busy", async () => {
// #given
// given
spyOn(console, "log").mockImplementation(() => {})
const ctx = createMockContext({
childrenBySession: {
@@ -90,15 +90,15 @@ describe("checkCompletionConditions", () => {
})
const { checkCompletionConditions } = await import("./completion")
// #when
// when
const result = await checkCompletionConditions(ctx)
// #then
// then
expect(result).toBe(false)
})
it("returns true when all children idle", async () => {
// #given
// given
spyOn(console, "log").mockImplementation(() => {})
const ctx = createMockContext({
childrenBySession: {
@@ -113,15 +113,15 @@ describe("checkCompletionConditions", () => {
})
const { checkCompletionConditions } = await import("./completion")
// #when
// when
const result = await checkCompletionConditions(ctx)
// #then
// then
expect(result).toBe(true)
})
it("returns false when grandchild is busy (recursive)", async () => {
// #given
// given
spyOn(console, "log").mockImplementation(() => {})
const ctx = createMockContext({
childrenBySession: {
@@ -136,15 +136,15 @@ describe("checkCompletionConditions", () => {
})
const { checkCompletionConditions } = await import("./completion")
// #when
// when
const result = await checkCompletionConditions(ctx)
// #then
// then
expect(result).toBe(false)
})
it("returns true when all descendants idle (recursive)", async () => {
// #given
// given
spyOn(console, "log").mockImplementation(() => {})
const ctx = createMockContext({
childrenBySession: {
@@ -161,10 +161,10 @@ describe("checkCompletionConditions", () => {
})
const { checkCompletionConditions } = await import("./completion")
// #when
// when
const result = await checkCompletionConditions(ctx)
// #then
// then
expect(result).toBe(true)
})
})
+39 -39
View File
@@ -17,56 +17,56 @@ async function* toAsyncIterable<T>(items: T[]): AsyncIterable<T> {
describe("serializeError", () => {
it("returns 'Unknown error' for null/undefined", () => {
// #given / #when / #then
// given / when / then
expect(serializeError(null)).toBe("Unknown error")
expect(serializeError(undefined)).toBe("Unknown error")
})
it("returns message from Error instance", () => {
// #given
// given
const error = new Error("Something went wrong")
// #when / #then
// when / then
expect(serializeError(error)).toBe("Something went wrong")
})
it("returns string as-is", () => {
// #given / #when / #then
// given / when / then
expect(serializeError("Direct error message")).toBe("Direct error message")
})
it("extracts message from plain object", () => {
// #given
// given
const errorObj = { message: "Object error message", code: "ERR_001" }
// #when / #then
// when / then
expect(serializeError(errorObj)).toBe("Object error message")
})
it("extracts message from nested error object", () => {
// #given
// given
const errorObj = { error: { message: "Nested error message" } }
// #when / #then
// when / then
expect(serializeError(errorObj)).toBe("Nested error message")
})
it("extracts message from data.message path", () => {
// #given
// given
const errorObj = { data: { message: "Data error message" } }
// #when / #then
// when / then
expect(serializeError(errorObj)).toBe("Data error message")
})
it("JSON stringifies object without message property", () => {
// #given
// given
const errorObj = { code: "ERR_001", status: 500 }
// #when
// when
const result = serializeError(errorObj)
// #then
// then
expect(result).toContain("ERR_001")
expect(result).toContain("500")
})
@@ -74,10 +74,10 @@ describe("serializeError", () => {
describe("createEventState", () => {
it("creates initial state with correct defaults", () => {
// #given / #when
// given / when
const state = createEventState()
// #then
// then
expect(state.mainSessionIdle).toBe(false)
expect(state.lastOutput).toBe("")
expect(state.lastPartText).toBe("")
@@ -88,7 +88,7 @@ describe("createEventState", () => {
describe("event handling", () => {
it("session.idle sets mainSessionIdle to true for matching session", async () => {
// #given
// given
const ctx = createMockContext("my-session")
const state = createEventState()
@@ -100,15 +100,15 @@ describe("event handling", () => {
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
// when
await processEvents(ctx, events, state)
// #then
// then
expect(state.mainSessionIdle).toBe(true)
})
it("session.idle does not affect state for different session", async () => {
// #given
// given
const ctx = createMockContext("my-session")
const state = createEventState()
@@ -120,15 +120,15 @@ describe("event handling", () => {
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
// when
await processEvents(ctx, events, state)
// #then
// then
expect(state.mainSessionIdle).toBe(false)
})
it("hasReceivedMeaningfulWork is false initially after session.idle", async () => {
// #given - session goes idle without any assistant output (race condition scenario)
// given - session goes idle without any assistant output (race condition scenario)
const ctx = createMockContext("my-session")
const state = createEventState()
@@ -140,16 +140,16 @@ describe("event handling", () => {
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
// when
await processEvents(ctx, events, state)
// #then - idle but no meaningful work yet
// then - idle but no meaningful work yet
expect(state.mainSessionIdle).toBe(true)
expect(state.hasReceivedMeaningfulWork).toBe(false)
})
it("message.updated with assistant role sets hasReceivedMeaningfulWork", async () => {
// #given
// given
const ctx = createMockContext("my-session")
const state = createEventState()
@@ -163,15 +163,15 @@ describe("event handling", () => {
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
// when
await processEvents(ctx, events, state)
// #then
// then
expect(state.hasReceivedMeaningfulWork).toBe(true)
})
it("message.updated with user role does not set hasReceivedMeaningfulWork", async () => {
// #given - user message should not count as meaningful work
// given - user message should not count as meaningful work
const ctx = createMockContext("my-session")
const state = createEventState()
@@ -185,15 +185,15 @@ describe("event handling", () => {
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
// when
await processEvents(ctx, events, state)
// #then - user role should not count as meaningful work
// then - user role should not count as meaningful work
expect(state.hasReceivedMeaningfulWork).toBe(false)
})
it("tool.execute sets hasReceivedMeaningfulWork", async () => {
// #given
// given
const ctx = createMockContext("my-session")
const state = createEventState()
@@ -209,15 +209,15 @@ describe("event handling", () => {
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
// when
await processEvents(ctx, events, state)
// #then
// then
expect(state.hasReceivedMeaningfulWork).toBe(true)
})
it("tool.execute from different session does not set hasReceivedMeaningfulWork", async () => {
// #given
// given
const ctx = createMockContext("my-session")
const state = createEventState()
@@ -233,15 +233,15 @@ describe("event handling", () => {
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
// when
await processEvents(ctx, events, state)
// #then - different session's tool call shouldn't count
// then - different session's tool call shouldn't count
expect(state.hasReceivedMeaningfulWork).toBe(false)
})
it("session.status with busy type sets mainSessionIdle to false", async () => {
// #given
// given
const ctx = createMockContext("my-session")
const state: EventState = {
mainSessionIdle: true,
@@ -261,10 +261,10 @@ describe("event handling", () => {
const events = toAsyncIterable([payload])
const { processEvents } = await import("./events")
// #when
// when
await processEvents(ctx, events, state)
// #then
// then
expect(state.mainSessionIdle).toBe(false)
})
})