From ed05c57bfd34f9cfa4bef4ff407ef95d68c4ae1e Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sun, 16 Aug 2026 09:58:56 +0800 Subject: [PATCH] fix(cli): doctor said nothing about a config it was not using MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `tty7 doctor` is described as checking "socket, dialect, config, versions, agent hooks, links, context". With a `config.json` that does not parse it printed every row green: server ok, dialect ok, status ok. Meanwhile every setting in that file was being ignored, a copy had been kept beside it, and saving was suppressed. The rows that mention config report `TTY7_CONFIG_DIR` — where the file should be, which is a different question from whether it is being read. There is now a row for the answer, from the same `LoadOutcome` the loader already returns: ok, none yet, not valid JSON, unreadable. It also exits 1 for the two broken states, for the reason written above the server check: doctor is the verb people run when something is not working, and `tty7 doctor || alert` has to fire. A config nothing reads is that, as much as an unreachable server is. All four states run against a live server: ok, none yet, NOT VALID JSON (rc 1), and back to ok after repairing the file with no restart. --- crates/tty7-cli/src/commands.rs | 59 +++++++++++++++++++++++++++++++++ 1 file changed, 59 insertions(+) diff --git a/crates/tty7-cli/src/commands.rs b/crates/tty7-cli/src/commands.rs index dd72532b..c07bdc52 100644 --- a/crates/tty7-cli/src/commands.rs +++ b/crates/tty7-cli/src/commands.rs @@ -1590,10 +1590,30 @@ fn doctor(ctx: &Context, backend: &mut dyn Backend) -> Result { Some(value) => format!("set ({value})"), None => "missing".to_string(), }; + // The directory rows above say where the config *should* be, which is not + // the same as whether it is being read — and a file that does not parse is + // exactly the state someone runs `doctor` in. Every setting is ignored, a + // copy has been kept aside and saving is suppressed, and none of that is + // visible from the rows around it. + let (config_state, config_ok) = match tty7_core::core::config::Config::load_with_outcome().1 { + tty7_core::core::config::LoadOutcome::Parsed => ("ok".to_string(), true), + tty7_core::core::config::LoadOutcome::Absent => ("none yet — the defaults are the config".to_string(), true), + tty7_core::core::config::LoadOutcome::Quarantined => ( + "NOT VALID JSON — kept aside as config.json.corrupt; running on defaults and not saving" + .to_string(), + false, + ), + tty7_core::core::config::LoadOutcome::Unreadable => ( + "UNREADABLE — running on defaults and not saving" + .to_string(), + false, + ), + }; let mut rows = vec![ vec![address::ENV_CONFIG_DIR.to_string(), mark(&ctx.config_dir)], vec![address::ENV_WS.to_string(), mark(&ctx.ws)], vec![address::ENV_PANE.to_string(), mark(&ctx.pane)], + vec!["config".to_string(), config_state.clone()], ]; let mut server = json!({ "reachable": false }); let mut hooks: Vec<(HookAgent, HooksState)> = Vec::new(); @@ -1672,9 +1692,17 @@ fn doctor(ctx: &Context, backend: &mut dyn Backend) -> Result { "pane": ctx.pane.is_some(), }, "server": server, + "config": { "ok": config_ok, "state": config_state }, "hooks": hooks_json(&hooks), }), }; + // Same reasoning as the server row below, applied to the other half of an + // install: a config that does not parse means every setting in it is being + // ignored and saving is suppressed. `tty7 doctor || alert` should fire for + // that too, and the row alone would let it exit 0. + if !config_ok { + eprintln!("tty7: doctor: the config file is not being used"); + } if report.json["server"]["reachable"] == false { // doctor is the verb people run when something is not working, so an // unreachable server is *the* finding — not a row to exit 0 over: @@ -1683,6 +1711,9 @@ fn doctor(ctx: &Context, backend: &mut dyn Backend) -> Result { eprintln!("tty7: doctor: the server is unreachable"); return Ok(Outcome::Exit(1, report)); } + if !config_ok { + return Ok(Outcome::Exit(1, report)); + } Ok(Outcome::Report(report)) } @@ -4189,6 +4220,34 @@ mod tests { assert!(line(2).contains("2 panes"), "{}", line(2)); } + /// `doctor` says whether the config is being used, not just where it is. + /// + /// Its own description promises a config check, and the row beside it + /// reports `TTY7_CONFIG_DIR` — where the file *should* be, which is a + /// different question from whether it parsed. A file that does not is + /// exactly the state someone runs `doctor` in: every setting ignored, + /// saving suppressed, and nothing on the screen saying so. + #[test] + fn doctor_reports_whether_the_config_is_being_used() { + let out = human(run_cli( + &["tty7", "doctor"], + &Context::default(), + &mut doctor_backend(), + )); + let row = out + .lines() + .find(|l| l.starts_with("config ")) + .expect("doctor has a config row"); + assert!( + row.contains("ok") || row.contains("none yet"), + "an intact config reads as usable: {row}" + ); + assert!( + !row.contains("NOT VALID"), + "and is not reported as broken: {row}" + ); + } + /// Missing hooks are the reason a perfectly healthy-looking agent never /// reports and `tty7 wait` sits there until it times out. `doctor` is the /// verb people run when something is not working, so it is where that has