refactor: technical-debt cleanup from first-principles review#33
Merged
refactor: technical-debt cleanup from first-principles review#33
Conversation
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.
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.
truncate_linegated 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_millispanicked on pre-epoch clock skew; now returnsOption<u64>with expiry predicates defaulting to "refresh" andwrite_refreshed_credentialspropagating aResult.truncate_titlewithmax_len < 4produced<first-char>...— guarded withdebug_assert.tui::app::dispatch_user_actionsilently droppedUserActions when the channel was closed (agent task dead); now pushes an error block and tears down the TUI.PendingCallsmoved fromtui::toagent::— it coordinatesAgentEvent::ToolCallStart/Endpairing, so it lives with the contract, not the first consumer. UnblocksStdioSinkadopting the same result-header fallback.Retry-After) / 529 / generic 5xx now get actionable prefixes instead of one identical string for every failure, raw body preserved asdetails: {body}.ContextEditenum-of-one collapsed to a tagged struct.model.to_lowercase()allocations replaced witheq_ignore_ascii_casesplits in hot paths. System-section assembly list promoted from a numbered//comment to///rustdoc. DeadStreamEvent::Unknownmatch arm removed from amatches!assertion.bytes_to_mbextracted — three duplicate#[expect(clippy::cast_precision_loss)]suppressions collapse to one.normalize_eolreturnsCow<'_, str>, skipping aStringallocation on pure-LF input (the common case).tool/bashtest bounds tightened — theTRUNCATION_OVERHEADslop 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 previousstarts_with("aaaa")/ends_with('b')pattern missed.pub→pub(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.tui::wraphad three tests asserting onlyresult.len() >= 2— replaced with exact-count + per-line content assertions that pin where the word-boundary breaks land.tui::components::statushad fivecontains()substring tests duplicating existinginstasnapshots at weaker strength — duplicates dropped, snapshots retained.session::writersubstring match on raw JSONL replaced with anEntry::Messageparse + exactContentBlock::Textcomparison.CLAUDE.mdcrate tree refreshed (addsagent/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..gitignoreexcludes.claude/worktrees/.Out of scope
Follow-up PRs for items that either need their own scope or are speculative until a concrete consumer lands:
agent.rs:205). Clean fix plumbs parse errors throughstream_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>insanitize_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.SessionManagerstd::sync::Mutex swap.record_messageis async because of file I/O; the fix is a sync /spawn_blockingrestructure, aligned with the write-batching track indocs/research/session-persistence.md.client/anthropic.rssplit (2761 LOC → wire / sse / betas / completion). Do before MCP client lands so MCP streaming doesn't force a second 3000-line file.session/chainextraction + sanitize-passes split.resolve_chain(70% graph walking) moves to its own module;sanitize_resumed_messagesdecomposes into four testable passes. Groundwork for context compression.ToolResultViewrenderer split. Before the next two variants (grep matches, glob list) land.AgentEvent::PromptRequestand widened tool content. Speculative until the first interactive feature / MCP result has a concrete shape to design around — "no speculative code" applies.Changes
tool.rs,config/oauth.rs,session/manager.rs,tui/app.rstruncate_linechar-cap fix + regression test;now_millis→Option<u64>;truncate_titleprecondition;dispatch_user_actionhandlesClosedagent/pending_calls.rs(moved fromtui/),tui.rs,agent.rs,tui/app.rs,tui/components/chat.rs,agent/event.rsPendingCallslives with its contractclient/anthropic.rsRetry-After;ContextEditstruct;eq_ignore_ascii_casefor family checks; promoted rustdoc; deadUnknownarm dropped; visibility narrowedtool.rs,tool/{read,edit,grep,bash}.rsbytes_to_mbhelper;normalize_eolreturnsCow;TRUNCATION_OVERHEADpromoted; multibyte tests tightenedsession/{manager,writer,history,list_view,store}.rswritertest parsesEntry::Message; test renamesconfig.rs,config/{file,oauth}.rs,client.rs,client/anthropic.rs,message.rspub→pub(crate)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.rstui/wrap.rs,tui/components/status.rsagent.rs,tool/{write,read,grep,glob}.rs,config/oauth.rs,session/list_view.rs,tui/components/input.rsCLAUDE.md.gitignore.claude/worktrees/excludedTest plan
cargo fmt --all --checkcargo buildcargo clippy --all-targets -- -D warnings— zero warningscargo test— 936 pass (five duplicate status-barcontains()tests dropped vs the 941 before this branch)pnpm run lint,pnpm run spellcheck— zero issues