Skip to content

refactor(api): typed TmuxError for terminal_tmux (#3576)#6237

Merged
houko merged 2 commits into
mainfrom
refactor/3576-terminal-tmux-error
Jun 19, 2026
Merged

refactor(api): typed TmuxError for terminal_tmux (#3576)#6237
houko merged 2 commits into
mainfrom
refactor/3576-terminal-tmux-error

Conversation

@houko

@houko houko commented Jun 19, 2026

Copy link
Copy Markdown
Contributor

Summary

Scoped slice toward #3576 ("inconsistent error contracts — String / anyhow / LibreFangError / LlmError mixed").
Migrates crates/librefang-api/src/terminal_tmux.rs off anyhow::Result / anyhow::anyhow! to a thiserror-derived TmuxError enum, following the typed-error pattern established by the #3711 series (tool_runner ToolError).

This is a partial — it does NOT close #3576.
The bulk of that issue is already covered by the ongoing #3711 migration:

  • tool_runner already uses a typed ToolError (crates/librefang-runtime/src/tool_runner/error.rs).
  • anyhow in librefang-kernel is already at zero; in librefang-runtime it survives only in code comments.
  • KernelError (crates/librefang-kernel/src/error.rs) now has structured variants (Hand, WasmSandbox, Python, …).
    The issue's "drop KernelError" suggestion is intentionally NOT actioned: 21 Error enums collapse to String at trait boundaries — restore structured kernel/runtime errors #3711 added those variants on purpose, so removing them would be a regression.

The one genuine remaining lib-crate anyhow concentration was terminal_tmux.rs (~33 anyhow! call sites across the tmux helper methods and the free parse_window_list), which this PR addresses.

TmuxError variants

#[derive(Debug, thiserror::Error)], #[non_exhaustive]:

  • Spawn { cmd: &'static str, source: std::io::Error }Command::spawn() failed; the spawn io::Error is preserved via #[source].
    cmd distinguishes "tmux" / "tmux has-session" / "tmux new-session".
  • Timeout { what: &'static str } — the subprocess exceeded the hard timeout ("tmux command" / "tmux has-session" / "tmux new-session").
  • Io { source: std::io::Error } — post-spawn wait/output collection failed; source preserved.
  • NonZeroExit { status: String, stderr: String } — tmux ran but exited non-zero, carrying the rendered status and trimmed stderr.
  • InvalidArgument(String) — a window id/name failed validation before any subprocess spawned; preserves the regex-pattern detail.
  • NoOutputnew-window -P succeeded but produced no parseable line.
  • MissingField { field: &'static str, line: String } — a list-windows -F line was missing the named column.
  • InvalidIndex { value: String } — the window_index token did not parse as u32.

Every Display string is preserved byte-for-byte from the old anyhow!(…) text because it reaches daemon logs via warn!(error = %e, …).
The spawn io::Error and post-spawn io::Error are kept on the source() chain via #[source].

Callers touched

All callers consume the error purely through Display (warn!(error = %e, …) / tracing::debug!("…: {e}")) and produce their own HTTP responses — none inspect the error type or walk source(), so they compile unchanged with equivalent behavior.
The route handler keeps its existing status mapping (SERVICE_UNAVAILABLE for ensure_session, INTERNAL_SERVER_ERROR for list/new, dedicated {"error": …} JSON for delete/rename); no response shape changed.

  • crates/librefang-api/src/routes/terminal.rs — HTTP boundary (list / create / delete / rename window, websocket attach + window-switch paths).
  • crates/librefang-api/src/server.rs — daemon-shutdown kill_session cleanup.
  • crates/librefang-api/src/lib.rspub mod terminal_tmux; (module declaration; no code change needed).

routes/memory.rs's intentional From<anyhow::Error> compat shim is out of scope and left untouched; no other crate is modified.

Tests

The parse_window_list unit tests are kept passing.
The two that previously asserted only .is_err() now assert the typed variant AND the byte-for-byte Display text:

  • parse_malformed_line_returns_errorTmuxError::MissingField { field: "window_active", .. }, Display missing window_active in tmux output: "@1|0|editor".
  • parse_bad_index_returns_errorTmuxError::InvalidIndex { .. }, Display invalid window index "abc".

Verification

Run in the repo's sanctioned Docker dev image (Dockerfile.rust-dev), scoped to librefang-api:

  • cargo check -p librefang-api --lib — clean (Finished in 1m34s, no warnings).
  • cargo clippy -p librefang-api --all-targets -- -D warnings — clean (Finished, no warnings).
  • cargo test -p librefang-api --lib terminal_tmux24 passed; 0 failed.
  • cargo fmt --check -p librefang-api — clean (exit 0).

Cargo.lock regenerated: one line adds the thiserror 2.0.18 dependency edge for librefang-api (already in the workspace dependency graph; no new transitive crates).

Refs #3576.

Migrate `crates/librefang-api/src/terminal_tmux.rs` off `anyhow::Result` / `anyhow::anyhow!` to a `thiserror`-derived `TmuxError` enum, following the #3711 typed-error pattern.

The new enum captures the real failure kinds from the previous call sites: process spawn failure (carrying the underlying `io::Error` on the `source()` chain), subprocess timeout, post-spawn I/O error, non-zero exit (carrying the rendered status and trimmed stderr), pre-spawn argument validation rejection, empty `new-window` output, and the `parse_window_list` field/index parse failures.

Every `Display` string is preserved byte-for-byte from the old `anyhow!(…)` text because it reaches daemon logs via `warn!(error = %e, …)` in `routes/terminal.rs`.
Callers consume the error purely through `Display` and produce their own HTTP responses, so the route handlers, `server.rs` shutdown cleanup, and the websocket attach path compile unchanged with equivalent behavior.

Scoped slice toward #3576 — the bulk of that issue is already covered by the #3711 series (`tool_runner` `ToolError`, kernel `anyhow` count at zero, structured `KernelError`).
The issue's "drop KernelError" suggestion is intentionally NOT actioned: #3711 added those structured variants on purpose, so removing them would be a regression.

Refs #3576.
@github-actions github-actions Bot added area/docs Documentation and guides size/M 50-249 lines changed labels Jun 19, 2026
@houko
houko merged commit 97c74c7 into main Jun 19, 2026
33 checks passed
@houko
houko deleted the refactor/3576-terminal-tmux-error branch June 19, 2026 16:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/docs Documentation and guides size/M 50-249 lines changed

Projects

None yet

Development

Successfully merging this pull request may close these issues.

arch: inconsistent error contracts — String / anyhow / LibreFangError / LlmError mixed

2 participants