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
//! Pins the section-resolver invariant for `save_provider_settings` in
//! `src/tui/app/dialogs.rs`.
//!
//! Regression: 2026-06-04. User opens `/models`, cursor sits on the
//! currently-active custom provider (`dialagram`). They navigate to the
//! "+ Add new custom" row. `reload_model_selector_custom_fields` correctly
//! clears `self.ps.custom_name = ""`. They start typing the new entry's
//! `base_url` field. Every keystroke fires `save_provider_settings` with
//! `close_dialog=false` (per-field merge save).
//!
//! Pre-fix, the section resolver's `""` arm rescued empty `custom_name`
//! with `config.providers.active_custom()` — which still pointed at
//! `dialagram`. The keystroke-driven save then did
//! `Config::write_key("providers.custom.dialagram", "base_url", <new-url>)`,
//! silently corrupting dialagram's section. The model field survived
//! because `write_key` is a per-key TOML merge.
//!
//! Concrete corruption observed in user's config.toml:
//! ```toml
//! [providers.custom.dialagram]
//! base_url = "https://api-inference.modelscope.ai/v1" # overwritten
//! default_model = "qwen-3.7-max-thinking" # untouched
//! ```
//!
//! The fix: never fall back to `active_custom()` in the section resolver.
//! Empty `custom_name` means "user is mid-draft, no name typed yet" — the
//! only safe action is to skip the write.
const DIALOGS_SRC_RAW: &str = include_str!("../tui/app/dialogs.rs");
/// Strip `//` line comments so source-level invariant scans don't false-
/// match against the regression doc-comments that describe the bug they're
/// guarding against. Same approach as the
/// `approval_requests_are_not_routed_through_session_state_mut` sentinel
/// in `background_session_test.rs`.
fn dialogs_src_code() -> String {
DIALOGS_SRC_RAW
.lines()
.map(|line| {
if let Some(idx) = line.find("//") {
let before = &line[..idx];
let quote_count = before.matches('"').count();
if quote_count % 2 == 0 {
return before.trim_end().to_string();
}
}
line.to_string()
})
.collect::<Vec<_>>()
.join("\n")
}
#[test]
fn pre_fix_active_custom_fallback_pattern_is_absent() {
// Exact code shape of the pre-fix bug: an `else-if-let` arm pulling
// `active_custom()` and feeding it into the section name. This
// specific pattern is what corrupted dialagram on 2026-06-04. Banning
// the exact shape is brittle to formatting but a fair regression
// sentinel — if a future refactor needs `active_custom()` for an
// unrelated read it won't trip this, but a re-introduction of the
// fallback would land textually-identical or near-identical code.
let src = dialogs_src_code();
let forbidden_signatures = [
"else if let Some((name, _)) = config.providers.active_custom()",
"else if let Some((name, _)) = self.providers.active_custom()",
".active_custom() {\n name.to_string()",
];
for sig in forbidden_signatures {
assert!(
!src.contains(sig),
"save_provider_settings: forbidden fallback pattern re-introduced.\n\
Signature: {sig:?}\n\
This is the 2026-06-04 dialagram-corruption pattern. See \
custom_provider_section_resolver_test doc for the full repro and the \
reasoning behind banning the fallback."
);
}
}
#[test]
fn empty_custom_name_guard_precedes_section_format_in_resolver_arm() {
// The resolver's "" arm has exactly one assignment of the form
// `custom_section = format!("providers.custom.{}", self.ps.custom_name);`
// — the line that builds the per-key write target. The empty-name
// guard MUST appear in the same arm, before that assignment, and end
// with `return Ok(())`. Anchor on the assignment string and walk
// backwards within a tight window.
let src = dialogs_src_code();
let format_marker = "custom_section = format!(\"providers.custom.{}\", self.ps.custom_name)";
let format_idx = src.find(format_marker).unwrap_or_else(|| {
panic!(
"expected resolver's section-format line `{format_marker}` in dialogs.rs — \
either the format string moved or the resolver was restructured; update \
this test if intentional"
)
});
// Window of ~600 chars preceding the format assignment, bounded at
// the previous match-arm divider so we don't scan into unrelated code.
let window_start = format_idx.saturating_sub(600);
let preceding = &src[window_start..format_idx];
assert!(
preceding.contains("self.ps.custom_name.is_empty()"),
"empty-custom_name guard must precede the section-format assignment in the \
resolver \"\" arm. Without the guard, an empty custom_name falls through to \
a section format with `self.ps.custom_name = \"\"`, producing the literal \
section name `providers.custom.` (TOML write to an empty-named subkey) — \
arguably less harmful than the active_custom() fallback but still corruption."
);
assert!(
preceding.contains("return Ok(())"),
"empty-custom_name guard must early-return — without the return, the format \
assignment still runs against `self.ps.custom_name = \"\"`."
);
}