opencrabs 0.5.1

The autonomous, self-improving AI agent. Single Rust binary. Every channel. Recommended: the 40MB prebuilt binary for macOS, Linux and Windows: https://github.com/adolfousier/opencrabs/releases
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
//! Regression tests for provider config section resolution.
//!
//! These tests ensure that adding or reordering providers can NEVER corrupt
//! config.toml by writing to the wrong section again (2026-04-28 bug where
//! custom provider selection wrote to [providers.anthropic]).
//!
//! Coverage:
//! - Every KNOWN_PROVIDER maps to the correct TOML section
//! - TUI PROVIDERS are in sync with KNOWN_PROVIDERS
//! - config_section() returns correct values for built-ins and customs
//! - is_first_time() checks all providers including new ones

use crate::utils::providers::{
    KNOWN_PROVIDERS, config_section, find_provider_meta, normalize_provider_name,
};

// ── KNOWN_PROVIDERS section mapping ─────────────────────────────────

#[test]
fn known_provider_anthropic_section() {
    let meta = find_provider_meta("anthropic").expect("anthropic must exist");
    assert_eq!(meta.config_section, "providers.anthropic");
    assert_eq!(meta.id, "anthropic");
    assert!(meta.needs_api_key);
}

#[test]
fn known_provider_openai_section() {
    let meta = find_provider_meta("openai").expect("openai must exist");
    assert_eq!(meta.config_section, "providers.openai");
}

#[test]
fn known_provider_github_section() {
    let meta = find_provider_meta("github").expect("github must exist");
    assert_eq!(meta.config_section, "providers.github");
}

#[test]
fn known_provider_gemini_section() {
    let meta = find_provider_meta("gemini").expect("gemini must exist");
    assert_eq!(meta.config_section, "providers.gemini");
}

#[test]
fn known_provider_openrouter_section() {
    let meta = find_provider_meta("openrouter").expect("openrouter must exist");
    assert_eq!(meta.config_section, "providers.openrouter");
}

#[test]
fn known_provider_minimax_section() {
    let meta = find_provider_meta("minimax").expect("minimax must exist");
    assert_eq!(meta.config_section, "providers.minimax");
}

#[test]
fn known_provider_zai_section() {
    // Canonical id resolves directly...
    let meta = find_provider_meta("zai").expect("zai must exist");
    assert_eq!(meta.id, "zai");
    assert_eq!(meta.config_section, "providers.zai");
}

#[test]
fn known_provider_legacy_zhipu_still_resolves() {
    // ...and the legacy `zhipu` spelling resolves through the alias (#1448).
    let meta = find_provider_meta("zhipu").expect("legacy zhipu must resolve");
    assert_eq!(meta.id, "zai");
    assert_eq!(meta.config_section, "providers.zai");
}

#[test]
fn zai_rename_normalizes_every_spelling_to_zai() {
    assert_eq!(normalize_provider_name("zhipu"), "zai");
    assert_eq!(normalize_provider_name("z.ai"), "zai");
    assert_eq!(normalize_provider_name("z.ai GLM"), "zai");
    assert_eq!(normalize_provider_name("z.ai glm"), "zai");
    assert_eq!(normalize_provider_name("zai"), "zai");
}

#[test]
fn zai_rename_serde_alias_loads_legacy_zhipu_section() {
    // A pre-rename config.toml `[providers.zhipu]` table must land in the
    // renamed `zai` field untouched.
    let cfg: crate::config::types::ProviderConfigs =
        toml::from_str("[zhipu]\nenabled = true\napi_key = \"k\"\n")
            .expect("legacy [providers.zhipu] table must load");
    assert!(cfg.zai.as_ref().is_some_and(|p| p.enabled));
    let back = toml::to_string(&cfg).expect("serialize renamed config");
    assert!(
        back.contains("[zai]"),
        "legacy [zhipu] table must round-trip as canonical [zai], got: {back}"
    );
}

#[test]
fn known_provider_claude_cli_section() {
    let meta = find_provider_meta("claude_cli").expect("claude_cli must exist");
    assert_eq!(meta.config_section, "providers.claude_cli");
    assert!(!meta.needs_api_key);
}

#[test]
fn known_provider_opencode_cli_section() {
    let meta = find_provider_meta("opencode_cli").expect("opencode_cli must exist");
    assert_eq!(meta.config_section, "providers.opencode_cli");
    assert!(!meta.needs_api_key);
}

#[test]
fn known_provider_codex_cli_section() {
    let meta = find_provider_meta("codex_cli").expect("codex_cli must exist");
    assert_eq!(meta.config_section, "providers.codex_cli");
    assert!(!meta.needs_api_key);
}

#[test]
fn known_provider_codex_oauth_section() {
    let meta = find_provider_meta("codex").expect("codex must exist");
    assert_eq!(meta.config_section, "providers.codex");
    assert!(!meta.needs_api_key);
}

