Skip to content

refactor: technical-debt cleanup from first-principles review#33

Merged
hakula139 merged 24 commits intomainfrom
refactor/cleanup-technical-debt
Apr 24, 2026
Merged

refactor: technical-debt cleanup from first-principles review#33
hakula139 merged 24 commits intomainfrom
refactor/cleanup-technical-debt

Conversation

@hakula139
Copy link
Copy Markdown
Owner

@hakula139 hakula139 commented Apr 24, 2026

Summary

A perfectionist-review pass on the crate, executed as 23 atomic commits on a single branch. Findings came from fanning out five review agents (architecture, Rust idioms, style / conventions, tests, usability) across the codebase; fixes were split into this branch (immediate wins) and the deferred follow-ups listed below (items that belong in their own PR).

Zero behavior regressions on the happy path; four of the commits are real correctness fixes for paths tests did not previously exercise.

  • Correctness bugs (4 commits). truncate_line gated on bytes when the cap is character-counted, so pure-multibyte input (500 é's) silently returned "... [N chars]" with an empty prefix — data loss; added a regression test. now_millis panicked on pre-epoch clock skew; now returns Option<u64> with expiry predicates defaulting to "refresh" and write_refreshed_credentials propagating a Result. truncate_title with max_len < 4 produced <first-char>... — guarded with debug_assert. tui::app::dispatch_user_action silently dropped UserActions when the channel was closed (agent task dead); now pushes an error block and tears down the TUI.
  • Architecture refinement (1 commit). PendingCalls moved from tui:: to agent:: — it coordinates AgentEvent::ToolCallStart/End pairing, so it lives with the contract, not the first consumer. Unblocks StdioSink adopting the same result-header fallback.
  • Client hardening (5 commits). Status-aware API error messages — 401 / 429 (with Retry-After) / 529 / generic 5xx now get actionable prefixes instead of one identical string for every failure, raw body preserved as details: {body}. ContextEdit enum-of-one collapsed to a tagged struct. model.to_lowercase() allocations replaced with eq_ignore_ascii_case splits in hot paths. System-section assembly list promoted from a numbered // comment to /// rustdoc. Dead StreamEvent::Unknown match arm removed from a matches! assertion.
  • Tool module cleanup (3 commits). bytes_to_mb extracted — three duplicate #[expect(clippy::cast_precision_loss)] suppressions collapse to one. normalize_eol returns Cow<'_, str>, skipping a String allocation on pure-LF input (the common case). tool/bash test bounds tightened — the TRUNCATION_OVERHEAD slop became a named constant the assertion actually references; multibyte-boundary tests now assert the emoji is absent from the truncated head and tail, catching a partial-byte leak the previous starts_with("aaaa") / ends_with('b') pattern missed.
  • Style sweeps (3 commits). pubpub(crate) across the binary-only crate (27 items across config, client, message). Mechanical test rename: *_returns_none, *_returns_err, *_returns_owned_value → scenario form (*_is_absent, *_errors, *_reads_value, etc.) across 15 files. Missing blank lines before section dividers fixed in 8 test modules.
  • Test quality (3 commits). tui::wrap had three tests asserting only result.len() >= 2 — replaced with exact-count + per-line content assertions that pin where the word-boundary breaks land. tui::components::status had five contains() substring tests duplicating existing insta snapshots at weaker strength — duplicates dropped, snapshots retained. session::writer substring match on raw JSONL replaced with an Entry::Message parse + exact ContentBlock::Text comparison.
  • Docs + infra (4 commits). CLAUDE.md crate tree refreshed (adds agent/pending_calls.rs) and four ambiguous rules clarified: const-group blank-line exception, super:: / crate:: grouped together in imports, top-down ordering is within-section, scenario-vs-mechanism test naming gets a synonym-case clause. Added a PR-convention rule: descriptions must stand on their own and not point at gitignored working docs. .gitignore excludes .claude/worktrees/.

Out of scope

Follow-up PRs for items that either need their own scope or are speculative until a concrete consumer lands:

  • Tool-input JSON parse-error surfacing (agent.rs:205). Clean fix plumbs parse errors through stream_response's return type — signature change with cascading test updates. The current behavior (empty input → tool schema error → model self-corrects) works; the concern is diagnostic quality. ~40 LOC follow-up.
  • HashSet<&str> in sanitize_resumed_messages. Borrow conflicts with the mutable-iteration path in the current &mut Vec<Message> signature; best tackled after the sanitize-passes extraction splits the function.
  • SessionManager std::sync::Mutex swap. record_message is async because of file I/O; the fix is a sync / spawn_blocking restructure, aligned with the write-batching track in docs/research/session-persistence.md.
  • client/anthropic.rs split (2761 LOC → wire / sse / betas / completion). Do before MCP client lands so MCP streaming doesn't force a second 3000-line file.
  • session/chain extraction + sanitize-passes split. resolve_chain (70% graph walking) moves to its own module; sanitize_resumed_messages decomposes into four testable passes. Groundwork for context compression.
  • Per-tool ToolResultView renderer split. Before the next two variants (grep matches, glob list) land.
  • AgentEvent::PromptRequest and widened tool content. Speculative until the first interactive feature / MCP result has a concrete shape to design around — "no speculative code" applies.

Changes

Area Files Notes
Correctness tool.rs, config/oauth.rs, session/manager.rs, tui/app.rs truncate_line char-cap fix + regression test; now_millisOption<u64>; truncate_title precondition; dispatch_user_action handles Closed
Architecture agent/pending_calls.rs (moved from tui/), tui.rs, agent.rs, tui/app.rs, tui/components/chat.rs, agent/event.rs PendingCalls lives with its contract
HTTP client client/anthropic.rs Status-aware errors + Retry-After; ContextEdit struct; eq_ignore_ascii_case for family checks; promoted rustdoc; dead Unknown arm dropped; visibility narrowed
Tool modules tool.rs, tool/{read,edit,grep,bash}.rs bytes_to_mb helper; normalize_eol returns Cow; TRUNCATION_OVERHEAD promoted; multibyte tests tightened
Session session/{manager,writer,history,list_view,store}.rs Title precondition; writer test parses Entry::Message; test renames
Style sweep config.rs, config/{file,oauth}.rs, client.rs, client/anthropic.rs, message.rs pubpub(crate)
Test renames util/{env,lock,path}.rs, config/{file,oauth}.rs, prompt*.rs, model.rs, session/{history,list_view,store}.rs, tui/markdown/{highlight,render}.rs, tui/components/{chat,input}.rs, agent/pending_calls.rs Scenario form instead of mechanism form
Test tightening tui/wrap.rs, tui/components/status.rs Exact-count assertions; substring-contains duplicates removed
Blank lines agent.rs, tool/{write,read,grep,glob}.rs, config/oauth.rs, session/list_view.rs, tui/components/input.rs One blank before first section divider in test modules
Docs CLAUDE.md Crate tree + four clarified rules + PR-description rule
Infra .gitignore .claude/worktrees/ excluded

Test plan

  • cargo fmt --all --check
  • cargo build
  • cargo clippy --all-targets -- -D warnings — zero warnings
  • cargo test — 936 pass (five duplicate status-bar contains() tests dropped vs the 941 before this branch)
  • pnpm run lint, pnpm run spellcheck — zero issues

The fast-path guard compared byte length against the character cap,
so multi-byte input with <= MAX_LINE_LENGTH chars but > MAX_LINE_LENGTH
bytes (e.g., 500 é's = 1000 bytes) entered the truncation path. Inside
the loop, `i == MAX_LINE_LENGTH` never fired because the iterator only
produced MAX_LINE_LENGTH items, leaving `boundary = 0` and returning
`"... [N chars]"` with an empty prefix — silent data loss.

Gate on the actual char count and use `nth()` for the boundary so the
fix reads as one operation rather than a hand-rolled enumerate loop.
Regression test pins the pure-multibyte case at the cap.
Two bare `expect`s would panic if the host clock was set before UNIX
epoch or the timestamp overflowed `u64`. The overflow is impossible for
~584 million years; the pre-epoch case is rare but real (container
misconfig, first-boot VMs without NTP). A clock-skew panic at launch is
a poor UX for a scenario we can handle gracefully.

`now_millis` now returns `Option<u64>`. Expiry predicates treat a broken
clock as "expired / near expiry" so callers force a refresh and let the
server adjudicate. `write_refreshed_credentials` propagates the
condition as a `Result` since it must record an absolute timestamp.
Also use `saturating_add` / `saturating_mul` for the expires_in math to
close the pre-existing overflow on malicious server input.
`max_len < 4` caused `saturating_sub(3)` to return 0, producing a nonsense
"<first-char>..." truncation. Only internal callers hit this helper with
the `MAX_TITLE_LEN = 80` constant, so a debug assertion is enough — the
condition is a programmer invariant, not runtime input.
`user_tx.try_send` was ignoring both Full and Closed errors via `_ = ...`.
Closed means the agent task has died; dropping the action silently left
the TUI stuck on "Streaming" with no way to recover. Now push an error
block, disable input, and set should_quit so the TUI exits cleanly on
the next iteration. Full is implausible (input is disabled while
streaming, so at most one in-flight action) but surface it symmetrically
if it ever trips.
PendingCalls is correlation state between AgentEvent::ToolCallStart and
ToolCallEnd — a contract over the agent event stream, consumed by both
the live TUI path and the transcript-resume walk. Per CLAUDE.md's
"a type used by N callers belongs in the module that names the contract"
rule, it belongs in `agent::` (the event contract), not `tui::` (one
of the consumers). Opens the door for StdioSink to adopt the same
result-header fallback as the TUI without pulling a TUI module.
`.claude/worktrees/` is used by subagent worktree isolation — transient
scratch repos, never checked in.
…, client)

oxide-code has no library target — every bare `pub` is dead surface area
making it harder to tell what's the crate's actual API. Sweep to
`pub(crate)` / `pub(super)` for items in config, client, and message.
Sibling sweeps for other modules land in follow-up commits to keep each
diff reviewable.
CLAUDE.md: "Name tests after the scenario they cover, not the return
type." Sweep away `*_returns_none`, `*_returns_err`,
`*_returns_owned_value`, etc. across util, config, session, prompt,
model, tui, and agent modules. Tests touched by subagent-owned files
(client/anthropic, tool/, session/manager, session/writer) land in
those commits.
Complete the binary-crate visibility sweep for client/anthropic.rs —
every externally-published item (StreamEvent, MessageResponse,
ContentBlockInfo, Delta, MessageDeltaBody, Usage, ApiError, Client and
its inherent methods) narrows from `pub` to `pub(crate)`. oxide-code has
no library target, so the distinction between `pub` and `pub(crate)`
was purely aspirational; the crate's real boundary is the binary.
CLAUDE.md: "One blank line before and after section dividers." Eight
offenders had the first `// ── name ──` land immediately after the
`use super::*;` line or a preceding test's closing brace, breaking
the convention. Sweep across agent, tool/{write,read,grep,glob},
config/oauth, session/list_view, and tui/components/input.
wrap.rs had three tests asserting only `result.len() >= 2`, which would
pass for any reasonable wrap output including pathological cases.
Replaced with exact-count + line-content assertions that pin the
word-boundary breaks where they actually land.

status.rs had five `contains()` substring tests that duplicated existing
`insta` snapshots (`render_idle_without_title_leaves_slot_unused`,
`render_streaming_shows_spinner_and_status_label`, etc.) at weaker
strength — snapshots catch reordering and spacing regressions that
`contains()` misses. Dropped the duplicates; kept the `render_top_row`
helper and the handful of tests that assert on relative ordering or
conditional slot absence where a full snapshot would be noisier.
- Crate tree: add `agent/pending_calls.rs`, which PR #moved from `tui/`.
- Blank lines: carve out closely-related one-line `const` groups (OAuth,
  beta-header runs), which the codebase already treats as one unit.
  Call out that the rule applies inside `#[cfg(test)]` modules too.
- Imports: `super::` and `crate::` are one group, not two — match what
  every file in the crate already does.
- Top-down ordering: rule is about local readability within a section,
  not a hard requirement on whole trait impls or unrelated concerns.
- Test naming: when scenario and return value are synonyms (env unset →
  None), prefer the scenario phrasing (`*_is_absent`) over the
  mechanism (`*_returns_none`). Applied in this branch's test rename.
`force_break_on_long_word` hard-coded the expected wrap chunks as
string literals; cspell flagged the alphabet substrings as unknown
words. Derive the expected slices from the input instead — same
assertion strength, no dictionary pollution.
@hakula139 hakula139 self-assigned this Apr 24, 2026
@hakula139 hakula139 added the enhancement New feature or request label Apr 24, 2026
@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 24, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

Review-facing surfaces should stand on their own. Referencing
`.claude/plans/*` or `.claude/agent-memory-local/*` in a PR body points
readers at files they cannot see. Describe deferred follow-ups inline
instead.
…nic arm

Codecov flagged 11 lines in `tui/app.rs::dispatch_user_action`
(Closed / Full error paths) and one in `session/writer.rs`.

- Two new `dispatch_user_action` tests: a Closed test drops the user
  channel before dispatching to prove the teardown path (push_error +
  disable + should_quit), and a Full test fills the 8-slot channel and
  confirms the overflow does NOT set `should_quit` while still surfacing
  the error to the user.
- `record_session_message_writes_through_to_manager` now asserts
  against the JSON wire shape (`json["type"]`, `json["message"]["role"]`,
  content[0]["text"]) instead of `let Entry::Message { .. } = ... else
  panic!(...)`. Same strictness, no unreachable panic arm.
@hakula139 hakula139 merged commit c7a7727 into main Apr 24, 2026
4 checks passed
@hakula139 hakula139 deleted the refactor/cleanup-technical-debt branch April 24, 2026 11:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant