Skip to content

Review residues from #247: read the base answer arm by arm, hold the default branch as one value - #257

Merged
blooop merged 3 commits into
mainfrom
fix/245-review-residues
Aug 18, 2026
Merged

blooop merged 3 commits into
mainfrom
fix/245-review-residues

Conversation

@blooop

@blooop blooop commented Aug 18, 2026 •

Copy link
Copy Markdown
Owner

The two substantive non-blocking findings from #247's two-axis review, applied, plus the findings from a fresh-eyes review of this PR itself.

1. prepare_cold consumed BranchBase with a bare isinstance(base, StaleBase), so a third arm added later would have been read as a fresh base — launching from a stale cache in silence, the exact defect the sum was added to close. repo_manager states this rule for its own sum and exports unhandled_fetch_outcome for it; BranchBase now has unhandled_branch_base beside it, the dispatch names both arms, and the refusal is pinned. Red first: the bogus arm sailed straight through before the change.

2. ensure_branch held the default branch as two locals — a name and an error string — reconciled downstream by default_branch_error or "no default branch is recorded", with nothing preventing them from disagreeing and falsiness read twice for the empty-name case. They travel as one sum now (_DefaultBranchNamed | _DefaultBranchUnknown), resolved in one place that folds an empty answer into the same arm a raise takes, with the start point read through one total function instead of an or at each site. The base-branch fetch moved to _fetch_base_branch with it — its dispatch was nested three levels inside the requested ref's own. This one is a refactor, so it is pinned rather than red-first.

Behavior parity was verified empirically, not just argued. All 13 combinations of (requested-ref outcome × default-branch resolution) were enumerated against origin/main and against this branch, comparing the start_point passed to ensure_branch_exists, the returned arm, the fetch_ref count, and the exact set of warnings. Output is identical except for one intentional difference, below.

The one intentional behavior difference. The old str(e) or "no default branch is recorded" fallback survived the refactor for an empty default-branch name but was dropped for an empty exception message — caught by the review of this PR. OSError() with no args would have rendered as could not be refreshed (), a sentence with its reason cut out. Rather than restore the old string, the exception's type is now the fallback (OSError with no message): the old text claimed no default branch was recorded, which is not what happened when the resolver raised. Unreachable in practice today — the real get_default_branch never raises on its own — and pinned by a test either way.

Also from the review of this PR: _unhandled_default_branch joins its four private siblings in coverage's exclude_also, since ty rejects any reachable call and a build that type-checks cannot execute it. The public unhandled_branch_base keeps its test instead — the house splits exactly this way, with unhandled_fetch_outcome and unhandled_unsaved both pinned by tests and only the _-private sentinels excluded. Two dead patches came off that test, and the empty-name arm now asserts the start point it cuts from.

Three findings from #247 deliberately not actioned, with reasons:

  • Layered warnings repeat the reason (nit). An offline launch prints the mechanical failure and then the consequence, sharing one reason string. The two lines say different things, the second is self-contained, and the first is pre-existing Fetch exactly the branch being launched, and nothing else #185 behavior with its own pin — removing it would take a shipped guarantee away from a direct caller of ensure_branch.
  • Plumb the BranchBase above prepare_cold — no programmatic consumer exists today, and ensure_branch: the three arms that cut a branch from an unfetched cache report it to the caller #245 framed wf's channel as dl's output. Widening the return shape for a hypothetical reader would trade this finding for Speculative Generality.
  • "Settle the start point in (2)/(3)" answered by pinning rather than behavior change — the Spec axis concluded this matches the Done-when list.

Full suite: 1786 unit + 48 integration green, pylint 10/10.

🤖 Generated with Claude Code

Summary by Sourcery

Make branch-base and default-branch handling exhaustive and consistent while preserving existing launch behavior.

Bug Fixes:

  • Reject unrecognized branch-base variants instead of silently treating them as fresh bases during cold workspace preparation.
  • Preserve a meaningful fallback reason when default-branch resolution raises an exception with no message.

