diff --git a/crates/ff-rdp-cli/src/cli/args.rs b/crates/ff-rdp-cli/src/cli/args.rs index 39b1d9d..f16a03a 100644 --- a/crates/ff-rdp-cli/src/cli/args.rs +++ b/crates/ff-rdp-cli/src/cli/args.rs @@ -176,6 +176,10 @@ COOKBOOK: OUTPUT FORMAT (iter-60 compact defaults): Default JSON: {\"results\": ..., \"total\": N} (meta omitted when empty) --verbose restores meta.connection (host, port, pid, uptime) to the envelope + meta.route (iter-128, all commands since iter-134) is always present on + every browser-touching command's envelope — \"daemon\" or \"direct\" — + regardless of --verbose, so you can tell how a command executed without a + separate `daemon status` call Truncated output adds: {\"truncated\": true, \"hint\": \"showing 20 of 84, use --all\"} --format json (default) machine-readable JSON — the stable API contract --format text human-readable tables and trees @@ -630,9 +634,9 @@ Field fidelity by source: `hint` is always present (iter-128) — null when there's nothing to report, a string when results are truncated, a timeout fired, or the capture was empty. -`meta.route` (iter-128) is always present — \"daemon\" or \"direct\" — regardless -of --verbose, so you can tell how this command executed without a separate -`daemon status` call. +`meta.route` (iter-128; all browser-touching commands since iter-134) is +always present — \"daemon\" or \"direct\" — regardless of --verbose, so you +can tell how this command executed without a separate `daemon status` call. Default: 20 results, sorted by duration (slowest first). Output (summary mode): {\"results\": {\"total_requests\": N, \"total_transfer_bytes\": N, \"by_cause_type\": {...}, \"slowest\": [...], \"timeout_reached\": false, \"hint\": null}, \"total\": N, \"meta\": {\"route\": \"daemon\", ...}} diff --git a/crates/ff-rdp-cli/src/commands/a11y.rs b/crates/ff-rdp-cli/src/commands/a11y.rs index 47d8af1..e5f8ef4 100644 --- a/crates/ff-rdp-cli/src/commands/a11y.rs +++ b/crates/ff-rdp-cli/src/commands/a11y.rs @@ -72,6 +72,10 @@ pub fn run( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); if used_js_fallback && let Some(m) = meta.as_object_mut() { m.insert("fallback".to_string(), json!(true)); m.insert("fallback_method".to_string(), json!("js-eval")); @@ -426,6 +430,10 @@ pub fn run_critical(cli: &Cli, root_selector: Option<&str>) -> Result<(), AppErr None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let controls = OutputControls::from_cli(cli, SortDir::Asc); let mut items = violations; diff --git a/crates/ff-rdp-cli/src/commands/a11y_contrast.rs b/crates/ff-rdp-cli/src/commands/a11y_contrast.rs index bd4fdcd..152e460 100644 --- a/crates/ff-rdp-cli/src/commands/a11y_contrast.rs +++ b/crates/ff-rdp-cli/src/commands/a11y_contrast.rs @@ -59,6 +59,10 @@ pub fn run(cli: &Cli, selector: Option<&str>, fail_only: bool) -> Result<(), App None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); // Apply output controls (sort, limit, fields). let controls = OutputControls::from_cli(cli, SortDir::Desc); diff --git a/crates/ff-rdp-cli/src/commands/a11y_summary.rs b/crates/ff-rdp-cli/src/commands/a11y_summary.rs index 1745c9d..3d13241 100644 --- a/crates/ff-rdp-cli/src/commands/a11y_summary.rs +++ b/crates/ff-rdp-cli/src/commands/a11y_summary.rs @@ -111,6 +111,10 @@ pub fn run(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&output_results, 1, &meta); // Custom text rendering for a11y summary. diff --git a/crates/ff-rdp-cli/src/commands/cascade.rs b/crates/ff-rdp-cli/src/commands/cascade.rs index fee0214..cb87c7e 100644 --- a/crates/ff-rdp-cli/src/commands/cascade.rs +++ b/crates/ff-rdp-cli/src/commands/cascade.rs @@ -582,6 +582,10 @@ pub fn run( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&Value::Array(results), total, &meta); let hint_ctx = HintContext::new(HintSource::Styles).with_selector(selector); diff --git a/crates/ff-rdp-cli/src/commands/click.rs b/crates/ff-rdp-cli/src/commands/click.rs index e8849f0..19b69b2 100644 --- a/crates/ff-rdp-cli/src/commands/click.rs +++ b/crates/ff-rdp-cli/src/commands/click.rs @@ -75,7 +75,7 @@ pub fn run_core( wait_for_network: Option<&str>, network_timeout: Option, opts: &ClickOptions<'_>, -) -> Result { +) -> Result<(Value, bool), AppError> { let mut ctx = connect_and_get_target(cli)?; // When --wait-for-network is requested in direct mode, subscribe to the @@ -232,7 +232,7 @@ pub fn run_core( result["match_count"] = json!(match_count); result["chosen_index"] = json!(chosen_index); } - Ok(result) + Ok((result, ctx.via_daemon)) } pub fn run( @@ -242,7 +242,8 @@ pub fn run( network_timeout: Option, opts: &ClickOptions<'_>, ) -> Result<(), AppError> { - let mut result = run_core(cli, selector, wait_for_network, network_timeout, opts)?; + let (mut result, via_daemon) = + run_core(cli, selector, wait_for_network, network_timeout, opts)?; // Preserve the pre-iter-61c CLI output shape: `settle_method` belongs in // `meta`, not in `results`. The script runner reads it from `results` @@ -267,6 +268,8 @@ pub fn run( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose. + crate::connection_meta::merge_route(&mut meta, via_daemon); let envelope = output::envelope(&result, 1, &meta); let hint_ctx = HintContext::new(HintSource::Click).with_selector(selector); diff --git a/crates/ff-rdp-cli/src/commands/computed.rs b/crates/ff-rdp-cli/src/commands/computed.rs index 9458b0b..d710253 100644 --- a/crates/ff-rdp-cli/src/commands/computed.rs +++ b/crates/ff-rdp-cli/src/commands/computed.rs @@ -180,6 +180,10 @@ pub fn run(cli: &Cli, selector: &str, props: &[String], include_all: bool) -> Re None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&results, total, &meta); let hint_ctx = HintContext::new(HintSource::Computed).with_selector(selector); diff --git a/crates/ff-rdp-cli/src/commands/consent.rs b/crates/ff-rdp-cli/src/commands/consent.rs index cb10da9..61edc79 100644 --- a/crates/ff-rdp-cli/src/commands/consent.rs +++ b/crates/ff-rdp-cli/src/commands/consent.rs @@ -38,6 +38,10 @@ pub fn run(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) diff --git a/crates/ff-rdp-cli/src/commands/console.rs b/crates/ff-rdp-cli/src/commands/console.rs index 3d0056b..f81435d 100644 --- a/crates/ff-rdp-cli/src/commands/console.rs +++ b/crates/ff-rdp-cli/src/commands/console.rs @@ -129,6 +129,10 @@ pub fn run(cli: &Cli, level: Option<&str>, pattern: Option<&str>) -> Result<(), None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let mut envelope = output::envelope_with_truncation(&json!(limited), shown, total, truncated, &meta); diff --git a/crates/ff-rdp-cli/src/commands/cookies.rs b/crates/ff-rdp-cli/src/commands/cookies.rs index 39795d4..ebfd0a2 100644 --- a/crates/ff-rdp-cli/src/commands/cookies.rs +++ b/crates/ff-rdp-cli/src/commands/cookies.rs @@ -104,6 +104,10 @@ pub fn run(cli: &Cli, name: Option<&str>, include_document_cookie: bool) -> Resu None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); if total == 0 && let Some(note) = detect_consent_banner(&mut ctx) && let Some(m) = meta.as_object_mut() diff --git a/crates/ff-rdp-cli/src/commands/dom.rs b/crates/ff-rdp-cli/src/commands/dom.rs index abaec8a..279bcbe 100644 --- a/crates/ff-rdp-cli/src/commands/dom.rs +++ b/crates/ff-rdp-cli/src/commands/dom.rs @@ -239,6 +239,10 @@ pub fn run( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); // Normalise to an array unconditionally (dogfood-49 #3): every `dom` // call now returns `results` as an array regardless of match count so @@ -422,6 +426,10 @@ pub fn run_count(cli: &Cli, selector: &str) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&results, usize::try_from(count).unwrap_or(0), &meta); let hint_ctx = HintContext::new(HintSource::Dom).with_selector(selector); @@ -589,6 +597,10 @@ pub fn run_stats(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&stats, 1, &meta); let hint_ctx = HintContext::new(HintSource::DomStats); diff --git a/crates/ff-rdp-cli/src/commands/dom_tree.rs b/crates/ff-rdp-cli/src/commands/dom_tree.rs index 3212625..3f49c33 100644 --- a/crates/ff-rdp-cli/src/commands/dom_tree.rs +++ b/crates/ff-rdp-cli/src/commands/dom_tree.rs @@ -65,6 +65,10 @@ pub fn run(cli: &Cli, selector: Option<&str>, depth: u32, max_chars: u32) -> Res None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); // Text short-circuit: render indented tree instead of JSON. if cli.format == "text" && cli.jq.is_none() { diff --git a/crates/ff-rdp-cli/src/commands/emulate.rs b/crates/ff-rdp-cli/src/commands/emulate.rs index 8b4b02a..5db21d4 100644 --- a/crates/ff-rdp-cli/src/commands/emulate.rs +++ b/crates/ff-rdp-cli/src/commands/emulate.rs @@ -196,6 +196,10 @@ pub fn run(cli: &Cli, args: &EmulateArgs) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, via_daemon); let envelope = output::envelope(&results, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) diff --git a/crates/ff-rdp-cli/src/commands/eval.rs b/crates/ff-rdp-cli/src/commands/eval.rs index a04fb37..2e3b697 100644 --- a/crates/ff-rdp-cli/src/commands/eval.rs +++ b/crates/ff-rdp-cli/src/commands/eval.rs @@ -639,6 +639,10 @@ pub fn run( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result_json, 1, &meta); let hint_ctx = HintContext::new(HintSource::Eval); diff --git a/crates/ff-rdp-cli/src/commands/geometry.rs b/crates/ff-rdp-cli/src/commands/geometry.rs index 060c58b..d5f14b5 100644 --- a/crates/ff-rdp-cli/src/commands/geometry.rs +++ b/crates/ff-rdp-cli/src/commands/geometry.rs @@ -112,6 +112,10 @@ pub fn run(cli: &Cli, selectors: &[String], include_hidden: bool) -> Result<(), None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&empty, 0, &meta); let first_sel = selectors.first().map_or("*", String::as_str); let hint_ctx = HintContext::new(HintSource::Geometry).with_selector(first_sel); @@ -173,6 +177,10 @@ pub fn run(cli: &Cli, selectors: &[String], include_hidden: bool) -> Result<(), None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); // Text short-circuit: render a human-readable table instead of JSON. if cli.format == "text" && cli.jq.is_none() { diff --git a/crates/ff-rdp-cli/src/commands/inspect.rs b/crates/ff-rdp-cli/src/commands/inspect.rs index b229706..80a9421 100644 --- a/crates/ff-rdp-cli/src/commands/inspect.rs +++ b/crates/ff-rdp-cli/src/commands/inspect.rs @@ -44,6 +44,10 @@ pub fn run(cli: &Cli, actor_id: &str, depth: u32) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result, 1, &meta); let hint_ctx = HintContext::new(HintSource::Inspect); OutputPipeline::from_cli(cli)?.finalize_with_hints(&envelope, Some(&hint_ctx)) diff --git a/crates/ff-rdp-cli/src/commands/manifest.rs b/crates/ff-rdp-cli/src/commands/manifest.rs index c2b5494..0926dd3 100644 --- a/crates/ff-rdp-cli/src/commands/manifest.rs +++ b/crates/ff-rdp-cli/src/commands/manifest.rs @@ -74,6 +74,10 @@ pub fn run(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&results, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) diff --git a/crates/ff-rdp-cli/src/commands/nav_action.rs b/crates/ff-rdp-cli/src/commands/nav_action.rs index c19795f..36d6491 100644 --- a/crates/ff-rdp-cli/src/commands/nav_action.rs +++ b/crates/ff-rdp-cli/src/commands/nav_action.rs @@ -126,6 +126,10 @@ pub fn run(cli: &Cli, action: NavAction) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result, 1, &meta); let hint_source = match action { @@ -213,7 +217,13 @@ fn run_reload_wait_idle_daemon( } }; - emit_reload_result(cli, requests_observed + inflight_count, idle_at_ms, force) + emit_reload_result( + cli, + requests_observed + inflight_count, + idle_at_ms, + force, + true, + ) } /// True when an I/O error kind signals the peer closed the connection @@ -302,7 +312,7 @@ fn run_reload_wait_idle_direct( let _ = WatcherActor::unwatch_resources(ctx.transport_mut(), &watcher_actor, &["network-event"]); - emit_reload_result(cli, requests_observed, idle_at_ms, force) + emit_reload_result(cli, requests_observed, idle_at_ms, force, false) } /// Drain network events from `transport` until idle or timeout. @@ -392,6 +402,7 @@ fn emit_reload_result( requests_observed: u64, idle_at_ms: u64, force: bool, + via_daemon: bool, ) -> Result<(), AppError> { let result = if force { json!({ @@ -415,6 +426,8 @@ fn emit_reload_result( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose. + crate::connection_meta::merge_route(&mut meta, via_daemon); let envelope = output::envelope(&result, 1, &meta); let hint_ctx = HintContext::new(HintSource::Reload); diff --git a/crates/ff-rdp-cli/src/commands/navigate.rs b/crates/ff-rdp-cli/src/commands/navigate.rs index 1a76e32..cdf220b 100644 --- a/crates/ff-rdp-cli/src/commands/navigate.rs +++ b/crates/ff-rdp-cli/src/commands/navigate.rs @@ -1480,7 +1480,7 @@ pub fn run_core( cli: &Cli, url: &str, wait_opts: &WaitAfterNav<'_>, -) -> Result { +) -> Result<(serde_json::Value, bool), AppError> { validate_url_with_opts(url, cli.allow_file_urls, cli.allow_unsafe_urls)?; let mut ctx = connect_and_get_target(cli)?; let target_actor = ctx.target.actor.clone(); @@ -1795,7 +1795,7 @@ pub fn run_core( { obj.insert("wait_for".to_string(), wf); } - Ok(result) + Ok((result, ctx.via_daemon)) } /// Run the iter-129 CMP-detection-and-accept flow and merge its result into @@ -1828,7 +1828,7 @@ pub fn run( wait_opts: &WaitAfterNav<'_>, auto_consent: bool, ) -> Result<(), AppError> { - let mut result = run_core(cli, url, wait_opts)?; + let (mut result, via_daemon) = run_core(cli, url, wait_opts)?; if auto_consent { merge_auto_consent(cli, &mut result); } @@ -1840,6 +1840,9 @@ pub fn run( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — matches the + // `--with-network` variant below, which already got this in iter-128. + crate::connection_meta::merge_route(&mut meta, via_daemon); let envelope = output::envelope(&result, 1, &meta); let hint_ctx = HintContext::new(HintSource::Navigate); diff --git a/crates/ff-rdp-cli/src/commands/page_text.rs b/crates/ff-rdp-cli/src/commands/page_text.rs index 1e63778..b586c24 100644 --- a/crates/ff-rdp-cli/src/commands/page_text.rs +++ b/crates/ff-rdp-cli/src/commands/page_text.rs @@ -31,6 +31,10 @@ pub fn run(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); // `results` holds the text string directly; the old `.text` alias has been // removed (iter-61j A1). Use `--jq '.results'` to extract the text. let envelope = output::envelope(&json!(text), 1, &meta); diff --git a/crates/ff-rdp-cli/src/commands/perf.rs b/crates/ff-rdp-cli/src/commands/perf.rs index 9bb94a2..623de50 100644 --- a/crates/ff-rdp-cli/src/commands/perf.rs +++ b/crates/ff-rdp-cli/src/commands/perf.rs @@ -482,6 +482,10 @@ pub fn run(cli: &Cli, entry_type: &str, filter: Option<&str>) -> Result<(), AppE None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope_with_truncation(&json!(limited), shown, total, truncated, &meta); @@ -645,6 +649,10 @@ pub fn run_vitals(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&results, 1, &meta); let hint_ctx = HintContext::new(HintSource::PerfVitals); @@ -812,6 +820,10 @@ pub fn run_summary(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&results, 1, &meta); let hint_ctx = HintContext::new(HintSource::PerfSummary); @@ -1339,6 +1351,10 @@ pub fn run_audit(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&results, 1, &meta); let hint_ctx = HintContext::new(HintSource::PerfAudit); @@ -1558,6 +1574,10 @@ pub fn run_group_by_domain( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&json!(results), total, &meta); let hint_ctx = HintContext::new(HintSource::Perf); diff --git a/crates/ff-rdp-cli/src/commands/perf_compare.rs b/crates/ff-rdp-cli/src/commands/perf_compare.rs index c3078a7..9f5368f 100644 --- a/crates/ff-rdp-cli/src/commands/perf_compare.rs +++ b/crates/ff-rdp-cli/src/commands/perf_compare.rs @@ -383,6 +383,10 @@ pub fn run(cli: &Cli, urls: &[String], labels: Option<&[String]>) -> Result<(), None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&Value::Array(results), total, &meta); let hint_ctx = HintContext::new(HintSource::Perf); diff --git a/crates/ff-rdp-cli/src/commands/responsive.rs b/crates/ff-rdp-cli/src/commands/responsive.rs index 09dc94d..0ff2672 100644 --- a/crates/ff-rdp-cli/src/commands/responsive.rs +++ b/crates/ff-rdp-cli/src/commands/responsive.rs @@ -399,6 +399,10 @@ pub fn run( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); // Text short-circuit: render a human-readable breakpoint table instead of JSON. if cli.format == "text" && cli.jq.is_none() { diff --git a/crates/ff-rdp-cli/src/commands/screenshot.rs b/crates/ff-rdp-cli/src/commands/screenshot.rs index dbda950..84ff36a 100644 --- a/crates/ff-rdp-cli/src/commands/screenshot.rs +++ b/crates/ff-rdp-cli/src/commands/screenshot.rs @@ -402,6 +402,11 @@ pub fn run(cli: &Cli, opts: &ScreenshotOpts<'_>) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: screenshot always connects directly (see `run_core`'s doc + // comment — the daemon's watcher subscription breaks the two-step + // capture protocol), and the `--window-size` batch path never opens an + // RDP connection at all, so the route is unconditionally "direct". + crate::connection_meta::merge_route(&mut meta, false); let envelope = output::envelope(&results, 1, &meta); let hint_ctx = HintContext::new(HintSource::Screenshot); diff --git a/crates/ff-rdp-cli/src/commands/scroll.rs b/crates/ff-rdp-cli/src/commands/scroll.rs index d0fcacb..58c6e11 100644 --- a/crates/ff-rdp-cli/src/commands/scroll.rs +++ b/crates/ff-rdp-cli/src/commands/scroll.rs @@ -114,6 +114,10 @@ pub fn run_to( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result_json, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) @@ -179,6 +183,10 @@ pub fn run_by( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result_json, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) @@ -248,6 +256,10 @@ fn run_scroll_absolute(cli: &Cli, y_expr: &str, error_label: &str) -> Result<(), None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result_json, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) @@ -308,6 +320,10 @@ pub fn run_container( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result_json, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) @@ -436,6 +452,10 @@ pub fn run_until( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result_json, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) @@ -506,6 +526,10 @@ pub fn run_text(cli: &Cli, text: &str) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&result_json, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) diff --git a/crates/ff-rdp-cli/src/commands/snapshot.rs b/crates/ff-rdp-cli/src/commands/snapshot.rs index 5460d55..c6a6a58 100644 --- a/crates/ff-rdp-cli/src/commands/snapshot.rs +++ b/crates/ff-rdp-cli/src/commands/snapshot.rs @@ -128,6 +128,10 @@ pub fn run(cli: &Cli, depth: u32, max_chars: u32) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let total = match &results { Value::Null => 0, diff --git a/crates/ff-rdp-cli/src/commands/sources.rs b/crates/ff-rdp-cli/src/commands/sources.rs index e98c4da..4cfcf30 100644 --- a/crates/ff-rdp-cli/src/commands/sources.rs +++ b/crates/ff-rdp-cli/src/commands/sources.rs @@ -179,6 +179,10 @@ pub fn run(cli: &Cli, filter: Option<&str>, pattern: Option<&str>) -> Result<(), m.insert("fallback".to_string(), json!(true)); m.insert("fallback_method".to_string(), json!(method_str)); } + // iter-134: always present, not gated by --verbose — an agent can tell + // how this command executed without a separate `daemon status` + // round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope_with_truncation(&result_json, shown, total, truncated, &meta); let hint_ctx = HintContext::new(HintSource::Sources); diff --git a/crates/ff-rdp-cli/src/commands/storage.rs b/crates/ff-rdp-cli/src/commands/storage.rs index 760a3f5..29e3ebb 100644 --- a/crates/ff-rdp-cli/src/commands/storage.rs +++ b/crates/ff-rdp-cli/src/commands/storage.rs @@ -35,6 +35,10 @@ pub fn run(cli: &Cli, storage_type: &str, key: Option<&str>) -> Result<(), AppEr None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); if let Some(k) = key { // Single-key lookup: embed key as a JSON-encoded string literal to diff --git a/crates/ff-rdp-cli/src/commands/styles.rs b/crates/ff-rdp-cli/src/commands/styles.rs index 4cdfab2..133c5dc 100644 --- a/crates/ff-rdp-cli/src/commands/styles.rs +++ b/crates/ff-rdp-cli/src/commands/styles.rs @@ -106,6 +106,10 @@ pub fn run(cli: &Cli, selector: &str, properties: Option<&[String]>) -> Result<( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope_with_truncation(&results, shown, total, truncated, &meta); @@ -177,6 +181,10 @@ pub fn run_applied(cli: &Cli, selector: &str) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope_with_truncation(&results, shown, total, truncated, &meta); @@ -203,6 +211,10 @@ pub fn run_layout(cli: &Cli, selector: &str) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, ctx.via_daemon); let envelope = output::envelope(&results, 1, &meta); diff --git a/crates/ff-rdp-cli/src/commands/tabs.rs b/crates/ff-rdp-cli/src/commands/tabs.rs index 77b129f..cda8a44 100644 --- a/crates/ff-rdp-cli/src/commands/tabs.rs +++ b/crates/ff-rdp-cli/src/commands/tabs.rs @@ -52,6 +52,11 @@ pub fn run(cli: &Cli) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: `tabs` connects via a raw `RdpConnection::connect` above, + // bypassing `ConnectedTab`/the daemon entirely — there is no + // daemon-vs-direct routing decision to report, so the route is + // unconditionally "direct". + crate::connection_meta::merge_route(&mut meta, false); // Use envelope_with_truncation so --limit emits the same `truncated` // signal as other OutputControls-backed list commands (e.g. network, dom). let envelope = diff --git a/crates/ff-rdp-cli/src/commands/throttle.rs b/crates/ff-rdp-cli/src/commands/throttle.rs index e9ea9e7..b6165d9 100644 --- a/crates/ff-rdp-cli/src/commands/throttle.rs +++ b/crates/ff-rdp-cli/src/commands/throttle.rs @@ -213,6 +213,10 @@ pub fn run(cli: &Cli, args: &ThrottleArgs) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose — an + // agent can tell how this command executed without a + // separate `daemon status` round-trip. + crate::connection_meta::merge_route(&mut meta, via_daemon); let envelope = output::envelope(&results, 1, &meta); OutputPipeline::from_cli(cli)?.finalize(&envelope) diff --git a/crates/ff-rdp-cli/src/commands/type_text.rs b/crates/ff-rdp-cli/src/commands/type_text.rs index 75fe73c..94fa885 100644 --- a/crates/ff-rdp-cli/src/commands/type_text.rs +++ b/crates/ff-rdp-cli/src/commands/type_text.rs @@ -40,7 +40,7 @@ pub fn run_core( text: &str, clear: bool, opts: &TypeOptions<'_>, -) -> Result { +) -> Result<(serde_json::Value, bool), AppError> { let mut ctx = connect_and_get_target(cli)?; let console_actor = ctx.target.console_actor.clone(); @@ -132,7 +132,7 @@ pub fn run_core( result["match_count"] = json!(match_count); result["chosen_index"] = json!(chosen_index); } - Ok(result) + Ok((result, ctx.via_daemon)) } pub fn run( @@ -142,7 +142,7 @@ pub fn run( clear: bool, opts: &TypeOptions<'_>, ) -> Result<(), AppError> { - let mut result_json = run_core(cli, selector, text, clear, opts)?; + let (mut result_json, via_daemon) = run_core(cli, selector, text, clear, opts)?; // Preserve the pre-iter-61c CLI output shape: `settle_method` belongs in // `meta`, not in `results`. The script runner reads it from `results`. @@ -160,6 +160,8 @@ pub fn run( None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose. + crate::connection_meta::merge_route(&mut meta, via_daemon); let envelope = output::envelope(&result_json, 1, &meta); let hint_ctx = HintContext::new(HintSource::TypeText).with_selector(selector); diff --git a/crates/ff-rdp-cli/src/commands/wait.rs b/crates/ff-rdp-cli/src/commands/wait.rs index e3f5dfb..fff3178 100644 --- a/crates/ff-rdp-cli/src/commands/wait.rs +++ b/crates/ff-rdp-cli/src/commands/wait.rs @@ -41,7 +41,14 @@ fn warn_if_timeout_alias_used() { /// Wait for a condition and return the result value without printing. /// /// Called by the script runner, which handles its own NDJSON output. -pub fn run_core(cli: &Cli, opts: &WaitOptions<'_>) -> Result { +/// Returns the result value alongside the resolved route (`Some(via_daemon)`), +/// or `None` when `--sleep-ms` took the no-connection short-circuit below — +/// there is no route to report because no Firefox connection was ever +/// resolved (iter-134: `meta.route` on every command). +pub fn run_core( + cli: &Cli, + opts: &WaitOptions<'_>, +) -> Result<(serde_json::Value, Option), AppError> { // iter-142 Theme F: --sleep-ms is a plain delay — no condition to poll, // no Firefox connection needed at all. Takes priority over the other // fields so a caller that somehow sets both never falls through to the @@ -49,9 +56,10 @@ pub fn run_core(cli: &Cli, opts: &WaitOptions<'_>) -> Result) -> Result) -> Result<(), AppError> { warn_if_timeout_alias_used(); - let result_json = run_core(cli, opts)?; + let (result_json, via_daemon) = run_core(cli, opts)?; let mut meta = json!({}); crate::connection_meta::merge_into_if_verbose( &mut meta, @@ -115,6 +126,11 @@ pub fn run(cli: &Cli, opts: &WaitOptions<'_>) -> Result<(), AppError> { None, cli.is_verbose(), ); + // iter-134: always present, not gated by --verbose, except when + // --sleep-ms short-circuited before resolving a connection at all. + if let Some(via_daemon) = via_daemon { + crate::connection_meta::merge_route(&mut meta, via_daemon); + } let envelope = output::envelope(&result_json, 1, &meta); let hint_ctx = HintContext::new(HintSource::Wait); @@ -215,11 +231,16 @@ mod tests { wait_timeout: 5000, }; let started = std::time::Instant::now(); - let result = run_core(&cli, &opts).expect("sleep form must succeed with no connection"); + let (result, via_daemon) = + run_core(&cli, &opts).expect("sleep form must succeed with no connection"); let elapsed = started.elapsed(); assert_eq!(result["matched"], true); assert_eq!(result["elapsed_ms"], 5); + assert_eq!( + via_daemon, None, + "sleep form never resolves a connection, so there is no route to report" + ); assert!( elapsed >= std::time::Duration::from_millis(5), "must actually sleep for the requested duration, elapsed={elapsed:?}" diff --git a/crates/ff-rdp-cli/src/script/runner.rs b/crates/ff-rdp-cli/src/script/runner.rs index f5a0aee..3718a5a 100644 --- a/crates/ff-rdp-cli/src/script/runner.rs +++ b/crates/ff-rdp-cli/src/script/runner.rs @@ -769,7 +769,7 @@ fn execute_navigate( // covers those cases without changing behaviour on event-rich pages. wait_strategy: crate::commands::navigate::WaitStrategy::Both, }; - nav_run_core(cli, &effective_url, &wait_opts) + nav_run_core(cli, &effective_url, &wait_opts).map(|(v, _)| v) } fn resolve_element_target_selector( @@ -813,6 +813,7 @@ fn execute_click(step: &super::format::ElementStep, cli: &Cli) -> Result` to target), so +//! this is gated on `FF_RDP_LIVE_TESTS=1` rather than the network tier — +//! matching `live_128_meta_route`'s gating rationale. +//! +//! daemon-parity: `live_134_meta_route_all_commands` itself runs each +//! command in BOTH modes (`--no-daemon` first, then the daemon-routed +//! default) within the same test — that comparison is the entire point of +//! `meta.route`, so there is no separate suite to point at. +//! +//! # Running +//! +//! FF_RDP_LIVE_TESTS=1 cargo test-live -p ff-rdp-cli \ +//! --test live live_134_meta_route_all_commands -- --nocapture + +use std::process::{Command, Output}; + +use serde_json::Value; + +use crate::common::{LiveFirefox, ff_rdp_bin, live_tests_enabled}; + +/// Bare `--host`/`--port`/`--timeout` args with NO daemon-mode opinion — +/// deliberately not `common::base_args`, which hardcodes `--no-daemon`. This +/// test needs to control that flag itself to compare both routes. +fn base_args(port: u16) -> Vec { + vec![ + "--host".to_owned(), + "127.0.0.1".to_owned(), + "--port".to_owned(), + port.to_string(), + "--timeout".to_owned(), + "30000".to_owned(), + ] +} + +fn stop_daemon(port: u16) { + let _ = Command::new(ff_rdp_bin()) + .args(["--host", "127.0.0.1", "--port", &port.to_string()]) + .args(["daemon", "stop"]) + .output(); +} + +fn parse_json(output: &Output) -> Value { + let s = String::from_utf8_lossy(&output.stdout); + serde_json::from_str(s.trim()).unwrap_or_else(|e| { + panic!( + "stdout is not valid JSON: {e}\nstdout={s}\nstderr={}", + String::from_utf8_lossy(&output.stderr) + ) + }) +} + +/// Run `args` against `port` with the given extra flags (e.g. `--no-daemon`) +/// prepended before the subcommand, and return the parsed JSON envelope. +/// Panics with full stdout/stderr on a non-zero exit so a failure is +/// diagnosable without re-running by hand. +fn run_json(port: u16, extra_flags: &[&str], args: &[&str]) -> Value { + let out = Command::new(ff_rdp_bin()) + .args(base_args(port)) + .args(extra_flags) + .args(args) + .output() + .unwrap_or_else(|e| panic!("spawn ff-rdp {args:?}: {e}")); + assert!( + out.status.success(), + "command {args:?} (flags {extra_flags:?}) failed: stdout={} stderr={}", + String::from_utf8_lossy(&out.stdout), + String::from_utf8_lossy(&out.stderr) + ); + parse_json(&out) +} + +/// `live_134_meta_route_all_commands`: for `click`, `eval`, `screenshot`, +/// and `dom`, `meta.route` is present and correct (`"daemon"` by default, +/// `"direct"` under `--no-daemon`) — without `--verbose`. +#[test] +#[ignore = "requires Firefox and FF_RDP_LIVE_TESTS=1"] +fn live_134_meta_route_all_commands() { + if !live_tests_enabled() { + eprintln!("live_134_meta_route_all_commands: set FF_RDP_LIVE_TESTS=1 to run"); + return; + } + + let Some(ff) = LiveFirefox::headless_on_random_port() else { + eprintln!("live_134_meta_route_all_commands: Firefox not available — skipping"); + return; + }; + let port = ff.port(); + + // Each (command, args, default_mode_route) triple targets `body`, which + // `about:blank` always has — no navigate/fixture-server setup needed, + // keeping this test network-free per the module doc. `default_mode_route` + // is what the command reports WITHOUT `--no-daemon`: "daemon" for the + // three that use `connect_and_get_target` (click/eval/dom), but + // `screenshot` always uses `connect_direct` regardless of daemon mode + // (its `run_core` doc comment explains why: the daemon's watcher + // subscription breaks the two-step capture protocol) — its route is + // "direct" in BOTH modes, which is itself the behaviour under test: + // `meta.route` reflects the connection Firefox actually saw, not the + // flag the caller passed. + let commands: &[(&str, &[&str], &str)] = &[ + ("click", &["click", "body", "--no-wait"], "daemon"), + ("eval", &["eval", "1+1"], "daemon"), + ("screenshot", &["screenshot", "--base64"], "direct"), + ("dom", &["dom", "body", "--count"], "daemon"), + ]; + + // Direct FIRST: --no-daemon bypasses the daemon proxy entirely, and + // doing this before any daemon exists sidesteps `daemon stop`'s + // process-group reap (iter-95 Theme A) taking the directly launched + // Firefox process down with it — same ordering rationale as + // `live_128_meta_route`. + for (name, args, _) in commands { + let json = run_json(port, &["--no-daemon"], args); + assert_eq!( + json["meta"]["route"], "direct", + "`{name} --no-daemon` must report meta.route == \"direct\" without --verbose, got: {json}" + ); + } + + // Daemon-routed SECOND: no --no-daemon, so connect_and_get_target + // resolves (and auto-starts) the daemon proxy against the same Firefox + // for the commands that actually route through it. + for (name, args, expected) in commands { + let json = run_json(port, &[], args); + assert_eq!( + json["meta"]["route"], *expected, + "`{name}` (default mode) must report meta.route == \"{expected}\" without --verbose, got: {json}" + ); + } + stop_daemon(port); + + eprintln!( + "live_134_meta_route_all_commands: PASSED — click/eval/screenshot/dom all report \ + meta.route (\"direct\" under --no-daemon, \"daemon\" by default)" + ); +} diff --git a/crates/ff-rdp-cli/tests/live/main.rs b/crates/ff-rdp-cli/tests/live/main.rs index a774ab2..0f4d7a9 100644 --- a/crates/ff-rdp-cli/tests/live/main.rs +++ b/crates/ff-rdp-cli/tests/live/main.rs @@ -47,6 +47,7 @@ mod live_130_navigation_truthfulness; mod live_131_measurement_honesty; mod live_132_cli_polish; mod live_133_viewport_emulation; +mod live_134_meta_route_all_commands; mod live_135_screenshot_ff153; mod live_137_daemon_mode_parity; mod live_138_navigation_truthfulness_2; diff --git a/kb/iterations/iteration-134-meta-route-all-commands.md b/kb/iterations/iteration-134-meta-route-all-commands.md index 48c911a..2ef9175 100644 --- a/kb/iterations/iteration-134-meta-route-all-commands.md +++ b/kb/iterations/iteration-134-meta-route-all-commands.md @@ -14,7 +14,7 @@ dogfood_path: | ff-rdp screenshot --jq '.meta.route' # → same "daemon"/"direct" self-identification on every browser-touching command first_call_sites: [] -status: planned +status: done --- # Iteration 134: meta.route on every command (carry-over from iter-128 Theme D) @@ -35,23 +35,47 @@ crates/ff-rdp-cli/src/commands/` for the full list), each of which already has ## Tasks -- [ ] Sweep every `crate::connection_meta::merge_into_if_verbose(&mut meta, ...)` call +- [x] Sweep every `crate::connection_meta::merge_into_if_verbose(&mut meta, ...)` call site and add an adjacent `crate::connection_meta::merge_route(&mut meta, ctx.via_daemon)` (or the equivalent local `via_daemon` binding). -- [ ] For commands using `connect_direct` (always `via_daemon: false`), still call +- [x] For commands using `connect_direct` (always `via_daemon: false`), still call `merge_route` so `meta.route` is present and consistently `"direct"`. -- [ ] Consider whether commands that build a bespoke envelope (bypassing +- [x] Consider whether commands that build a bespoke envelope (bypassing `output::envelope`/`envelope_with_truncation`) need a call-site audit too. -- [ ] Grep `crates/ff-rdp-cli/src/commands/*.rs` for envelope construction NOT preceded +- [x] Grep `crates/ff-rdp-cli/src/commands/*.rs` for envelope construction NOT preceded by `merge_route` after the sweep — should be zero for browser-touching commands. -## Acceptance Criteria [0/1] +## Acceptance Criteria [1/1] -- [ ] live_134_meta_route_all_commands: for a representative sample of browser-touching +- [x] live_134_meta_route_all_commands: for a representative sample of browser-touching commands (e.g. `click`, `eval`, `screenshot`, `dom`), `meta.route` is present and correct (`"daemon"` by default, `"direct"` under `--no-daemon`) without `--verbose`. ## Notes -Non-browser-touching commands (`daemon status`, `doctor`) are out of scope — they don't -resolve a `via_daemon` in the first place. +Non-browser-touching commands (`daemon status`, `doctor`, `launch`, `throttle status`) are +out of scope — they don't resolve a `via_daemon` in the first place (`launch` spawns +Firefox but never opens a `ConnectedTab`; `throttle status`/`daemon status` only read +registry files). + +`tabs` and `screenshot` always report `"direct"` regardless of daemon mode: `tabs` uses a +raw `RdpConnection::connect` that bypasses `ConnectedTab`/the daemon entirely, and +`screenshot` always calls `connect_direct` (the daemon's watcher subscription breaks its +two-step capture protocol — see `run_core`'s doc comment). `meta.route` reports the +connection Firefox actually saw, not the flag the caller passed, so both are hardcoded +`merge_route(&mut meta, false)` rather than threading a `ctx.via_daemon` that would always +be `false` anyway. + +`navigate`'s plain `run()`/`run_core()`, `click::run_core`, `type_text::run_core`, and +`wait::run_core` didn't expose `via_daemon` to their `run()` callers (only the inner +`run_core` held the `ConnectedTab`) — their signatures changed to return +`(Value, bool)` (or `(Value, Option)` for `wait`, whose `--sleep-ms` short-circuit +never opens a connection at all). The script runner (`src/script/runner.rs`) and +`index.rs`'s crawl path call these same `run_core`s; both were updated to discard or +destructure the added `bool`. + +`sources.rs` (connects via `connect_direct`) turned up during the final envelope-construction +audit (Task 4): it builds its own `meta` object but never called +`merge_into_if_verbose` in the first place, so it wasn't in the original +`merge_into_if_verbose` grep. Only `merge_route` was added here — the missing connection +block is a separate, pre-existing gap left out of scope for this iteration. diff --git a/kb/iterations/iteration-143-native-a11y-tree.md b/kb/iterations/iteration-143-native-a11y-tree.md index 92b601e..c6181bf 100644 --- a/kb/iterations/iteration-143-native-a11y-tree.md +++ b/kb/iterations/iteration-143-native-a11y-tree.md @@ -54,6 +54,17 @@ Fix: report the source in `meta` (e.g. `"source": "native" | "js-fallback"` plus reason when it fell back). Same treatment for `a11y audit`/`--interactive` if they share the path. +Implementation precedent: [[iteration-134-meta-route-all-commands]] just rolled out the +same "always present regardless of `--verbose`" shape for `meta.route` via a small +`connection_meta::merge_route(&mut meta, via_daemon)` helper called at every relevant +command's meta-building call site (not gated behind the existing +`merge_into_if_verbose`). A `connection_meta::merge_source` (or local equivalent) called +the same way — right before `output::envelope(...)` in `a11y::run`/`run_critical` and +`a11y audit` — is the straightforward way to satisfy this theme without inventing a new +pattern. Note `a11y.rs` and `a11y_contrast.rs` already gained a `merge_route` call site in +iter-134; `meta.source` is an independent field added at the same call site, not a +replacement for it. + ### Theme B — no way to ask for the real thing There is no opt-in. A user who wants the platform tree has to enable Firefox @@ -65,9 +76,12 @@ restores the previous state afterwards when it was ff-rdp that turned it on. Mus honestly: if `enable` fails or `bootstrap` still reports disabled, say so rather than silently falling back. -Open question to settle first: should `--native` also become the default once it is -proven, or stay opt-in because of the global performance cost? Decide in -[[decision-log]] before writing the flag. +**Resolved before this iteration started**: [[decision-log]] DEC-027 (filed ahead of +iter-143 landing) already answers the default-vs-opt-in question — `--native` stays +opt-in, never the default, because `enable()` is browser-global/process-wide and its +`disable()` can be blocked by an active Windows screen reader. No further decision-log +work is needed for this theme; implement the flag opt-in from the start rather than +re-opening the question. ### Theme C — bound the stall @@ -76,7 +90,7 @@ the service is off will block for the full socket read timeout. Consider a short purpose-specific deadline on accessibility walker requests so a mistake costs milliseconds, not the default timeout. -## Acceptance Criteria +## Acceptance Criteria [1/5] - [ ] live_a11y_source_meta: `ff-rdp a11y` output carries a `meta.source` of `js-fallback` against a Firefox with the accessibility service off @@ -86,7 +100,10 @@ milliseconds, not the default timeout. `bootstrap().state.enabled` is back to its pre-run value - [ ] unit/e2e: enable failure surfaces as an explicit error or an annotated fallback, never a silent one -- [ ] [[decision-log]] records the default-vs-opt-in decision +- [x] [[decision-log]] records the default-vs-opt-in decision — done ahead of this + iteration's implementation work: DEC-027 (filed on main before this branch existed) + settles opt-in-never-default. No code landed yet for this iteration; only the + decision-log prerequisite is satisfied. ## Notes