Synced from monorepo
Changes: - Non-blocking coding-data sharing upsell banner - Consolidate remediation in Doctor - Auto mode defers fail-closed gate asks to the classifier - Coalesce marketplace list fetches - Allow removing a marketplace source by name - Contain hung git marketplace sources (timeouts, non-blocking refresh, unbrick modal) - Label failed workspace RPCs with error_kind - Drop redundant explicit tonic/prost deps from xai-grok-shell - Report real exit codes for completed background shells - Narrow the date-rollover reminder to date-bearing templates - Wire toolOverrides through the session and agent - Security: Bash(git:*) allowlist matches whole command chain by prefix - Split prompt-trigger telemetry and record classifier provenance - Raise connectors-manager timeout to 60s - Auto classifier honors recorded approvals for repeat actions - Apply doctor fixes in the TUI - Auto-mode classifier timeouts prompt instead of silently denying - Scope subagent completion drains to the owning session - Add the toolOverrides wire types - Set client_identifier=grok-agent-sdk - Accept both spellings of the workspace-teleport kill switch - Persist one-shot occurrence journal - Stop turns that poll the exact same tool call 16x in a row - Copy compaction checkpoint files when forking sessions - Auto-focus permission prompt from scrollback - Esc cancels the running turn in non-vim and minimal modes - List Ctrl+Z undo and redo in keyboard shortcuts - Out-of-process macOS mic capture - Show active auth mode on session-info - Install the npm binary under $GROK_HOME - Remove hover/click dead zones between dashboard items - Route startup warnings to doctor - Document [feedback.user] author identity config - Extend bang command timeout - Close combine-queued edit-hold race - Integrate relocation recovery - Expose privacy notice rollout flag - Break harness discovery ref cycle so connections can idle-evict - Shift/Alt+Enter inserts newline when editing a queued prompt - Gate project Claude permissions on folder trust - Echo response.create.event_id on response.created - Toast when session creation fails from disk full - Add shared test process lifecycle - Enable dynamic workflows by default - Add relocation transaction state machine - Add shared test sandbox - Surface auth failures on model-switch compact - Persist durable scheduler expiry - Confirm before removing extensions-modal items - Re-run compact and prompt after login when compact hit expired auth - Recap sends hosted tools under backend search
This commit is contained in:
parent
3af4d5d398
commit
a5727c5960
482 changed files with 37627 additions and 13402 deletions
|
|
@ -5,17 +5,17 @@ use crate::diagnostics::{
|
|||
};
|
||||
use crate::host::{DisplayServer, HostOs};
|
||||
|
||||
const LIVE_TUI_PROBE_CTA: &str = "Run /doctor inside Grok.";
|
||||
const LIVE_TUI_PROBE_CTA: &str = "Some checks only run in Grok. Start Grok and run /doctor.";
|
||||
|
||||
pub(super) fn format(report: &DiagnosticReport) -> String {
|
||||
let facts = &report.facts;
|
||||
let mut out = String::from("Grok Doctor\n\nTerminal\n");
|
||||
let mut out = String::from("Grok Doctor\n\nEnvironment\n");
|
||||
|
||||
fact(&mut out, "terminal", &facts.terminal.to_string());
|
||||
match &facts.xtversion {
|
||||
RuntimeFact::Available(value) => fact(&mut out, "xtversion", value),
|
||||
RuntimeFact::NoReply => unavailable(&mut out, "xtversion", "no reply"),
|
||||
RuntimeFact::Unavailable => unavailable(&mut out, "xtversion", "unavailable"),
|
||||
RuntimeFact::Available(value) => fact(&mut out, "terminal version", value),
|
||||
RuntimeFact::NoReply => unavailable(&mut out, "terminal version", "no reply"),
|
||||
RuntimeFact::Unavailable => unavailable(&mut out, "terminal version", "unavailable"),
|
||||
}
|
||||
fact(&mut out, "multiplexer", &facts.multiplexer.to_string());
|
||||
if let Some(byobu) = facts.byobu {
|
||||
|
|
@ -95,7 +95,7 @@ pub(super) fn format(report: &DiagnosticReport) -> String {
|
|||
);
|
||||
fact(
|
||||
&mut out,
|
||||
"wrap",
|
||||
"SSH wrap",
|
||||
if clipboard.wrap_sink { "on" } else { "off" },
|
||||
);
|
||||
if clipboard.display_server == DisplayServer::Wayland {
|
||||
|
|
@ -125,9 +125,6 @@ pub(super) fn format(report: &DiagnosticReport) -> String {
|
|||
ClipboardDelivery::Failed => "unavailable",
|
||||
};
|
||||
fact(&mut out, "status", status);
|
||||
if let Some(fix) = &clipboard.fix {
|
||||
fact(&mut out, "fix", fix);
|
||||
}
|
||||
|
||||
if let Some(voice) = &facts.voice {
|
||||
out.push_str("\nVoice\n");
|
||||
|
|
@ -154,7 +151,7 @@ pub(super) fn format(report: &DiagnosticReport) -> String {
|
|||
.filter(|note| !fact_already_shows_probe(note.probe));
|
||||
let mut notes = visible_notes.peekable();
|
||||
if notes.peek().is_some() {
|
||||
out.push_str("\nProbe notes\n");
|
||||
out.push_str("\nChecks not completed\n");
|
||||
for note in notes {
|
||||
let message = match ¬e.message {
|
||||
Some(message) => format!("{}: {message}", probe_status(note.status)),
|
||||
|
|
@ -169,7 +166,7 @@ pub(super) fn format(report: &DiagnosticReport) -> String {
|
|||
.iter()
|
||||
.any(crate::diagnostics::probe_requires_live_tui)
|
||||
{
|
||||
out.push_str("\nLive TUI evidence\n");
|
||||
out.push_str("\nNeeds a running session\n");
|
||||
out.push_str(&format!(" {LIVE_TUI_PROBE_CTA}\n"));
|
||||
}
|
||||
|
||||
|
|
@ -220,7 +217,7 @@ fn format_finding(out: &mut String, finding: &DiagnosticFinding) {
|
|||
let instruction = match (&remediation.config_path, &finding.automatic_remediation) {
|
||||
(Some(path), _) => format!("Add `{}` to {path}", remediation.fix),
|
||||
(None, Some(_)) => format!("One-off: `{}`", remediation.fix),
|
||||
(None, None) => format!("Run `{}`", remediation.fix),
|
||||
(None, None) => format!("Run: `{}`", remediation.fix),
|
||||
};
|
||||
out.push_str(&format!(" → {instruction}\n"));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -13,7 +13,7 @@ pub const SCHEMA_VERSION: &str = "1";
|
|||
#[derive(Clone, Debug, Default, Eq, PartialEq, clap::Args)]
|
||||
#[command(args_conflicts_with_subcommands = true)]
|
||||
pub struct DoctorArgs {
|
||||
/// Emit machine-readable JSON output.
|
||||
/// Print the diagnostic report as JSON.
|
||||
#[arg(long)]
|
||||
pub json: bool,
|
||||
#[command(subcommand)]
|
||||
|
|
@ -22,16 +22,16 @@ pub struct DoctorArgs {
|
|||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, clap::Subcommand)]
|
||||
pub enum DoctorCommand {
|
||||
/// Apply a named automatic remediation.
|
||||
/// Apply an automatic fix.
|
||||
Fix(FixArgs),
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, clap::Args)]
|
||||
pub struct FixArgs {
|
||||
/// Short fix handle (`ssh-wrap`); canonical `terminal.ssh-wrap` is also accepted.
|
||||
pub id: String,
|
||||
/// Apply without prompting after printing the exact plan.
|
||||
#[arg(long)]
|
||||
/// Fix to apply. Use `ssh-wrap` or `terminal.ssh-wrap`. Omit it to list available automatic fixes.
|
||||
pub id: Option<String>,
|
||||
/// Apply the displayed changes without confirmation.
|
||||
#[arg(long, requires = "id")]
|
||||
pub yes: bool,
|
||||
}
|
||||
|
||||
|
|
@ -50,7 +50,7 @@ pub fn run(args: DoctorArgs) -> Result<()> {
|
|||
pub fn run_with_writer(args: DoctorArgs, writer: &mut impl Write) -> Result<()> {
|
||||
match args.command {
|
||||
None => run_report(args.json, writer),
|
||||
Some(_) => anyhow::bail!("doctor fixes require interactive input/output"),
|
||||
Some(_) => anyhow::bail!("Doctor fixes require interactive input and output."),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -69,25 +69,20 @@ fn configured_report_for_terminal(
|
|||
report: DiagnosticReport,
|
||||
terminal: &crate::terminal::TerminalContext,
|
||||
) -> DiagnosticReport {
|
||||
let configured = shell_home_and_kind()
|
||||
.map(|(home, shell)| {
|
||||
crate::diagnostics::managed_alias_configured(&shell.config_path(&home), shell)
|
||||
})
|
||||
.unwrap_or(false);
|
||||
if terminal.is_ssh || terminal.is_official_vscode_remote {
|
||||
report
|
||||
} else {
|
||||
crate::diagnostics::configured_report(report, configured)
|
||||
return report;
|
||||
}
|
||||
let configured = shell_home_and_kind().is_some_and(|(home, shell)| {
|
||||
crate::diagnostics::managed_alias_configured(&shell.config_path(&home), shell)
|
||||
});
|
||||
crate::diagnostics::configured_report(report, configured)
|
||||
}
|
||||
|
||||
fn collect_report_with(
|
||||
snapshot: crate::diagnostics::probes::StandaloneDiagnosticSnapshot<'_>,
|
||||
) -> DiagnosticReport {
|
||||
let mut report = crate::diagnostics::view(snapshot.into());
|
||||
// Passive mic fact when audio is compiled in. No issue finding — headless
|
||||
// hosts often have no input device; the Voice fact row is enough.
|
||||
crate::diagnostics::apply_voice_probe(&mut report, false);
|
||||
crate::diagnostics::apply_voice_probe(&mut report, true);
|
||||
report
|
||||
}
|
||||
|
||||
|
|
@ -110,12 +105,20 @@ fn run_fix(
|
|||
input: &mut impl std::io::BufRead,
|
||||
writer: &mut impl Write,
|
||||
) -> Result<()> {
|
||||
let id = crate::diagnostics::resolve_fix_id(&args.id)?;
|
||||
let terminal = crate::terminal::standalone_terminal_context();
|
||||
let report = configured_report_for_terminal(
|
||||
collect_report_with(crate::diagnostics::probes::collect_standalone(&terminal)),
|
||||
&terminal,
|
||||
);
|
||||
let Some(value) = args.id.as_deref() else {
|
||||
write!(
|
||||
writer,
|
||||
"{}",
|
||||
crate::diagnostics::format_applicable_automatic_fixes(&report, &terminal)
|
||||
)?;
|
||||
return Ok(());
|
||||
};
|
||||
let id = crate::diagnostics::resolve_fix_id(value)?;
|
||||
let request = crate::diagnostics::FixRequest::from_environment(id)?;
|
||||
let plan = crate::diagnostics::plan_fix(request, &report, &terminal)?;
|
||||
apply_fix_plan(args, stdin_is_terminal, input, writer, &terminal, plan)
|
||||
|
|
@ -129,21 +132,20 @@ fn apply_fix_plan(
|
|||
terminal: &crate::terminal::TerminalContext,
|
||||
plan: FixPlan,
|
||||
) -> Result<()> {
|
||||
let id = plan.id;
|
||||
write_fix_preview(&plan, writer)?;
|
||||
|
||||
if !args.yes {
|
||||
if !stdin_is_terminal {
|
||||
anyhow::bail!(
|
||||
"refusing to apply a doctor fix from non-interactive stdin without --yes"
|
||||
"Cannot apply this fix without confirmation. Run it in an interactive terminal or add `--yes`."
|
||||
);
|
||||
}
|
||||
write!(writer, "\nApply this change? [y/N] ")?;
|
||||
write!(writer, "\nApply this fix? [y/N] ")?;
|
||||
writer.flush()?;
|
||||
let mut answer = String::new();
|
||||
input.read_line(&mut answer)?;
|
||||
if !matches!(answer.trim().to_ascii_lowercase().as_str(), "y" | "yes") {
|
||||
writeln!(writer, "Cancelled.")?;
|
||||
writeln!(writer, "Fix cancelled.")?;
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
|
@ -154,63 +156,38 @@ fn apply_fix_plan(
|
|||
collect_report_with(crate::diagnostics::probes::collect_standalone(terminal)),
|
||||
crate::diagnostics::managed_alias_configured(&outcome.changed_path, shell),
|
||||
);
|
||||
if post_report.findings.iter().any(|finding| finding.id == id) {
|
||||
anyhow::bail!("fix applied, but `{id}` is still reported");
|
||||
if post_report
|
||||
.findings
|
||||
.iter()
|
||||
.any(|finding| finding.id == outcome.id)
|
||||
{
|
||||
anyhow::bail!(
|
||||
"The change was applied, but Doctor still reports `{}`.",
|
||||
outcome.id
|
||||
);
|
||||
}
|
||||
|
||||
match outcome.status {
|
||||
FixStatus::Applied => writeln!(
|
||||
writer,
|
||||
"\nConfigured {id} in {}.",
|
||||
"\nSet up SSH wrapping in {}.",
|
||||
outcome.changed_path.display()
|
||||
)?,
|
||||
FixStatus::AlreadyConfigured => writeln!(
|
||||
writer,
|
||||
"\n{id} is already configured in {}.",
|
||||
"\nSSH wrapping is already set up in {}.",
|
||||
outcome.changed_path.display()
|
||||
)?,
|
||||
}
|
||||
if let Some(backup) = outcome.backup_path {
|
||||
writeln!(writer, "Backup: {}", backup.display())?;
|
||||
}
|
||||
writeln!(writer, "Open a new interactive shell to use the alias.")?;
|
||||
writeln!(writer, "Start a new shell to use the alias.")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_fix_preview(plan: &FixPlan, writer: &mut impl Write) -> std::io::Result<()> {
|
||||
writeln!(writer, "Doctor fix: {}", plan.id)?;
|
||||
writeln!(writer, "Shell: {}", plan.shell.name())?;
|
||||
for change in &plan.changes {
|
||||
writeln!(writer, "File: {}", change.requested_path.display())?;
|
||||
if change.target_path != change.requested_path {
|
||||
writeln!(writer, "Physical target: {}", change.target_path.display())?;
|
||||
}
|
||||
writeln!(writer, "\nManaged block:")?;
|
||||
writeln!(writer, "{}", change.block)?;
|
||||
match &change.backup_path_hint {
|
||||
Some(path) => writeln!(
|
||||
writer,
|
||||
"\nProposed backup: {} (apply retries a nearby unique name on collision)",
|
||||
path.display()
|
||||
)?,
|
||||
None => writeln!(writer, "\nBackup: none (new file or exact no-op)")?,
|
||||
}
|
||||
}
|
||||
writeln!(writer, "\nBehavior:")?;
|
||||
writeln!(
|
||||
writer,
|
||||
" New interactive shells run typed `ssh ...` as `grok wrap ssh ...`."
|
||||
)?;
|
||||
writeln!(
|
||||
writer,
|
||||
" One-off alternative without changing config: `{}`.",
|
||||
crate::diagnostics::SSH_WRAP_ONE_OFF
|
||||
)?;
|
||||
writeln!(writer, "Caveats:")?;
|
||||
for caveat in &plan.caveats {
|
||||
writeln!(writer, " - {caveat}")?;
|
||||
}
|
||||
Ok(())
|
||||
write!(writer, "{}", crate::diagnostics::format_fix_preview(plan))
|
||||
}
|
||||
|
||||
fn shell_home_and_kind() -> Option<(std::path::PathBuf, ShellKind)> {
|
||||
|
|
|
|||
|
|
@ -309,6 +309,19 @@ fn human_wayland_error_includes_detail_once() {
|
|||
report.facts.clipboard.data_control = DataControlFact::Error;
|
||||
report.facts.clipboard.delivery = ClipboardDelivery::Failed;
|
||||
report.facts.clipboard.fix = Some("/minimal".to_owned());
|
||||
report.findings.push(DiagnosticFinding {
|
||||
id: crate::diagnostics::CLIPBOARD_DELIVERY_UNAVAILABLE_ID,
|
||||
disposition: FindingDisposition::Issue,
|
||||
message: "No configured clipboard route can reach the intended clipboard".to_owned(),
|
||||
remediation: None,
|
||||
automatic_remediation: None,
|
||||
note: Some(
|
||||
"Each in-app copy is also written to the backup path shown by the operation. Use \
|
||||
`/copy <file>` for an explicit file or `/minimal` for terminal-native selection, \
|
||||
then check the native clipboard tool reported above."
|
||||
.to_owned(),
|
||||
),
|
||||
});
|
||||
report.probe_notes = vec![ProbeNote {
|
||||
probe: "wayland.data-control",
|
||||
status: ProbeStatus::Error,
|
||||
|
|
@ -319,9 +332,9 @@ fn human_wayland_error_includes_detail_once() {
|
|||
concat!(
|
||||
"Grok Doctor\n",
|
||||
"\n",
|
||||
"Terminal\n",
|
||||
"Environment\n",
|
||||
" · terminal Ghostty\n",
|
||||
" ? xtversion no reply\n",
|
||||
" ? terminal version no reply\n",
|
||||
" · multiplexer None detected\n",
|
||||
" · ssh no\n",
|
||||
" · color truecolor\n",
|
||||
|
|
@ -331,10 +344,13 @@ fn human_wayland_error_includes_detail_once() {
|
|||
" · native unavailable\n",
|
||||
" · tmux off\n",
|
||||
" · osc 52 off\n",
|
||||
" · wrap off\n",
|
||||
" · SSH wrap off\n",
|
||||
" ? data-control error: probe worker died\n",
|
||||
" · status unavailable\n",
|
||||
" · fix /minimal\n",
|
||||
"\n",
|
||||
"Findings\n",
|
||||
" ! clipboard.delivery-unavailable No configured clipboard route can reach the intended clipboard\n",
|
||||
" Each in-app copy is also written to the backup path shown by the operation. Use `/copy <file>` for an explicit file or `/minimal` for terminal-native selection, then check the native clipboard tool reported above.\n",
|
||||
"\n",
|
||||
"1 issue, 0 recommendations\n",
|
||||
)
|
||||
|
|
@ -424,9 +440,9 @@ fn human_healthy_fixture_is_exact() {
|
|||
concat!(
|
||||
"Grok Doctor\n",
|
||||
"\n",
|
||||
"Terminal\n",
|
||||
"Environment\n",
|
||||
" · terminal Ghostty\n",
|
||||
" ? xtversion no reply\n",
|
||||
" ? terminal version no reply\n",
|
||||
" · multiplexer None detected\n",
|
||||
" · ssh no\n",
|
||||
" · color truecolor\n",
|
||||
|
|
@ -436,7 +452,7 @@ fn human_healthy_fixture_is_exact() {
|
|||
" · native local (pbcopy)\n",
|
||||
" · tmux off\n",
|
||||
" · osc 52 off\n",
|
||||
" · wrap off\n",
|
||||
" · SSH wrap off\n",
|
||||
" · status confirmed\n",
|
||||
"\n",
|
||||
"0 issues, 0 recommendations\n",
|
||||
|
|
@ -451,9 +467,9 @@ fn human_mixed_fixture_is_exact() {
|
|||
concat!(
|
||||
"Grok Doctor\n",
|
||||
"\n",
|
||||
"Terminal\n",
|
||||
"Environment\n",
|
||||
" · terminal Ghostty\n",
|
||||
" · xtversion Ghostty 1.2.3\n",
|
||||
" · terminal version Ghostty 1.2.3\n",
|
||||
" · multiplexer tmux\n",
|
||||
" · byobu tmux\n",
|
||||
" · ssh yes\n",
|
||||
|
|
@ -466,7 +482,7 @@ fn human_mixed_fixture_is_exact() {
|
|||
" · native local (pbcopy)\n",
|
||||
" · tmux on\n",
|
||||
" · osc 52 supported\n",
|
||||
" · wrap off\n",
|
||||
" · SSH wrap off\n",
|
||||
" · status confirmed\n",
|
||||
"\n",
|
||||
"Findings\n",
|
||||
|
|
@ -477,15 +493,15 @@ fn human_mixed_fixture_is_exact() {
|
|||
" → Automatic setup: `grok doctor fix ssh-wrap`\n",
|
||||
" → One-off: `grok wrap ssh <host>`\n",
|
||||
"\n",
|
||||
"Probe notes\n",
|
||||
"Checks not completed\n",
|
||||
" ? tmux.version unavailable\n",
|
||||
" ? tmux.extended-keys unavailable\n",
|
||||
" ? tmux.allow-passthrough-support unsupported\n",
|
||||
" ? runtime.fullscreen-active unavailable\n",
|
||||
" ? tmux.control-mode error: server unavailable\n",
|
||||
"\n",
|
||||
"Live TUI evidence\n",
|
||||
" Run /doctor inside Grok.\n",
|
||||
"Needs a running session\n",
|
||||
" Some checks only run in Grok. Start Grok and run /doctor.\n",
|
||||
"\n",
|
||||
"1 issue, 1 recommendation\n",
|
||||
)
|
||||
|
|
@ -505,15 +521,14 @@ fn fix_preview_contains_exact_change_and_caveats() {
|
|||
let mut preview = Vec::new();
|
||||
write_fix_preview(&plan, &mut preview).unwrap();
|
||||
let preview = String::from_utf8(preview).unwrap();
|
||||
assert_eq!(preview, crate::diagnostics::format_fix_preview(&plan));
|
||||
assert!(preview.contains("File: "));
|
||||
assert!(
|
||||
preview.contains(
|
||||
"# >>> grok doctor >>>\n# >>> terminal.ssh-wrap >>>\nalias ssh='grok wrap ssh'"
|
||||
)
|
||||
);
|
||||
assert!(
|
||||
preview.contains("One-off alternative without changing config: `grok wrap ssh <host>`")
|
||||
);
|
||||
assert!(preview.contains("To use once without changing config: `grok wrap ssh <host>`"));
|
||||
assert!(preview.contains("Use `command ssh ...` to bypass the alias."));
|
||||
assert!(preview.contains("ssh -f"));
|
||||
assert!(preview.contains("ControlPersist"));
|
||||
|
|
@ -534,7 +549,7 @@ fn decline_is_success_and_does_not_write() {
|
|||
let mut output = Vec::new();
|
||||
apply_fix_plan(
|
||||
FixArgs {
|
||||
id: "ssh-wrap".to_owned(),
|
||||
id: Some("ssh-wrap".to_owned()),
|
||||
yes: false,
|
||||
},
|
||||
true,
|
||||
|
|
@ -544,7 +559,11 @@ fn decline_is_success_and_does_not_write() {
|
|||
plan,
|
||||
)
|
||||
.unwrap();
|
||||
assert!(String::from_utf8(output).unwrap().ends_with("Cancelled.\n"));
|
||||
assert!(
|
||||
String::from_utf8(output)
|
||||
.unwrap()
|
||||
.ends_with("Fix cancelled.\n")
|
||||
);
|
||||
assert!(!temp.path().join(".bashrc").exists());
|
||||
}
|
||||
|
||||
|
|
@ -560,7 +579,7 @@ fn non_tty_without_yes_fails_safely_before_write() {
|
|||
.unwrap();
|
||||
let error = apply_fix_plan(
|
||||
FixArgs {
|
||||
id: "terminal.ssh-wrap".to_owned(),
|
||||
id: Some("terminal.ssh-wrap".to_owned()),
|
||||
yes: false,
|
||||
},
|
||||
false,
|
||||
|
|
@ -573,7 +592,7 @@ fn non_tty_without_yes_fails_safely_before_write() {
|
|||
assert!(
|
||||
error
|
||||
.to_string()
|
||||
.contains("non-interactive stdin without --yes")
|
||||
.contains("Cannot apply this fix without confirmation")
|
||||
);
|
||||
assert!(!temp.path().join(".bashrc").exists());
|
||||
}
|
||||
|
|
@ -607,9 +626,9 @@ fn human_incomplete_fixture_is_exact_without_duplicate_probe_rows() {
|
|||
concat!(
|
||||
"Grok Doctor\n",
|
||||
"\n",
|
||||
"Terminal\n",
|
||||
"Environment\n",
|
||||
" · terminal Ghostty\n",
|
||||
" ? xtversion unavailable\n",
|
||||
" ? terminal version unavailable\n",
|
||||
" · multiplexer None detected\n",
|
||||
" · ssh no\n",
|
||||
" ? color unavailable\n",
|
||||
|
|
@ -619,11 +638,11 @@ fn human_incomplete_fixture_is_exact_without_duplicate_probe_rows() {
|
|||
" · native local (pbcopy)\n",
|
||||
" · tmux off\n",
|
||||
" · osc 52 off\n",
|
||||
" · wrap off\n",
|
||||
" · SSH wrap off\n",
|
||||
" · status confirmed\n",
|
||||
"\n",
|
||||
"Live TUI evidence\n",
|
||||
" Run /doctor inside Grok.\n",
|
||||
"Needs a running session\n",
|
||||
" Some checks only run in Grok. Start Grok and run /doctor.\n",
|
||||
"\n",
|
||||
"0 issues, 0 recommendations\n",
|
||||
)
|
||||
|
|
@ -950,6 +969,49 @@ fn newline_variant_and_field_mappings_are_stable() {
|
|||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn clipboard_issue_count_preserves_legacy_reports_without_double_counting_named_findings() {
|
||||
let mut report = healthy_report();
|
||||
report.facts.clipboard.delivery = ClipboardDelivery::Failed;
|
||||
assert_eq!(report.issue_count(), 1, "legacy fact-only report");
|
||||
report.findings.push(DiagnosticFinding {
|
||||
id: crate::diagnostics::CLIPBOARD_DELIVERY_UNAVAILABLE_ID,
|
||||
disposition: FindingDisposition::Issue,
|
||||
message: "clipboard unavailable".to_owned(),
|
||||
remediation: None,
|
||||
automatic_remediation: None,
|
||||
note: Some("manual recovery".to_owned()),
|
||||
});
|
||||
assert_eq!(report.issue_count(), 1, "named finding replaces fact count");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_named_findings_extend_json_without_schema_changes() {
|
||||
let mut report = healthy_report();
|
||||
report.facts.clipboard.delivery = ClipboardDelivery::Unverified;
|
||||
report.facts.clipboard.fix = Some("grok wrap <ssh command> or /minimal".to_owned());
|
||||
report.findings.push(DiagnosticFinding {
|
||||
id: crate::diagnostics::CLIPBOARD_DELIVERY_UNVERIFIED_ID,
|
||||
disposition: FindingDisposition::Issue,
|
||||
message: "Clipboard delivery could not be verified across this remote boundary".to_owned(),
|
||||
remediation: None,
|
||||
automatic_remediation: None,
|
||||
note: Some("Run /doctor guidance".to_owned()),
|
||||
});
|
||||
|
||||
let mut output = Vec::new();
|
||||
write_report(&report, true, &mut output).unwrap();
|
||||
let json: serde_json::Value = serde_json::from_slice(&output).unwrap();
|
||||
assert_eq!(json["schemaVersion"], "1");
|
||||
assert_eq!(json["facts"]["clipboard"]["delivery"], "unverified");
|
||||
assert_eq!(
|
||||
json["facts"]["clipboard"]["fix"],
|
||||
"grok wrap <ssh command> or /minimal"
|
||||
);
|
||||
assert_eq!(json["findings"][0]["id"], "clipboard.delivery-unverified");
|
||||
assert_eq!(json["counts"]["issues"], 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn output_writer_errors_propagate() {
|
||||
struct BrokenWriter;
|
||||
|
|
|
|||
Loading…
Reference in a new issue