Enhancements:

  • Represent default-branch resolution as a single named-or-unknown result and centralize default-base fetching and start-point selection.
  • Treat empty default-branch names consistently as unknown branches that use HEAD without an additional fetch.

Tests:

  • Add coverage for empty default-branch names, message-less resolver failures, and rejection of unhandled branch-base values.

blooop added 2 commits August 18, 2026 17:06
prepare_cold consumed BranchBase with a bare isinstance(base, StaleBase),
so a third arm would have been read as a fresh base -- launching from a
stale cache in silence, the exact defect the sum was added to close. Adds
unhandled_branch_base beside the module's siblings and pins the refusal
at the surface that reports staleness.
ensure_branch held it as two locals -- a name and an error string --
reconciled downstream by 'error or "no default branch is recorded"',
with nothing stopping them disagreeing and falsiness read twice for the
empty-name case. They travel as one sum now, resolved in one place that
folds an empty answer into the same arm a raise takes, and the start
point is read through one total function rather than an 'or' at each site.

The base-branch fetch moves to _fetch_base_branch with it: the dispatch
was three levels deep inside the requested ref's own. No behavior change
-- the empty-name arm gained the pin it never had, and it passed before
this refactor as well as after.

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry @blooop, you have reached your weekly rate limit of 500000 diff characters.

Please try again later or upgrade to continue using Sourcery

@sourcery-ai

sourcery-ai Bot commented Aug 18, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors default-branch handling into explicit sum types and helper methods and makes BranchBase handling exhaustive, to prevent silent stale-cache launches while keeping behavior unchanged.

File-Level Changes

Change Details Files
Make BranchBase handling in prepare_cold exhaustive to guard against future sum extensions.
  • Replaced single isinstance(base, StaleBase) check with explicit FreshBase/StaleBase branches
  • Added else arm that calls unhandled_branch_base for unexpected BranchBase variants
  • Introduced unhandled_branch_base helper that raises AssertionError for unhandled bases
  • Added a regression test ensuring unexpected base answers cause a crash rather than silent success
devlaunch/worktree/workspace_clone.py
test/test_workspace_clone.py
Refactor default-branch resolution and usage into explicit sum types and helpers to keep the default branch as a single coherent value.
  • Introduced _DefaultBranchNamed and _DefaultBranchUnknown dataclasses and their union type _DefaultBranch
  • Added _unhandled_default_branch and _start_point helpers to centralize default-branch dispatch and fallback logic
  • Refactored ensure_branch to use _resolve_default_branch and _fetch_base_branch, removing separate default_branch/default_branch_error locals and nested dispatch
  • Changed workspace creation start_point to use _start_point(default) instead of default_branch or 'HEAD'
  • Updated logging and error reporting so empty default-branch names are treated as 'no default branch is recorded' with a single source of truth
devlaunch/worktree/workspace_clone.py
Add tests to pin behavior for empty default branch names and unhandled base answers.
  • Added test to assert that an empty default-branch name reports HEAD as the stale base with a specific reason string
  • Added test to assert prepare_cold crashes with AssertionError when ensure_branch returns an unrecognized base answer
test/test_workspace_clone.py

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@codecov

codecov Bot commented Aug 18, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 93.26%. Comparing base (4ad035e) to head (8bc40eb).
⚠️ Report is 4 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #257      +/-   ##
==========================================
+ Coverage   93.22%   93.26%   +0.03%     
==========================================
  Files          25       25              
  Lines        3750     3771      +21     
==========================================
+ Hits         3496     3517      +21     
  Misses        254      254              
Files with missing lines Coverage Δ
devlaunch/worktree/workspace_clone.py 97.22% <100.00%> (+0.25%) ⬆️

Impacted file tree graph

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

