From bb14537b14022e7e18dcd649f8fa0b2a9532f7f6 Mon Sep 17 00:00:00 2001 From: justsisyphus Date: Thu, 22 Jan 2026 22:47:43 +0900 Subject: [PATCH] test(cli): add install command tests with snapshots Add comprehensive tests for the install command with snapshot testing for generated configurations. Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-opencode) Co-authored-by: Sisyphus --- .../__snapshots__/model-fallback.test.ts.snap | 1393 +++++++++++++++++ src/cli/install.test.ts | 151 ++ src/cli/install.ts | 55 +- 3 files changed, 1570 insertions(+), 29 deletions(-) create mode 100644 src/cli/__snapshots__/model-fallback.test.ts.snap create mode 100644 src/cli/install.test.ts diff --git a/src/cli/__snapshots__/model-fallback.test.ts.snap b/src/cli/__snapshots__/model-fallback.test.ts.snap new file mode 100644 index 00000000..b412674c --- /dev/null +++ b/src/cli/__snapshots__/model-fallback.test.ts.snap @@ -0,0 +1,1393 @@ +// Bun Snapshot v1, https://bun.sh/docs/test/snapshots + +exports[`generateModelConfig no providers available returns ULTIMATE_FALLBACK for all agents and categories when no providers 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "opencode/glm-4.7-free", + }, + "Metis (Plan Consultant)": { + "model": "opencode/glm-4.7-free", + }, + "Momus (Plan Reviewer)": { + "model": "opencode/glm-4.7-free", + }, + "Prometheus (Planner)": { + "model": "opencode/glm-4.7-free", + }, + "Sisyphus": { + "model": "opencode/glm-4.7-free", + }, + "explore": { + "model": "opencode/glm-4.7-free", + }, + "librarian": { + "model": "opencode/glm-4.7-free", + }, + "multimodal-looker": { + "model": "opencode/glm-4.7-free", + }, + "oracle": { + "model": "opencode/glm-4.7-free", + }, + }, + "categories": { + "artistry": { + "model": "opencode/glm-4.7-free", + }, + "quick": { + "model": "opencode/glm-4.7-free", + }, + "ultrabrain": { + "model": "opencode/glm-4.7-free", + }, + "unspecified-high": { + "model": "opencode/glm-4.7-free", + }, + "unspecified-low": { + "model": "opencode/glm-4.7-free", + }, + "visual-engineering": { + "model": "opencode/glm-4.7-free", + }, + "writing": { + "model": "opencode/glm-4.7-free", + }, + }, +} +`; + +exports[`generateModelConfig single native provider uses Claude models when only Claude is available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "anthropic/claude-opus-4-5", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-sonnet-4-5", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "anthropic/claude-sonnet-4-5", + }, + "multimodal-looker": { + "model": "anthropic/claude-haiku-4-5", + }, + "oracle": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + }, + "categories": { + "artistry": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "unspecified-high": { + "model": "anthropic/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "writing": { + "model": "anthropic/claude-sonnet-4-5", + }, + }, +} +`; + +exports[`generateModelConfig single native provider uses Claude models with isMax20 flag 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "anthropic/claude-opus-4-5", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "explore": { + "model": "anthropic/claude-haiku-4-5", + }, + "librarian": { + "model": "anthropic/claude-sonnet-4-5", + }, + "multimodal-looker": { + "model": "anthropic/claude-haiku-4-5", + }, + "oracle": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + }, + "categories": { + "artistry": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "unspecified-high": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "writing": { + "model": "anthropic/claude-sonnet-4-5", + }, + }, +} +`; + +exports[`generateModelConfig single native provider uses OpenAI models when only OpenAI is available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "openai/gpt-5.2", + }, + "Metis (Plan Consultant)": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "Momus (Plan Reviewer)": { + "model": "openai/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "Sisyphus": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "opencode/glm-4.7-free", + }, + "multimodal-looker": { + "model": "openai/gpt-5.2", + }, + "oracle": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "openai/gpt-5.2", + }, + "quick": { + "model": "openai/gpt-5.1-codex-mini", + }, + "ultrabrain": { + "model": "openai/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "openai/gpt-5.2", + }, + "unspecified-low": { + "model": "openai/gpt-5.2", + }, + "visual-engineering": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "writing": { + "model": "openai/gpt-5.2", + }, + }, +} +`; + +exports[`generateModelConfig single native provider uses OpenAI models with isMax20 flag 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "openai/gpt-5.2", + }, + "Metis (Plan Consultant)": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "Momus (Plan Reviewer)": { + "model": "openai/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "Sisyphus": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "opencode/glm-4.7-free", + }, + "multimodal-looker": { + "model": "openai/gpt-5.2", + }, + "oracle": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "openai/gpt-5.2", + }, + "quick": { + "model": "openai/gpt-5.1-codex-mini", + }, + "ultrabrain": { + "model": "openai/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "unspecified-low": { + "model": "openai/gpt-5.2", + }, + "visual-engineering": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + "writing": { + "model": "openai/gpt-5.2", + }, + }, +} +`; + +exports[`generateModelConfig single native provider uses Gemini models when only Gemini is available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "google/gemini-3-pro-preview", + }, + "Metis (Plan Consultant)": { + "model": "google/gemini-3-pro-preview", + }, + "Momus (Plan Reviewer)": { + "model": "google/gemini-3-pro-preview", + }, + "Prometheus (Planner)": { + "model": "google/gemini-3-pro-preview", + }, + "Sisyphus": { + "model": "google/gemini-3-pro-preview", + }, + "explore": { + "model": "google/gemini-3-flash-preview", + }, + "librarian": { + "model": "opencode/glm-4.7-free", + }, + "multimodal-looker": { + "model": "google/gemini-3-flash-preview", + }, + "oracle": { + "model": "google/gemini-3-pro-preview", + }, + }, + "categories": { + "artistry": { + "model": "google/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "google/gemini-3-flash-preview", + }, + "ultrabrain": { + "model": "google/gemini-3-pro-preview", + }, + "unspecified-high": { + "model": "google/gemini-3-flash-preview", + }, + "unspecified-low": { + "model": "google/gemini-3-flash-preview", + }, + "visual-engineering": { + "model": "google/gemini-3-pro-preview", + }, + "writing": { + "model": "google/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig single native provider uses Gemini models with isMax20 flag 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "google/gemini-3-pro-preview", + }, + "Metis (Plan Consultant)": { + "model": "google/gemini-3-pro-preview", + }, + "Momus (Plan Reviewer)": { + "model": "google/gemini-3-pro-preview", + }, + "Prometheus (Planner)": { + "model": "google/gemini-3-pro-preview", + }, + "Sisyphus": { + "model": "google/gemini-3-pro-preview", + }, + "explore": { + "model": "google/gemini-3-flash-preview", + }, + "librarian": { + "model": "opencode/glm-4.7-free", + }, + "multimodal-looker": { + "model": "google/gemini-3-flash-preview", + }, + "oracle": { + "model": "google/gemini-3-pro-preview", + }, + }, + "categories": { + "artistry": { + "model": "google/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "google/gemini-3-flash-preview", + }, + "ultrabrain": { + "model": "google/gemini-3-pro-preview", + }, + "unspecified-high": { + "model": "google/gemini-3-pro-preview", + }, + "unspecified-low": { + "model": "google/gemini-3-flash-preview", + }, + "visual-engineering": { + "model": "google/gemini-3-pro-preview", + }, + "writing": { + "model": "google/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig all native providers uses preferred models from fallback chains when all natives available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "openai/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-sonnet-4-5", + }, + "explore": { + "model": "google/gemini-3-flash-preview", + }, + "librarian": { + "model": "anthropic/claude-sonnet-4-5", + }, + "multimodal-looker": { + "model": "google/gemini-3-flash-preview", + }, + "oracle": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "google/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "openai/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "anthropic/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "google/gemini-3-pro-preview", + }, + "writing": { + "model": "google/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig all native providers uses preferred models with isMax20 flag when all natives available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "openai/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "explore": { + "model": "google/gemini-3-flash-preview", + }, + "librarian": { + "model": "anthropic/claude-sonnet-4-5", + }, + "multimodal-looker": { + "model": "google/gemini-3-flash-preview", + }, + "oracle": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "google/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "openai/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "google/gemini-3-pro-preview", + }, + "writing": { + "model": "google/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig fallback providers uses OpenCode Zen models when only OpenCode Zen is available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "opencode/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "opencode/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "opencode/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "opencode/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "opencode/claude-sonnet-4-5", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "opencode/glm-4.7-free", + }, + "multimodal-looker": { + "model": "opencode/gemini-3-flash-preview", + }, + "oracle": { + "model": "opencode/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "opencode/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "opencode/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "opencode/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "opencode/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "opencode/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "opencode/gemini-3-pro-preview", + }, + "writing": { + "model": "opencode/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig fallback providers uses OpenCode Zen models with isMax20 flag 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "opencode/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "opencode/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "opencode/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "opencode/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "opencode/claude-opus-4-5", + "variant": "max", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "opencode/glm-4.7-free", + }, + "multimodal-looker": { + "model": "opencode/gemini-3-flash-preview", + }, + "oracle": { + "model": "opencode/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "opencode/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "opencode/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "opencode/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "opencode/claude-opus-4-5", + "variant": "max", + }, + "unspecified-low": { + "model": "opencode/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "opencode/gemini-3-pro-preview", + }, + "writing": { + "model": "opencode/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig fallback providers uses GitHub Copilot models when only Copilot is available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "github-copilot/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "multimodal-looker": { + "model": "github-copilot/gemini-3-flash-preview", + }, + "oracle": { + "model": "github-copilot/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "github-copilot/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "github-copilot/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "github-copilot/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "github-copilot/gemini-3-pro-preview", + }, + "writing": { + "model": "github-copilot/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig fallback providers uses GitHub Copilot models with isMax20 flag 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "github-copilot/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "multimodal-looker": { + "model": "github-copilot/gemini-3-flash-preview", + }, + "oracle": { + "model": "github-copilot/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "github-copilot/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "github-copilot/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "github-copilot/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "unspecified-low": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "github-copilot/gemini-3-pro-preview", + }, + "writing": { + "model": "github-copilot/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig fallback providers uses ZAI model for librarian when only ZAI is available 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "opencode/glm-4.7-free", + }, + "Metis (Plan Consultant)": { + "model": "opencode/glm-4.7-free", + }, + "Momus (Plan Reviewer)": { + "model": "opencode/glm-4.7-free", + }, + "Prometheus (Planner)": { + "model": "opencode/glm-4.7-free", + }, + "Sisyphus": { + "model": "opencode/glm-4.7-free", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "zai-coding-plan/glm-4.7", + }, + "multimodal-looker": { + "model": "opencode/glm-4.7-free", + }, + "oracle": { + "model": "opencode/glm-4.7-free", + }, + }, + "categories": { + "artistry": { + "model": "opencode/glm-4.7-free", + }, + "quick": { + "model": "opencode/glm-4.7-free", + }, + "ultrabrain": { + "model": "opencode/glm-4.7-free", + }, + "unspecified-high": { + "model": "opencode/glm-4.7-free", + }, + "unspecified-low": { + "model": "opencode/glm-4.7-free", + }, + "visual-engineering": { + "model": "opencode/glm-4.7-free", + }, + "writing": { + "model": "opencode/glm-4.7-free", + }, + }, +} +`; + +exports[`generateModelConfig fallback providers uses ZAI model for librarian with isMax20 flag 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "opencode/glm-4.7-free", + }, + "Metis (Plan Consultant)": { + "model": "opencode/glm-4.7-free", + }, + "Momus (Plan Reviewer)": { + "model": "opencode/glm-4.7-free", + }, + "Prometheus (Planner)": { + "model": "opencode/glm-4.7-free", + }, + "Sisyphus": { + "model": "opencode/glm-4.7-free", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "zai-coding-plan/glm-4.7", + }, + "multimodal-looker": { + "model": "opencode/glm-4.7-free", + }, + "oracle": { + "model": "opencode/glm-4.7-free", + }, + }, + "categories": { + "artistry": { + "model": "opencode/glm-4.7-free", + }, + "quick": { + "model": "opencode/glm-4.7-free", + }, + "ultrabrain": { + "model": "opencode/glm-4.7-free", + }, + "unspecified-high": { + "model": "opencode/glm-4.7-free", + }, + "unspecified-low": { + "model": "opencode/glm-4.7-free", + }, + "visual-engineering": { + "model": "opencode/glm-4.7-free", + }, + "writing": { + "model": "opencode/glm-4.7-free", + }, + }, +} +`; + +exports[`generateModelConfig mixed provider scenarios uses Claude + OpenCode Zen combination 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "opencode/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-sonnet-4-5", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "opencode/glm-4.7-free", + }, + "multimodal-looker": { + "model": "opencode/gemini-3-flash-preview", + }, + "oracle": { + "model": "opencode/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "opencode/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "opencode/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "anthropic/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "opencode/gemini-3-pro-preview", + }, + "writing": { + "model": "opencode/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig mixed provider scenarios uses OpenAI + Copilot combination 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "openai/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "multimodal-looker": { + "model": "github-copilot/gemini-3-flash-preview", + }, + "oracle": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "github-copilot/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "github-copilot/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "openai/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "github-copilot/gemini-3-pro-preview", + }, + "writing": { + "model": "github-copilot/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig mixed provider scenarios uses Claude + ZAI combination (librarian uses ZAI) 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "anthropic/claude-opus-4-5", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-sonnet-4-5", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "zai-coding-plan/glm-4.7", + }, + "multimodal-looker": { + "model": "anthropic/claude-haiku-4-5", + }, + "oracle": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + }, + "categories": { + "artistry": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "unspecified-high": { + "model": "anthropic/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "writing": { + "model": "anthropic/claude-sonnet-4-5", + }, + }, +} +`; + +exports[`generateModelConfig mixed provider scenarios uses Gemini + Claude combination (explore uses Gemini) 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "anthropic/claude-opus-4-5", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-sonnet-4-5", + }, + "explore": { + "model": "google/gemini-3-flash-preview", + }, + "librarian": { + "model": "anthropic/claude-sonnet-4-5", + }, + "multimodal-looker": { + "model": "google/gemini-3-flash-preview", + }, + "oracle": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + }, + "categories": { + "artistry": { + "model": "google/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "unspecified-high": { + "model": "anthropic/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "google/gemini-3-pro-preview", + }, + "writing": { + "model": "google/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig mixed provider scenarios uses all fallback providers together 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "github-copilot/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "github-copilot/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "explore": { + "model": "opencode/grok-code", + }, + "librarian": { + "model": "zai-coding-plan/glm-4.7", + }, + "multimodal-looker": { + "model": "github-copilot/gemini-3-flash-preview", + }, + "oracle": { + "model": "github-copilot/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "github-copilot/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "github-copilot/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "github-copilot/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "github-copilot/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "github-copilot/gemini-3-pro-preview", + }, + "writing": { + "model": "github-copilot/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig mixed provider scenarios uses all providers together 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "openai/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-sonnet-4-5", + }, + "explore": { + "model": "google/gemini-3-flash-preview", + }, + "librarian": { + "model": "zai-coding-plan/glm-4.7", + }, + "multimodal-looker": { + "model": "google/gemini-3-flash-preview", + }, + "oracle": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "google/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "openai/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "anthropic/claude-sonnet-4-5", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "google/gemini-3-pro-preview", + }, + "writing": { + "model": "google/gemini-3-flash-preview", + }, + }, +} +`; + +exports[`generateModelConfig mixed provider scenarios uses all providers with isMax20 flag 1`] = ` +{ + "$schema": "https://raw.githubusercontent.com/code-yeongyu/oh-my-opencode/master/assets/oh-my-opencode.schema.json", + "agents": { + "Atlas": { + "model": "anthropic/claude-sonnet-4-5", + }, + "Metis (Plan Consultant)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Momus (Plan Reviewer)": { + "model": "openai/gpt-5.2", + "variant": "medium", + }, + "Prometheus (Planner)": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "Sisyphus": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "explore": { + "model": "google/gemini-3-flash-preview", + }, + "librarian": { + "model": "zai-coding-plan/glm-4.7", + }, + "multimodal-looker": { + "model": "google/gemini-3-flash-preview", + }, + "oracle": { + "model": "openai/gpt-5.2", + "variant": "high", + }, + }, + "categories": { + "artistry": { + "model": "google/gemini-3-pro-preview", + "variant": "max", + }, + "quick": { + "model": "anthropic/claude-haiku-4-5", + }, + "ultrabrain": { + "model": "openai/gpt-5.2-codex", + "variant": "xhigh", + }, + "unspecified-high": { + "model": "anthropic/claude-opus-4-5", + "variant": "max", + }, + "unspecified-low": { + "model": "anthropic/claude-sonnet-4-5", + }, + "visual-engineering": { + "model": "google/gemini-3-pro-preview", + }, + "writing": { + "model": "google/gemini-3-flash-preview", + }, + }, +} +`; diff --git a/src/cli/install.test.ts b/src/cli/install.test.ts new file mode 100644 index 00000000..a17fcb4d --- /dev/null +++ b/src/cli/install.test.ts @@ -0,0 +1,151 @@ +import { describe, expect, test, mock, beforeEach, afterEach, spyOn } from "bun:test" +import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" +import { install } from "./install" +import * as configManager from "./config-manager" +import type { InstallArgs } from "./types" + +// Mock console methods to capture output +const mockConsoleLog = mock(() => {}) +const mockConsoleError = mock(() => {}) + +describe("install CLI - binary check behavior", () => { + let tempDir: string + let originalEnv: string | undefined + let isOpenCodeInstalledSpy: ReturnType + let getOpenCodeVersionSpy: ReturnType + + beforeEach(() => { + // #given temporary config directory + tempDir = join(tmpdir(), `omo-test-${Date.now()}-${Math.random().toString(36).slice(2)}`) + mkdirSync(tempDir, { recursive: true }) + + originalEnv = process.env.OPENCODE_CONFIG_DIR + process.env.OPENCODE_CONFIG_DIR = tempDir + + // Reset config context + configManager.resetConfigContext() + configManager.initConfigContext("opencode", null) + + // Capture console output + console.log = mockConsoleLog + mockConsoleLog.mockClear() + }) + + afterEach(() => { + if (originalEnv !== undefined) { + process.env.OPENCODE_CONFIG_DIR = originalEnv + } else { + delete process.env.OPENCODE_CONFIG_DIR + } + + if (existsSync(tempDir)) { + rmSync(tempDir, { recursive: true, force: true }) + } + + isOpenCodeInstalledSpy?.mockRestore() + getOpenCodeVersionSpy?.mockRestore() + }) + + test("non-TUI mode: should show warning but continue when OpenCode binary not found", async () => { + // #given OpenCode binary is NOT installed + isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false) + getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null) + + const args: InstallArgs = { + tui: false, + claude: "yes", + openai: "no", + gemini: "no", + copilot: "no", + opencodeZen: "no", + zaiCodingPlan: "no", + } + + // #when running install + const exitCode = await install(args) + + // #then should return success (0), not failure (1) + expect(exitCode).toBe(0) + + // #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 + isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(false) + getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue(null) + + // #given mock npm fetch + globalThis.fetch = mock(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ latest: "3.0.0" }), + } as Response) + ) as unknown as typeof fetch + + const args: InstallArgs = { + tui: false, + claude: "yes", + openai: "no", + gemini: "no", + copilot: "no", + opencodeZen: "no", + zaiCodingPlan: "no", + } + + // #when running install + const exitCode = await install(args) + + // #then should create opencode.json + const configPath = join(tempDir, "opencode.json") + expect(existsSync(configPath)).toBe(true) + + // #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) + expect(exitCode).toBe(0) + }) + + test("non-TUI mode: should still succeed and complete all steps when binary exists", async () => { + // #given OpenCode binary IS installed + isOpenCodeInstalledSpy = spyOn(configManager, "isOpenCodeInstalled").mockResolvedValue(true) + getOpenCodeVersionSpy = spyOn(configManager, "getOpenCodeVersion").mockResolvedValue("1.0.200") + + // #given mock npm fetch + globalThis.fetch = mock(() => + Promise.resolve({ + ok: true, + json: () => Promise.resolve({ latest: "3.0.0" }), + } as Response) + ) as unknown as typeof fetch + + const args: InstallArgs = { + tui: false, + claude: "yes", + openai: "no", + gemini: "no", + copilot: "no", + opencodeZen: "no", + zaiCodingPlan: "no", + } + + // #when running install + const exitCode = await install(args) + + // #then should return success + expect(exitCode).toBe(0) + + // #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") + }) +}) diff --git a/src/cli/install.ts b/src/cli/install.ts index 30f13ecf..6f9ce982 100644 --- a/src/cli/install.ts +++ b/src/cli/install.ts @@ -16,13 +16,13 @@ import packageJson from "../../package.json" with { type: "json" } const VERSION = packageJson.version const SYMBOLS = { - check: color.green("✓"), - cross: color.red("✗"), - arrow: color.cyan("→"), - bullet: color.dim("•"), - info: color.blue("ℹ"), - warn: color.yellow("⚠"), - star: color.yellow("★"), + check: color.green("[OK]"), + cross: color.red("[X]"), + arrow: color.cyan("->"), + bullet: color.dim("*"), + info: color.blue("[i]"), + warn: color.yellow("[!]"), + star: color.yellow("*"), } function formatProvider(name: string, enabled: boolean, detail?: string): string { @@ -295,14 +295,13 @@ async function runNonTuiInstall(args: InstallArgs): Promise { printStep(step++, totalSteps, "Checking OpenCode installation...") const installed = await isOpenCodeInstalled() - if (!installed) { - printError("OpenCode is not installed on this system.") - printInfo("Visit https://opencode.ai/docs for installation instructions") - return 1 - } - const version = await getOpenCodeVersion() - printSuccess(`OpenCode ${version ?? ""} detected`) + if (!installed) { + printWarning("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.") + printInfo("Visit https://opencode.ai/docs for installation instructions") + } else { + printSuccess(`OpenCode ${version ?? ""} detected`) + } if (isUpdate) { const initial = detectedToInitialValues(detected) @@ -351,7 +350,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise { if (!config.hasClaude) { console.log() - console.log(color.bgRed(color.white(color.bold(" ⚠️ CRITICAL WARNING ")))) + console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING ")))) console.log() console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5."))) console.log(color.red(" Without Claude, you may experience significantly degraded performance:")) @@ -375,7 +374,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise { `${color.bold("Pro Tip:")} Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` + `All features work like magic—parallel agents, background tasks,\n` + `deep exploration, and relentless execution until completion.`, - "🪄 The Magic Word" + "The Magic Word" ) console.log(`${SYMBOLS.star} ${color.yellow("If you found this helpful, consider starring the repo!")}`) @@ -390,7 +389,7 @@ async function runNonTuiInstall(args: InstallArgs): Promise { (config.hasClaude ? ` ${SYMBOLS.bullet} Anthropic ${color.gray("→ Claude Pro/Max")}\n` : "") + (config.hasGemini ? ` ${SYMBOLS.bullet} Google ${color.gray("→ OAuth with Antigravity")}\n` : "") + (config.hasCopilot ? ` ${SYMBOLS.bullet} GitHub ${color.gray("→ Copilot")}` : ""), - "🔐 Authenticate Your Providers" + "Authenticate Your Providers" ) } @@ -416,16 +415,14 @@ export async function install(args: InstallArgs): Promise { s.start("Checking OpenCode installation") const installed = await isOpenCodeInstalled() - if (!installed) { - s.stop("OpenCode is not installed") - p.log.error("OpenCode is not installed on this system.") - p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide") - p.outro(color.red("Please install OpenCode first.")) - return 1 - } - const version = await getOpenCodeVersion() - s.stop(`OpenCode ${version ?? "installed"} ${color.green("✓")}`) + if (!installed) { + s.stop(`OpenCode binary not found ${color.yellow("[!]")}`) + p.log.warn("OpenCode binary not found. Plugin will be configured, but you'll need to install OpenCode to use it.") + p.note("Visit https://opencode.ai/docs for installation instructions", "Installation Guide") + } else { + s.stop(`OpenCode ${version ?? "installed"} ${color.green("[OK]")}`) + } const config = await runTuiMode(detected) if (!config) return 1 @@ -470,7 +467,7 @@ export async function install(args: InstallArgs): Promise { if (!config.hasClaude) { console.log() - console.log(color.bgRed(color.white(color.bold(" ⚠️ CRITICAL WARNING ")))) + console.log(color.bgRed(color.white(color.bold(" CRITICAL WARNING ")))) console.log() console.log(color.red(color.bold(" Sisyphus agent is STRONGLY optimized for Claude Opus 4.5."))) console.log(color.red(" Without Claude, you may experience significantly degraded performance:")) @@ -495,7 +492,7 @@ export async function install(args: InstallArgs): Promise { `Include ${color.cyan("ultrawork")} (or ${color.cyan("ulw")}) in your prompt.\n` + `All features work like magic—parallel agents, background tasks,\n` + `deep exploration, and relentless execution until completion.`, - "🪄 The Magic Word" + "The Magic Word" ) p.log.message(`${color.yellow("★")} If you found this helpful, consider starring the repo!`) @@ -510,7 +507,7 @@ export async function install(args: InstallArgs): Promise { if (config.hasCopilot) providers.push(`GitHub ${color.gray("→ Copilot")}`) console.log() - console.log(color.bold("🔐 Authenticate Your Providers")) + console.log(color.bold("Authenticate Your Providers")) console.log() console.log(` Run ${color.cyan("opencode auth login")} and select:`) for (const provider of providers) {