#[test]
fn known_provider_opencode_section() {
    let meta = find_provider_meta("opencode").expect("opencode must exist");
    assert_eq!(meta.config_section, "providers.opencode");
    assert!(meta.needs_api_key);
}

#[test]
fn known_provider_qwen_section() {
    let meta = find_provider_meta("qwen").expect("qwen must exist");
    assert_eq!(meta.config_section, "providers.qwen");
}

#[test]
fn known_provider_ollama_section() {
    let meta = find_provider_meta("ollama").expect("ollama must exist");
    assert_eq!(meta.config_section, "providers.ollama");
}

// ── config_section() function ─────────────────��─────────────────────

#[test]
fn config_section_builtin_returns_correct() {
    assert_eq!(
        config_section("anthropic"),
        Some("providers.anthropic".to_string())
    );
    assert_eq!(
        config_section("openai"),
        Some("providers.openai".to_string())
    );
    assert_eq!(
        config_section("ollama"),
        Some("providers.ollama".to_string())
    );
    assert_eq!(
        config_section("opencode"),
        Some("providers.opencode".to_string())
    );
}

#[test]
fn config_section_custom_returns_correct() {
    assert_eq!(
        config_section("custom:dialagram"),
        Some("providers.custom.dialagram".to_string())
    );
    assert_eq!(
        config_section("custom:opencode-qwen"),
        Some("providers.custom.opencode-qwen".to_string())
    );
    assert_eq!(
        config_section("custom(opencode-kimi)"),
        Some("providers.custom.opencode-kimi".to_string())
    );
}

#[test]
fn config_section_unknown_returns_none() {
    assert_eq!(config_section("nonexistent"), None);
}

// ── normalize_provider_name ─────────────────────────────────────────

#[test]
fn normalize_builtin_returns_canonical_id() {
    assert_eq!(normalize_provider_name("Anthropic"), "anthropic");
    assert_eq!(normalize_provider_name("claude_cli"), "claude-cli");
    assert_eq!(normalize_provider_name("opencode_cli"), "opencode-cli");
    assert_eq!(normalize_provider_name("codex_cli"), "codex-cli");
    assert_eq!(normalize_provider_name("codex_oauth"), "codex");
    assert_eq!(normalize_provider_name("Codex CLI"), "codex-cli");
    assert_eq!(normalize_provider_name("Codex"), "codex");
    assert_eq!(normalize_provider_name("Ollama"), "ollama");
}

#[test]
fn normalize_custom_preserves_prefix() {
    assert_eq!(
        normalize_provider_name("custom:dialagram"),
        "custom:dialagram"
    );
    assert_eq!(
        normalize_provider_name("Custom(dialagram)"),
        "custom:dialagram"
    );
}

// ── KNOWN_PROVIDERS completeness ────────────────────────────────────

#[test]
fn all_known_providers_have_unique_ids() {
    let ids: Vec<&str> = KNOWN_PROVIDERS.iter().map(|p| p.id).collect();
    let mut seen = std::collections::HashSet::new();
    for id in &ids {
        assert!(seen.insert(*id), "Duplicate provider id: {}", id);
    }
}

#[test]
fn all_known_providers_have_unique_sections() {
    let sections: Vec<&str> = KNOWN_PROVIDERS.iter().map(|p| p.config_section).collect();
    let mut seen = std::collections::HashSet::new();
    for s in &sections {
        assert!(seen.insert(*s), "Duplicate config section: {}", s);
    }
}

#[test]
fn all_known_providers_have_non_empty_display_names() {
    for p in KNOWN_PROVIDERS {
        assert!(
            !p.display_name.is_empty(),
            "Provider {} has empty display_name",
            p.id
        );
    }
}

#[test]
fn known_provider_count_matches_expected() {
    // If this fails, a provider was added/removed.
    // Update this count AND verify all section mappings above.
    assert_eq!(KNOWN_PROVIDERS.len(), 17);
}

// ── TUI PROVIDERS sync with KNOWN_PROVIDERS ─────────────────────────

#[test]
fn tui_providers_all_have_matching_known_provider() {
    use crate::tui::onboarding::PROVIDERS;

    for p in PROVIDERS {
        // Custom provider has empty id — skip it
        if p.id.is_empty() {
            continue;
        }
        let meta = find_provider_meta(p.id);
        assert!(
            meta.is_some(),
            "TUI provider '{}' (id='{}') has no matching KNOWN_PROVIDER entry",
            p.name,
            p.id
        );
        let meta = meta.unwrap();
        assert_eq!(
            meta.config_section,
            format!("providers.{}", p.id.replace('-', "_")),
            "TUI provider '{}' section mismatch",
            p.id
        );
    }
}

#[test]
fn tui_custom_provider_is_last() {
    use crate::tui::onboarding::PROVIDERS;

    let last = PROVIDERS.last().expect("PROVIDERS must not be empty");
    assert!(
        last.id.is_empty(),
        "Last PROVIDERS entry must be Custom (empty id), got '{}'",
        last.id
    );
    assert!(
        last.name.contains("Custom"),
        "Last PROVIDERS entry must be named Custom, got '{}'",
        last.name
    );
}