A fresh-eyes review found one behavior drift in the refactor: the old
'error or sentence' fallback survived for an empty default-branch *name*
but was dropped for an empty exception *message*, so an exception whose
str() is empty would have printed 'could not be refreshed ()' -- a
sentence with its reason cut out. Reported by exception type instead,
which is the one intentional difference from what shipped: the old
fallback said 'no default branch is recorded', which is not what happened
when the resolver raised.

Also from the review: _unhandled_default_branch joins its four private
siblings in coverage's exclude_also -- ty rejects any reachable call, so a
build that type-checks cannot execute it. The public unhandled_branch_base
keeps its test instead, as unhandled_fetch_outcome and unhandled_unsaved
already have. Drops two dead patches from that test and asserts the start
point the empty-name arm cuts from.

@blooop blooop left a comment

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was generated by AI during review.

Self-review (fresh-context reviewer + empirical parity check)

Verdict: approve. One real finding was raised and fixed; behavior parity is verified by enumeration rather than by argument.

Behavior drift — found and fixed. The refactor dropped the str(e) or <sentence> fallback for an empty exception message while keeping it for an empty name, so OSError() with no args would have printed could not be refreshed (). Fixed by falling back to the exception's type name; see the PR body for why that text rather than the old sentence. Pinned by test_a_resolver_failure_with_no_message_still_reports_a_reason.

Parity method. All 13 combinations of (requested-ref fetch outcome × default-branch resolution, including both empty-name and empty-message) were run against origin/main and against this branch, comparing the start_point handed to ensure_branch_exists, the returned arm, the fetch_ref call count, and the exact multiset of warnings. Diff is empty apart from the one intentional line above. That covers the reviewer's specific concerns: the except set is unchanged, the ValueError catch still scopes only the base fetch_ref (so an AssertionError from unhandled_fetch_outcome still propagates), and _start_point reproduces default_branch or "HEAD" exactly because _DefaultBranchNamed.name is non-empty by construction.

Exhaustiveness helpers. Total, and ty agrees — both arms return, so the parameter narrows to Never at the sentinel call.

One reviewer finding not taken, on a factual correction. The review recommended deleting the unhandled_branch_base test and covering the sentinel by coverage exclusion instead, on the premise that "the analogous branch for FetchOutcome has no such test anywhere in the suite." It does — test_workspace_clone.py asserts match="Unhandled fetch outcome" in two places. The house convention splits on visibility: public sentinels (unhandled_fetch_outcome, unhandled_unsaved) are pinned by tests, and only _-private ones (_unhandled_removal/source/status/usage) sit in exclude_also. Applied that split — private one excluded, public one tested — and took the valid sub-points: the two dead patches are gone and the test says why it patches the producer (ensure_branch's dispatch is total, so no input yields a fourth arm).

Remaining nits accepted as-is: the new test overlaps its neighbour (now differentiated by asserting the start point), the arm is mock-only because the real resolver falls back to "main", and the comment density is heavy but matches this file.

@blooop
blooop merged commit 2bd67e2 into main Aug 18, 2026
9 checks passed
@blooop
blooop deleted the fix/245-review-residues branch August 18, 2026 16:23
@blooop blooop mentioned this pull request Aug 18, 2026
blooop added a commit that referenced this pull request Aug 18, 2026
Ships two merges that landed after 0.0.28 was cut.

#258: the claude-code feature mounts ~/.claude/skills and
~/.claude/wf-skills read-only. The allow-list had neither, so a
workspace this repo's devcontainer built had no ~/.claude/skills at all
and every skill installed on the host was invisible inside — a wf launch
into a node landing in a session that could not see the /wf skill it was
launched to run. Repo content rather than package content, so the fix
arrives with a checkout of main and a container recreate, not with this
version bump; the release is here so main and the published package
agree.

#257: prepare_cold names both arms of the base answer and refuses an
unhandled one, closing the silent launch-from-stale-cache a third arm
would have reintroduced behind #245's report.

Goes out through Auto-publish → Conda Publish, the same way 0.0.28 did:
no tag or GitHub release is created by hand.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant