fix(cli): doctor said nothing about a config it was not using

`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.
This commit is contained in:
l0ng-ai
2026-08-16 09:58:56 +08:00
parent 1906dc4b64
commit ed05c57bfd
+59
View File
@@ -1590,10 +1590,30 @@ fn doctor(ctx: &Context, backend: &mut dyn Backend) -> Result<Outcome> {
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<Outcome> {
"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<Outcome> {
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