diff --git a/dev/config.example.toml b/dev/config.example.toml index b22a62a251..3ec1732019 100644 --- a/dev/config.example.toml +++ b/dev/config.example.toml @@ -103,8 +103,7 @@ # # columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set) # -# task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables -# timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +# timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables # # `columns` selects and orders the columns to render; omit it for the default set. # It is meant to drive a per-invocation alias (https://worktrunk.dev/extending/#aliases) diff --git a/docs/content/config.md b/docs/content/config.md index 4aaa63c298..3184b2175f 100644 --- a/docs/content/config.md +++ b/docs/content/config.md @@ -190,8 +190,7 @@ json-schema = 2 # JSON output schema: 2 (envelope) or 1 (bare array, the curr columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set) -task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables -timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables ``` `columns` selects and orders the columns to render; omit it for the default set. diff --git a/plugins/worktrunk/skills/worktrunk/reference/config.md b/plugins/worktrunk/skills/worktrunk/reference/config.md index 5116d8a0f0..aa83b13a81 100644 --- a/plugins/worktrunk/skills/worktrunk/reference/config.md +++ b/plugins/worktrunk/skills/worktrunk/reference/config.md @@ -189,8 +189,7 @@ json-schema = 2 # JSON output schema: 2 (envelope) or 1 (bare array, the curr columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set) -task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables -timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables ``` `columns` selects and orders the columns to render; omit it for the default set. diff --git a/skills/worktrunk/reference/config.md b/skills/worktrunk/reference/config.md index 5116d8a0f0..aa83b13a81 100644 --- a/skills/worktrunk/reference/config.md +++ b/skills/worktrunk/reference/config.md @@ -189,8 +189,7 @@ json-schema = 2 # JSON output schema: 2 (envelope) or 1 (bare array, the curr columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set) -task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables -timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables ``` `columns` selects and orders the columns to render; omit it for the default set. diff --git a/src/cli/mod.rs b/src/cli/mod.rs index 0daf30c4ac..127c5610b3 100644 --- a/src/cli/mod.rs +++ b/src/cli/mod.rs @@ -2055,8 +2055,7 @@ json-schema = 2 # JSON output schema: 2 (envelope) or 1 (bare array, the curr columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set) -task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables -timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables ``` `columns` selects and orders the columns to render; omit it for the default set. diff --git a/src/commands/list/collect/mod.rs b/src/commands/list/collect/mod.rs index 04701f5426..32ae1a614a 100644 --- a/src/commands/list/collect/mod.rs +++ b/src/commands/list/collect/mod.rs @@ -597,7 +597,6 @@ pub enum ShowConfig { Resolved { show_branches: bool, show_remotes: bool, - command_timeout: Option, /// Wall-clock deadline for the collect phase. `None` uses the default /// [`DRAIN_TIMEOUT`](results::DRAIN_TIMEOUT) and shows a warning on timeout. collect_deadline: Option, @@ -612,7 +611,7 @@ pub enum ShowConfig { }, /// Raw CLI flags; config resolution deferred to collect's parallel phase /// so project_identifier runs concurrently with other git operations. - /// Timeouts are resolved from config internally. + /// The collect deadline is resolved from config internally. DeferredToParallel { cli_branches: bool, cli_remotes: bool, @@ -908,7 +907,6 @@ pub fn collect( show_branches, show_remotes, show_full, - command_timeout, collect_deadline, list_width, progressive_handler, @@ -917,7 +915,6 @@ pub fn collect( ShowConfig::Resolved { show_branches, show_remotes, - command_timeout, collect_deadline, list_width, progressive_handler, @@ -930,7 +927,6 @@ pub fn collect( // opts out of the untracked-inclusive working diff — the last tuple // field — so the two `show_full`-shaped values aren't the same bucket. true, - command_timeout, collect_deadline, list_width, progressive_handler, @@ -945,19 +941,16 @@ pub fn collect( let show_branches = cli_branches || config.list.branches(); let show_remotes = cli_remotes || config.list.remotes(); let show_full = cli_full || config.list.full(); - // Resolve timeouts from merged config (--full disables both) - let (command_timeout, collect_deadline) = if show_full { - (None, None) + // Resolve the collect budget from merged config (--full disables it) + let collect_deadline = if show_full { + None } else { - let task_timeout = config.list.task_timeout(); - let deadline = config.list.timeout().map(|d| std::time::Instant::now() + d); - (task_timeout, deadline) + config.list.timeout().map(|d| std::time::Instant::now() + d) }; ( show_branches, show_remotes, show_full, - command_timeout, collect_deadline, None, None, @@ -1592,15 +1585,6 @@ pub fn collect( if let Some(snap_arc) = snap.as_ref() { let snap_for_primer = std::sync::Arc::clone(snap_arc); s.spawn(move |_| { - // Honor `list.task-timeout-ms` for the primer's git - // commands — these are the same `for-each-ref - // %(ahead-behind)` / `rev-list` invocations that - // used to run inside `UpstreamTask`, where the - // worker loop sets the per-thread timeout. Without - // this, `wt list` could sit at the skeleton on a - // pathologically slow git until the (untimed) batch - // returned. - worktrunk::shell_exec::set_command_timeout(command_timeout); let all_locals = snap_for_primer.local_branches(); let filtered_locals: Vec; let candidates: &[LocalBranch] = if show_branches { @@ -1780,7 +1764,6 @@ pub fn collect( // when the picker is open. See `COLLECT_POOL`. COLLECT_POOL.install(|| { all_work_items.into_par_iter().for_each(|item| { - worktrunk::shell_exec::set_command_timeout(command_timeout); let result = item.execute(); let _ = tx_worker.send(result); }); diff --git a/src/commands/picker/mod.rs b/src/commands/picker/mod.rs index 9f220e57b7..b5b4a93ee2 100644 --- a/src/commands/picker/mod.rs +++ b/src/commands/picker/mod.rs @@ -1377,7 +1377,6 @@ struct PipelineFactory { header_flash: Arc, preview_dims: (usize, usize), skim_list_width: usize, - command_timeout: Option, llm_command: Option, summary_hint: Option, show_branches: bool, @@ -1507,7 +1506,6 @@ impl PipelineFactory { let bg_repo = spawn_repo.clone(); let show_branches = self.show_branches; let show_remotes = self.show_remotes; - let command_timeout = self.command_timeout; let skim_list_width = self.skim_list_width; let collect_handle = std::thread::Builder::new() .name("picker-collect".into()) @@ -1517,7 +1515,6 @@ impl PipelineFactory { collect::ShowConfig::Resolved { show_branches, show_remotes, - command_timeout, collect_deadline: None, list_width: Some(skim_list_width), progressive_handler: Some(bg_handler), @@ -1726,10 +1723,6 @@ pub fn handle_picker( // the same live status. `--prs` rows carry their own number from the explicit // `--prs` forge call. - // Per-task command timeout (bounds any single git invocation) from - // shared `[list]` config. Still applies in progressive mode. - let command_timeout = config.list.task_timeout(); - // Progressive rendering means the picker never blocks waiting for // collect — so there's no UI-freeze budget to bound. The drain runs // until its results channel closes or the fallback DRAIN_TIMEOUT @@ -1872,7 +1865,6 @@ summary = true header_flash: Arc::new(items::HeaderFlash::default()), preview_dims, skim_list_width, - command_timeout, llm_command, summary_hint, show_branches, @@ -3158,7 +3150,6 @@ pub mod tests { header_flash: Arc::new(super::items::HeaderFlash::default()), preview_dims: (80, 24), skim_list_width: 80, - command_timeout: None, llm_command: None, summary_hint: None, show_branches: false, diff --git a/src/config/deprecation.rs b/src/config/deprecation.rs index 8e2ff3d75f..99da933009 100644 --- a/src/config/deprecation.rs +++ b/src/config/deprecation.rs @@ -484,6 +484,9 @@ pub enum DeprecationKind { NoCd, /// `timeout-ms` under `[switch.picker]` (removed — picker renders progressively). SwitchPickerTimeout, + /// `task-timeout-ms` under `[list]` (removed — `[list] timeout-ms` bounds + /// the collect phase). + ListTaskTimeout, /// `[list] json-schema` unset while the default is scheduled to switch to /// schema 2 — `wt config update` writes the upcoming `json-schema = 2`. /// Warns at the JSON-emitting surface (`resolve_json_schema`), not at @@ -647,6 +650,17 @@ const DEPRECATION_RULES: &[DeprecationRule] = &[ Vec::new() } }), + // list.task-timeout-ms — removed; `[list] timeout-ms` bounds the collect + // phase, and the drain has its own fallback bound. + DeprecationRule::Structural(|doc| { + if for_each_config_table_mut(doc, |_, table| { + remove_section_key_in(table, "list", "task-timeout-ms") + }) { + vec![DeprecationKind::ListTaskTimeout] + } else { + Vec::new() + } + }), // [list] json-schema unset → write json-schema = 2, adopting the default // ahead of the release that switches it. User config only: the key isn't // valid in project config, and the top-level write covers every repo @@ -1203,6 +1217,21 @@ fn remove_switch_picker_timeout_in(table: &mut toml_edit::Table) -> bool { } } +/// Remove `key` from a top-level `section` in a table (top-level or project). +/// An emptied section is left in place — it round-trips harmlessly. +/// +/// A section can be written as a section table (`[list]`) or inline +/// (`list = { … }`); `toml_edit` surfaces these as different node types, so +/// each shape gets its own branch — matching the inline-aware `no-cd`/`no-ff` +/// rules and the two-level [`remove_switch_picker_timeout_in`]. +fn remove_section_key_in(table: &mut toml_edit::Table, section: &str, key: &str) -> bool { + match table.get_mut(section) { + Some(toml_edit::Item::Table(t)) => t.remove(key).is_some(), + Some(toml_edit::Item::Value(toml_edit::Value::InlineTable(it))) => it.remove(key).is_some(), + _ => false, + } +} + fn migrate_content_from_doc(content: &str, mut doc: toml_edit::DocumentMut) -> String { if migrate_content_doc(&mut doc) { doc.to_string() @@ -1677,6 +1706,15 @@ fn format_warning_lines<'a>( )) ); } + DeprecationKind::ListTaskTimeout => { + let _ = writeln!( + out, + "{}", + warning_message(cformat!( + "{label}: list.task-timeout-ms is no longer used — list.timeout-ms bounds the collect phase" + )) + ); + } DeprecationKind::JsonSchemaUnset => { let _ = writeln!( out, @@ -3265,6 +3303,10 @@ json-schema = 1 // timeout-ms under an inline `switch` is stripped like the section form "switch = { picker = { timeout-ms = 500 } }\n", "[select]\ntimeout-ms = 500\n", + // list.task-timeout-ms, section and inline forms (project-scoped so + // the appended `[list]` below isn't a duplicate table) + "[projects.\"github.com/u/r\".list]\ntask-timeout-ms = 500\n", + "[projects.\"github.com/u/r\"]\nlist = { task-timeout-ms = 500 }\n", "worktree-path = \"../{{ repo_root }}.{{ branch }}\"\n", "[projects.\"github.com/u/r\"]\napproved-commands = [\"npm test\"]\n", ]; @@ -4302,6 +4344,104 @@ pager = "delta" ); } + #[test] + fn test_detect_list_task_timeout_top_level() { + let content = r#" +[list] +branches = true +task-timeout-ms = 500 +"#; + let deprecations = detect_deprecations(content, ConfigFileKind::User); + assert!(has_kind(&deprecations, |k| matches!( + k, + DeprecationKind::ListTaskTimeout + ))); + } + + #[test] + fn test_detect_list_task_timeout_project_level() { + let content = r#" +[projects."github.com/user/repo".list] +task-timeout-ms = 300 +"#; + let deprecations = detect_deprecations(content, ConfigFileKind::User); + assert!(has_kind(&deprecations, |k| matches!( + k, + DeprecationKind::ListTaskTimeout + ))); + } + + #[test] + fn test_detect_list_task_timeout_absent() { + let content = r#" +[list] +timeout-ms = 500 +"#; + let deprecations = detect_deprecations(content, ConfigFileKind::User); + assert!(!has_kind(&deprecations, |k| matches!( + k, + DeprecationKind::ListTaskTimeout + ))); + } + + #[test] + fn test_migrate_list_task_timeout_removes_key() { + let content = r#" +[list] +branches = true +task-timeout-ms = 500 +timeout-ms = 2000 +"#; + let result = migrate_content(content); + assert!( + !result.contains("task-timeout-ms"), + "Should strip task-timeout-ms: {result}" + ); + assert!( + result.contains("timeout-ms = 2000") && result.contains("branches"), + "Should preserve sibling keys: {result}" + ); + } + + #[test] + fn test_migrate_list_task_timeout_inline_table() { + let content = r#" +list = { branches = true, task-timeout-ms = 500 } +"#; + let result = migrate_content(content); + assert!(!result.contains("task-timeout-ms")); + assert!(result.contains("branches")); + } + + #[test] + fn test_migrate_list_task_timeout_noop_when_absent() { + let content = r#" +[list] +timeout-ms = 500 +"#; + let result = migrate_content(content); + assert_eq!(result, content); + } + + #[test] + fn test_format_deprecation_warnings_list_task_timeout() { + let info = DeprecationInfo { + config_path: std::path::PathBuf::from("/tmp/test-config.toml"), + deprecations: vec![DeprecationKind::ListTaskTimeout], + kind: ConfigFileKind::User, + main_worktree_path: None, + }; + let output = format_deprecation_warnings(&info); + assert!( + output.contains("list.task-timeout-ms"), + "Should mention the field: {output}" + ); + assert!( + output.contains("list.timeout-ms"), + "Should point at the surviving budget: {output}" + ); + } + // ==================== negated bool format + migration tests ==================== #[test] diff --git a/src/config/user/sections.rs b/src/config/user/sections.rs index b8b072c11e..a3a7c9c8ed 100644 --- a/src/config/user/sections.rs +++ b/src/config/user/sections.rs @@ -201,13 +201,6 @@ pub struct ListConfig { #[serde(rename = "json-schema", skip_serializing_if = "Option::is_none")] pub json_schema: Option, - /// Per-task timeout in milliseconds. - /// Kills individual git commands that exceed this duration. Applies to both - /// `wt list` and the `wt switch` picker. Set to 0 to explicitly disable - /// (useful to override a global setting). Disabled when --full is used. - #[serde(rename = "task-timeout-ms", skip_serializing_if = "Option::is_none")] - pub task_timeout_ms: Option, - /// Wall-clock budget for the entire collect phase in milliseconds. /// Tasks that complete within the budget contribute data; tasks still /// running when it expires are abandoned silently. Set to 0 to disable. @@ -265,14 +258,6 @@ impl ListConfig { self.summary.unwrap_or(false) } - /// Per-task command timeout (default: None — no per-command timeout). - /// Returns `None` when disabled (task_timeout_ms = 0 or unset). - pub fn task_timeout(&self) -> Option { - self.task_timeout_ms - .filter(|&ms| ms > 0) - .map(std::time::Duration::from_millis) - } - /// Wall-clock budget for the collect phase (default: None — no budget). /// Returns `None` when disabled (timeout_ms = 0 or unset). pub fn timeout(&self) -> Option { @@ -309,7 +294,6 @@ impl Merge for ListConfig { remotes: other.remotes.or(self.remotes), summary: other.summary.or(self.summary), json_schema: other.json_schema.or(self.json_schema), - task_timeout_ms: other.task_timeout_ms.or(self.task_timeout_ms), timeout_ms: other.timeout_ms.or(self.timeout_ms), columns, custom_columns, diff --git a/src/config/user/tests.rs b/src/config/user/tests.rs index 8a58c59fa3..75f2378764 100644 --- a/src/config/user/tests.rs +++ b/src/config/user/tests.rs @@ -277,7 +277,6 @@ fn test_list_config_serde() { remotes: None, summary: None, json_schema: None, - task_timeout_ms: Some(500), timeout_ms: None, columns: vec!["branch".into(), "ci".into(), "path".into()], custom_columns: Default::default(), @@ -288,7 +287,6 @@ fn test_list_config_serde() { assert_eq!(parsed.branches, Some(false)); assert_eq!(parsed.remotes, None); assert_eq!(parsed.summary, None); - assert_eq!(parsed.task_timeout_ms, Some(500)); assert_eq!(parsed.timeout_ms, None); assert_eq!(parsed.columns, vec!["branch", "ci", "path"]); } @@ -630,7 +628,6 @@ fn test_merge_list_config() { remotes: None, summary: Some(true), json_schema: None, - task_timeout_ms: Some(1000), timeout_ms: Some(2000), columns: vec!["branch".into(), "ci".into()], custom_columns: Default::default(), @@ -641,9 +638,8 @@ fn test_merge_list_config() { remotes: Some(true), // Should override (base was None) summary: None, // Should fall back to base json_schema: None, - task_timeout_ms: None, // Should fall back to base - timeout_ms: None, // Should fall back to base - columns: Vec::new(), // Empty → fall back to base + timeout_ms: None, // Should fall back to base + columns: Vec::new(), // Empty → fall back to base custom_columns: Default::default(), }; @@ -652,7 +648,6 @@ fn test_merge_list_config() { assert_eq!(merged.branches, Some(true)); // From override assert_eq!(merged.remotes, Some(true)); // From override assert_eq!(merged.summary, Some(true)); // From base - assert_eq!(merged.task_timeout_ms, Some(1000)); // From base assert_eq!(merged.timeout_ms, Some(2000)); // From base assert_eq!(merged.columns, vec!["branch", "ci"]); // From base (override empty) } @@ -1092,7 +1087,6 @@ fn test_list_config_accessor_methods_defaults() { assert!(!config.full()); assert!(!config.branches()); assert!(!config.remotes()); - assert!(config.task_timeout().is_none()); assert!(config.timeout().is_none()); } @@ -1104,7 +1098,6 @@ fn test_list_config_accessor_methods_with_values() { remotes: Some(false), summary: Some(true), json_schema: None, - task_timeout_ms: Some(5000), timeout_ms: Some(3000), columns: Vec::new(), custom_columns: Default::default(), @@ -1113,10 +1106,6 @@ fn test_list_config_accessor_methods_with_values() { assert!(config.branches()); assert!(!config.remotes()); assert!(config.summary()); - assert_eq!( - config.task_timeout(), - Some(std::time::Duration::from_millis(5000)) - ); assert_eq!( config.timeout(), Some(std::time::Duration::from_millis(3000)) diff --git a/src/shell_exec.rs b/src/shell_exec.rs index 5ed36d356b..090c2865fe 100644 --- a/src/shell_exec.rs +++ b/src/shell_exec.rs @@ -666,32 +666,6 @@ pub fn shell_escape_for(mode: ShellEscapeMode, s: &str) -> String { } } -// ============================================================================ -// Thread-Local Command Timeout -// ============================================================================ - -use std::cell::Cell; - -thread_local! { - /// Per-thread ceiling for commands run via `run()`, set from - /// `[list] task-timeout-ms` on each Rayon collect worker so a pathologically - /// slow git can't hold up the first paint of `wt list` or the `wt switch` - /// picker. Unset by default. See [`set_command_timeout`]. - static COMMAND_TIMEOUT: Cell> = const { Cell::new(None) }; -} - -/// Set the command timeout for the current thread. -/// -/// When set, every command executed via `run()` on this thread is killed if it -/// exceeds the specified duration; a command carrying its own [`Cmd::timeout`] -/// is bound by whichever of the two is tighter. `None` disables it. -/// -/// This is typically called at the start of a Rayon worker task to apply timeout -/// to all git operations within that task. -pub fn set_command_timeout(timeout: Option) { - COMMAND_TIMEOUT.with(|t| t.set(timeout)); -} - /// Maximum lines of the bounded subprocess preview per stream. Exceeded /// content is elided with a `… (N more lines, M bytes elided)` marker; the /// full output is still written to `subprocess.log` via @@ -1318,9 +1292,6 @@ impl Cmd { /// Set a timeout for command execution (only applies to `.run()`). /// - /// Under a thread-local timeout ([`set_command_timeout`]) the tighter of the - /// two bounds the command, so this cannot widen a caller's budget. - /// /// Note: Timeout is not supported by `.stream()` since streaming commands /// are interactive and should not be time-limited. /// @@ -1529,14 +1500,6 @@ impl Cmd { let mut cmd = self.direct_command(); self.apply_common_settings(&mut cmd); - // Both timeouts are ceilings rather than a precedence chain: one bounds - // this command, the other bounds every command on the thread, so a - // command carrying both is bound by the tighter of the two. - let effective_timeout = match (self.timeout, COMMAND_TIMEOUT.with(|t| t.get())) { - (Some(own), Some(per_thread)) => Some(own.min(per_thread)), - (own, per_thread) => own.or(per_thread), - }; - // Execute with or without stdin. Every branch produces a single // `Result` so spawn/write failures resolve the trace through // `record_captured` rather than `?`-ing past it (which would leave the @@ -1566,7 +1529,7 @@ impl Cmd { } Err(e) => Err(e), } - } else if let Some(timeout_duration) = effective_timeout { + } else if let Some(timeout_duration) = self.timeout { // Timeout handling uses the existing impl run_with_timeout_impl(&mut cmd, timeout_duration) } else { @@ -2577,76 +2540,6 @@ mod tests { assert!(String::from_utf8_lossy(&output.stdout).contains("hello from stdin")); } - #[test] - fn test_thread_local_timeout_setting() { - // Initially no timeout (or whatever was set by previous test) - let initial = COMMAND_TIMEOUT.with(|t| t.get()); - - // Set a timeout - set_command_timeout(Some(Duration::from_millis(100))); - let after_set = COMMAND_TIMEOUT.with(|t| t.get()); - assert_eq!(after_set, Some(Duration::from_millis(100))); - - // Clear the timeout - set_command_timeout(initial); - let after_clear = COMMAND_TIMEOUT.with(|t| t.get()); - assert_eq!(after_clear, initial); - } - - #[test] - fn test_cmd_uses_thread_local_timeout() { - // Set no timeout (ensure fast completion) - set_command_timeout(None); - - let result = Cmd::new("echo").arg("thread local test").run(); - assert!(result.is_ok()); - - // Clean up - set_command_timeout(None); - } - - #[test] - #[cfg(unix)] - fn test_cmd_thread_local_timeout_kills_slow_command() { - // Set a short thread-local timeout - set_command_timeout(Some(Duration::from_millis(50))); - - // Command that would take too long - let result = Cmd::new("sleep").arg("10").run(); - - // Should be killed by the thread-local timeout - assert!(result.is_err()); - assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::TimedOut); - - // Clean up - set_command_timeout(None); - } - - #[test] - #[cfg(unix)] - fn test_cmd_timeout_cannot_widen_the_thread_local_budget() { - // `wt list` gives each collect task a budget via the thread-local, and a - // command inside one can carry a longer `.timeout()` of its own (the - // 10s bound on remote default-branch detection). The tighter budget has - // to win, or one command spends the whole task's allowance. - set_command_timeout(Some(Duration::from_millis(50))); - - let start = std::time::Instant::now(); - let result = Cmd::new("sleep") - .arg("10") - .timeout(Duration::from_secs(30)) - .run(); - let elapsed = start.elapsed(); - - set_command_timeout(None); - - assert_eq!(result.unwrap_err().kind(), std::io::ErrorKind::TimedOut); - assert!( - elapsed < Duration::from_secs(5), - "the explicit timeout widened the thread-local budget: {elapsed:?}" - ); - } - // ======================================================================== // Cmd::stream() tests // ======================================================================== diff --git a/tests/integration_tests/list_config.rs b/tests/integration_tests/list_config.rs index a00cfa184b..cd190ad390 100644 --- a/tests/integration_tests/list_config.rs +++ b/tests/integration_tests/list_config.rs @@ -287,59 +287,6 @@ url = "http://localhost:8080/{{ branch }}" assert_eq!(url, "http://localhost:8080/main"); } -/// Test that task-timeout-ms config option is parsed correctly. -/// We use a very short timeout (1ms) to trigger timeouts. -#[rstest] -fn test_list_config_timeout_triggers_timeouts(repo: TestRepo) { - fs::write( - repo.test_config_path(), - r#"[list] -task-timeout-ms = 1 -"#, - ) - .unwrap(); - - let mut cmd = wt_command(); - repo.configure_wt_cmd(&mut cmd); - cmd.arg("list").current_dir(repo.root_path()); - - let output = cmd.output().unwrap(); - let stderr = String::from_utf8_lossy(&output.stderr); - - // With a 1ms timeout, some tasks should time out - // The footer should show the timeout count - assert!( - stderr.contains("timed out") || output.status.success(), - "Expected either timeout message in footer or success (if git was fast enough)" - ); -} - -/// Test that task-timeout-ms = 0 explicitly disables timeout. -#[rstest] -fn test_list_config_timeout_zero_means_no_timeout(repo: TestRepo) { - fs::write( - repo.test_config_path(), - r#"[list] -task-timeout-ms = 0 -"#, - ) - .unwrap(); - - let mut cmd = wt_command(); - repo.configure_wt_cmd(&mut cmd); - cmd.arg("list").current_dir(repo.root_path()); - - let output = cmd.output().unwrap(); - let stderr = String::from_utf8_lossy(&output.stderr); - - // With task-timeout-ms = 0, there should be no timeout - assert!( - !stderr.contains("timed out"), - "Expected no timeout message with task-timeout-ms = 0, but got: {}", - stderr - ); -} - /// Regression: setting a typed env-var override (e.g. `WORKTRUNK__LIST__TIMEOUT_MS`) /// must not wipe unrelated fields in the same section. /// @@ -710,33 +657,6 @@ fn test_list_config_malformed_system_config_non_section_field(repo: TestRepo) { }); } -/// Test that --full disables the task timeout. -#[rstest] -fn test_list_config_timeout_disabled_with_full(repo: TestRepo) { - fs::write( - repo.test_config_path(), - r#"[list] -task-timeout-ms = 1 -"#, - ) - .unwrap(); - - let mut cmd = wt_command(); - repo.configure_wt_cmd(&mut cmd); - cmd.args(["list", "--full"]).current_dir(repo.root_path()); - - let output = cmd.output().unwrap(); - let stderr = String::from_utf8_lossy(&output.stderr); - - // With --full, the timeout is disabled so we shouldn't see timeout messages - // (though tasks may still fail for other reasons) - assert!( - !stderr.contains("timed out"), - "Expected no timeout message with --full flag, but got: {}", - stderr - ); -} - #[rstest] fn test_list_custom_columns(repo: TestRepo) { // A vars-backed column (only feature-a has the key; other rows render diff --git a/tests/snapshots/integration__integration_tests__help__help_config_create.snap b/tests/snapshots/integration__integration_tests__help__help_config_create.snap index 258786968a..f2d749fb92 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_create.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_create.snap @@ -9,6 +9,7 @@ info: env: CLICOLOR_FORCE: "1" COLUMNS: "500" + GIT_ALLOW_PROTOCOL: file LANG: C LC_ALL: C LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]" @@ -170,8 +171,7 @@ Creates ~/.config/worktrunk/config.toml with the following content:   #   # columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set)   # -  # task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables -  # timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +  # timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables   #   # `columns` selects and orders the columns to render; omit it for the default set.   # It is meant to drive a per-invocation alias (https://worktrunk.dev/extending/#aliases) diff --git a/tests/snapshots/integration__integration_tests__help__help_config_long.snap b/tests/snapshots/integration__integration_tests__help__help_config_long.snap index 5bd6917840..6ac5b87eee 100644 --- a/tests/snapshots/integration__integration_tests__help__help_config_long.snap +++ b/tests/snapshots/integration__integration_tests__help__help_config_long.snap @@ -8,6 +8,7 @@ info: env: CLICOLOR_FORCE: "1" COLUMNS: "500" + GIT_ALLOW_PROTOCOL: file LANG: C LC_ALL: C LLVM_PROFILE_FILE: "[LLVM_PROFILE_FILE]" @@ -218,8 +219,7 @@ Persistent flag values for wt list. Override on command line as needed.     columns = ["branch", "status", "ci", "path"] # Columns to show, in order — built-ins or custom headers (omit for the default set)   -  task-timeout-ms = 0 # Kill individual git commands after N ms; 0 disables -  timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables +  timeout-ms = 0 # Wall-clock budget for the entire collect phase; 0 disables columns selects and orders the columns to render; omit it for the default set. It is meant to drive a per-invocation alias