// ── is_first_time provider coverage ─────────────────────────────────

#[test]
fn is_first_time_uses_provider_registry_not_hardcoded_list() {
    // is_first_time() must derive "is there a configured provider?" from
    // active_provider_and_model() (which walks provider_registry — the single
    // source of truth), NOT a hardcoded OR-chain of provider fields.
    //
    // History: the hardcoded chain silently omitted new providers — ollama and
    // opencode (2026-04-28), then Xiaomi (2026-06) — so an enabled but
    // un-listed provider re-triggered onboarding on every restart. Routing
    // through provider_registry makes that class of bug impossible: adding a
    // provider to the registry automatically covers is_first_time().
    let source = include_str!("../tui/onboarding/fetch.rs");
    assert!(
        source.contains("active_provider_and_model"),
        "is_first_time() must use active_provider_and_model (provider_registry), \
         not a hardcoded provider list that drifts every time a provider is added"
    );
}

// ── apply_config section routing (registry-driven) ──────────────────
//
// The save path that /models reuses lives in `OnboardingState::apply_config`.
// The old ModelSelector resolved sections via a hand-maintained `match
// provider.id` with one arm per provider — a list that could silently drift
// out of sync with the providers themselves. The refactor replaced that with
// the `utils::providers` registry: section resolution goes through
// `find_provider_meta(id).config_section` and the disable-all sweep iterates
// `all_config_sections` / `KNOWN_PROVIDERS`. These tests pin the stronger,
// drift-proof guarantee: the registry routes every provider, and the save
// path stays registry-driven (never reverts to a hardcoded match).

const SAVE_SRC: &str = include_str!("../tui/onboarding/config.rs");

#[test]
fn save_provider_section_routing_covers_all_providers() {
    // Every provider id resolves to a TOML section through the registry —
    // this is what prevents the index-based corruption bug (writing a custom
    // provider's settings into [providers.anthropic]).
    let required_ids = [
        "anthropic",
        "openai",
        "github",
        "gemini",
        "openrouter",
        "minimax",
        "zai",
        "claude-cli",
        "opencode-cli",
        "codex-cli",
        "codex",
        "opencode",
        "qwen",
        "ollama",
    ];

    for id in &required_ids {
        let meta = find_provider_meta(id).unwrap_or_else(|| {
            panic!("provider '{id}' is missing from the KNOWN_PROVIDERS registry")
        });
        assert!(
            meta.config_section.starts_with("providers."),
            "provider '{id}' resolves to a malformed section '{}'",
            meta.config_section
        );
    }

    // Pin that the save path resolves sections via the registry, not a
    // hardcoded `match provider.id` (the drift-prone shape that was removed).
    assert!(
        SAVE_SRC.contains("find_provider_meta(id)"),
        "apply_config must resolve the provider section via \
         find_provider_meta(id).config_section, not a hardcoded match"
    );
}

#[test]
fn save_provider_disables_all_known_sections() {
    // Every built-in section the disable-all sweep must cover is present in the
    // registry, so iterating KNOWN_PROVIDERS / all_config_sections disables
    // them all without a hand-maintained list.
    let required_sections = [
        "providers.anthropic",
        "providers.openai",
        "providers.github",
        "providers.gemini",
        "providers.openrouter",
        "providers.minimax",
        "providers.zai",
        "providers.claude_cli",
        "providers.opencode_cli",
        "providers.codex_cli",
        "providers.codex",
        "providers.opencode",
        "providers.qwen",
        "providers.ollama",
    ];

    for section in &required_sections {
        assert!(
            KNOWN_PROVIDERS.iter().any(|p| p.config_section == *section),
            "section '{section}' is missing from the KNOWN_PROVIDERS registry, so the \
             registry-driven disable-all sweep would skip it"
        );
    }

    // Pin that the disable-all sweep stays registry-driven.
    assert!(
        SAVE_SRC.contains("all_config_sections"),
        "apply_config's disable-all loop must iterate all_config_sections \
         (registry-driven), not a hardcoded section list"
    );
}

#[test]
fn config_for_resolves_moonshot() {
    // #611: config_for had no moonshot arm, so it fell through to the `custom:`
    // branch and returned None — the /models dialog's detect_existing_key then
    // always showed an empty key even when the key was present in config.
    use crate::config::{ProviderConfig, ProviderConfigs};
    use crate::utils::providers::config_for;
    let providers = ProviderConfigs {
        moonshot: Some(ProviderConfig {
            api_key: Some("sk-kimi".to_string()),
            ..Default::default()
        }),
        ..Default::default()
    };
    // Direct id plus the aliases find_provider_meta accepts.
    for name in ["moonshot", "kimi", "moonshotai"] {
        let cfg = config_for(&providers, name);
        assert!(
            cfg.is_some(),
            "config_for('{name}') must resolve the moonshot slot"
        );
        assert_eq!(cfg.unwrap().api_key.as_deref(), Some("sk-kimi"));
    }
}