Skip to content

One directory for the guard, the listing and the delete (#174) - #181

Merged
blooop merged 4 commits into
mainfrom
fix/174-one-path-for-the-guard-and-the-delete
Aug 13, 2026
Merged

blooop merged 4 commits into
mainfrom
fix/174-one-path-for-the-guard-and-the-delete

Conversation

@blooop

@blooop blooop commented Aug 13, 2026 •

Copy link
Copy Markdown
Owner

Closes #174.

dl <ws> rm could destroy unsaved work, return 0, and log nothing.

The defect

The guard read the clone directory straight off the record:

return workspace_state.holds_unsaved_work(Path(record.local_path))   # dl.py

The delete used that path only when it was on disk, and otherwise fell back to the derivation:

ws_path = Path(wt_info.local_path) if wt_info.local_path else None
if ws_path is None or not ws_path.exists():
    ws_path = self.get_workspace_path(...)                            # workspace_clone.py

So when the recorded path is stale the two name different directories. The guard asks about the absent one and gets NothingToLose — which is correct, an absent directory holds nothing — and the delete then removes the derived one, which is where the work is.

Reproduced before fixing:

guard verdict on the recorded path : NothingToLose()
guard verdict on the derived path  : WouldLose('1 uncommitted change(s)
                                     (IMPORTANT_UNSAVED.txt) and 1 unpushed commit(s)')
remove_workspace_by_id returned    : True
derived path still exists          : False

dl --ls --json read the record the same way, so path and unsaved could describe a third directory — which matters because wf reap decides from that listing.

The second face: an empty recorded path deletes the working directory

WorktreeInfo.from_dict builds this field with Path(data["local_path"]), and Path("") is Path(".") — truthy, and its exists() is True. So an empty recorded path passed the if wt_info.local_path test and the .exists() test, and shutil.rmtree was handed Path(".").

Executed in a sandbox: it emptied the directory, .git included, and only then failed on os.rmdir("."). The traceback is the only thing the user gets.

Truthiness cannot distinguish this, so a recorded path is now honoured only when it is absolute.

The fix

One resolver, WorkspaceCloneManager.resolve_clone_path, that the guard, the listing and the delete all go through. A guard is only a guard if it inspects what the delete will actually remove.

It returns Optional[Path] rather than raising. get_workspace_path raises ValueError on an unsafe ref, and one hand-edited record must not be able to take down the whole of dl --ls --json — the same harm read_clone's stat guard exists to prevent, one layer out. None is a refusal at every caller: the delete removes nothing, the guard answers CouldNotTell.

Tests

Six new, mutation-verified rather than trusted:

  • reverting the guard to the direct read turns test_a_stale_record_does_not_let_the_delete_past_the_guard red with the bug's own signature — exit 0 and Removed local clone
  • reverting the resolver to the old rule turns test_an_empty_recorded_path_is_not_the_working_directory red

test_the_delete_removes_exactly_what_resolve_named is the binding: without it the two could be made to disagree again by editing remove_workspace_by_id alone and every other test would stay green.

The test double now calls the real method with the mock as self rather than restating its rule — #174 was two places naming one directory and drifting apart, and a double holding a third copy is the same mistake.

One assertion changed rather than preserved

test_a_clone_that_is_simply_gone_is_still_read_rather_than_assumed asserted path was the recorded directory. It is now the one rm would act on, which is the point of the ticket. In production these are the same string — a clone's leaf is its workspace id — and they differ only because the fixture gives the workspace a shorter id than the derivation would.

Verification

1310 tests pass, ruff check clean, ruff format clean, pylint 10.00/10.

🤖 Generated with Claude Code

Summary by Sourcery

Unify workspace clone path resolution so the delete operation, its safety guard, and JSON listing all act on the same directory and avoid unsafe deletions when metadata is stale or malformed.

Bug Fixes:

  • Prevent dl <ws> rm from deleting a derived clone directory while the guard inspects a different, stale recorded path.
  • Ensure an empty local_path no longer resolves to the current working directory and cannot trigger removal of dl’s own workspace.
  • Handle records with missing or unsafe clone metadata by refusing to delete and reporting that the clone directory cannot be determined.

Enhancements:

  • Introduce WorkspaceCloneManager.resolve_clone_path as the single source of truth for mapping worktree records to clone directories and reuse it across guard, delete, and listing paths.

Documentation:

  • Document the guard/delete behavior and clone path resolution changes in CHANGELOG.md, including the new absolute-path requirement for recorded clone directories.

Tests:

  • Add focused tests around resolve_clone_path and remove_workspace_by_id to ensure guards and deletes name the same directory and to cover stale, empty, and missing local_path scenarios.
  • Update workspace state tests to exercise the new resolver in JSON listings and to verify the guard behavior when records are stale or invalid.

`dl <ws> rm` could destroy unsaved work and exit 0.

The guard read `record.local_path` unconditionally. The delete used that path
only when it existed and otherwise fell back to the derivation. So a record
pointing somewhere stale had the two naming different directories: the guard
answered `NothingToLose` about the absent recorded one -- correctly, an absent
directory holds nothing -- and the delete then removed the derived one, which was
where the work was. No `--force`, nothing logged.

Reproduced before fixing, and the reproduction is now the test
`test_a_stale_record_does_not_let_the_delete_past_the_guard`: mutating the guard
back to the direct read turns it red with the bug's own signature, exit 0 and
"Removed local clone".

A second face of the same field, worse. `WorktreeInfo.from_dict` builds
`local_path` with `Path(data["local_path"])`, and `Path("")` is `Path(".")` --
truthy, and its `exists()` is True. An empty recorded path therefore passed the
`if wt_info.local_path` test *and* the `.exists()` test and handed
`shutil.rmtree` dl's own working directory. Executed in a sandbox: it emptied
the directory, `.git` included, and only then failed on `os.rmdir(".")`. A
recorded path is now honoured only when it is absolute, since truthiness does
not distinguish these.

All three readers -- the guard, the `--ls --json` row and the delete -- now go
through `WorkspaceCloneManager.resolve_clone_path`. It returns `Optional[Path]`
rather than raising: `get_workspace_path` raises `ValueError` on an unsafe ref,
and one hand-edited record must not be able to take down the whole listing,
which is the harm `read_clone`'s stat guard exists to prevent one layer out.
`None` is a refusal at every caller -- the delete removes nothing, the guard
answers `CouldNotTell`.

The test double gets the real method called with the mock as `self`, not a copy
of the rule. #174 was two places naming one directory and drifting apart; a
double holding a third copy would be the same mistake, and would keep passing
after the rule changed under it.

One existing assertion changed rather than being preserved:
`test_a_clone_that_is_simply_gone_is_still_read_rather_than_assumed` asserted
`path` was the recorded directory. It is now the one `rm` would act on, which is
the point of the ticket. In production these are the same string -- a clone's
leaf *is* its workspace id -- and they differ here only because the fixture gives
the workspace a shorter id than the derivation would.
@sourcery-ai

sourcery-ai Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

Reviewer's Guide

Unifies clone directory resolution across guard, listing, and delete by introducing WorkspaceCloneManager.resolve_clone_path, tightening safety around recorded paths (including empty/relative ones), and updating tests and documentation to ensure all operations act on the same, correctly derived workspace directory or refuse when it cannot be named.

Sequence diagram for unified guard and delete clone resolution

sequenceDiagram
    actor User
    participant dl as dl
    participant WorkspaceCloneManager as WorkspaceCloneManager
    participant workspace_state as workspace_state

    User->>dl: _unsaved_work_in(workspace_id)
    dl->>WorkspaceCloneManager: storage.get_worktree_by_workspace_id(workspace_id)
    WorkspaceCloneManager-->>dl: WorktreeInfo or None
    alt [record is None]
        dl-->>User: workspace_state.NothingToLose()
    else [record exists]
        dl->>WorkspaceCloneManager: resolve_clone_path(wt_info)
        alt [clone_path is None]
            dl-->>User: workspace_state.CouldNotTell("could not work out which directory ...")
        else [clone_path is Path]
            dl->>workspace_state: holds_unsaved_work(clone_path)
            workspace_state-->>dl: Unsaved verdict
            dl-->>User: Unsaved verdict
        end
    end

    User->>WorkspaceCloneManager: remove_workspace_by_id(workspace_id)
    WorkspaceCloneManager->>WorkspaceCloneManager: storage.get_worktree_by_workspace_id(workspace_id)
    alt [wt_info is None]
        WorkspaceCloneManager-->>User: False
    else [wt_info exists]
        WorkspaceCloneManager->>WorkspaceCloneManager: resolve_clone_path(wt_info)
        alt [ws_path is None]
            WorkspaceCloneManager-->>User: False
        else [ws_path is Path]
            WorkspaceCloneManager->>WorkspaceCloneManager: _remove_clone(ws_path, owner, repo, branch)
            WorkspaceCloneManager-->>User: bool
        end
    end
Loading

File-Level Changes

Change Details Files
Introduce a single clone-path resolver and use it for guard, listing, and delete to ensure they all operate on the same directory and handle unsafe/invalid records safely.
  • Add WorkspaceCloneManager.resolve_clone_path returning Optional[Path], preferring a usable absolute recorded path and otherwise falling back to get_workspace_path, logging and returning None on unsafe refs.
  • Update remove_workspace_by_id to call resolve_clone_path and to bail out (return False) when no safe directory can be determined.
  • Change _unsaved_work_in in dl.py to fetch the clone manager once, resolve the clone path via resolve_clone_path, and return CouldNotTell when the path cannot be resolved.
  • Adjust workspaces_as_json to use resolve_clone_path when a record has a local_path, preserving the measured clone if resolution fails so rows remain internally consistent.
devlaunch/worktree/workspace_clone.py
devlaunch/dl.py
Strengthen and realign tests so they exercise the shared resolver, ensure guard/delete consistency, and cover edge cases around stale, empty, and missing recorded paths.
  • Add TestTheGuardAndTheDeleteNameOneDirectory test class verifying resolver behaviour for stale, empty, missing, and valid recorded paths, plus a binding test that delete removes exactly what resolve_clone_path names.
  • Extend the _clone_manager test helper to provide a realistic get_workspace_path and to delegate resolve_clone_path to the real WorkspaceCloneManager implementation, avoiding duplicated resolution logic in test doubles.
  • Add an integration-style test that a stale record does not let the delete past the guard, asserting exit code, logging, and that the derived path remains untouched.
  • Update an existing listing test to assert that the reported path matches the derived workspace directory (the one rm would act on), not the stale recorded path.
test/test_workspace_clone.py
test/test_workspace_state.py
Document the fix and its safety implications in the changelog, clarifying the previous dangerous behaviour and the new invariant around absolute recorded paths.
  • Add a Fixed entry describing how dl rm could previously destroy unsaved work without --force due to divergent path resolution between guard, delete, and listing.
  • Explain that local_path is now honoured only when absolute, preventing empty/relative paths (e.g., Path("") -> Path(".")) from causing the working directory to be deleted.
  • Note that all three call sites (guard, listing, delete) now go through WorkspaceCloneManager.resolve_clone_path to keep them consistent.
CHANGELOG.md

Assessment against linked issues

Issue Objective Addressed Explanation
#174 Ensure guard, listing, and delete all act on a single, consistently resolved clone directory (one path, decided once). ✅
#174 When both the recorded path and the derived path exist and differ, treat this as "cannot tell which" and refuse (fail closed rather than silently choosing one). ❌ The PR introduces WorkspaceCloneManager.resolve_clone_path and defines that a usable recorded path (absolute and existing) wins over the derived path. In the case where both recorded and derived paths exist and differ, resolve_clone_path returns the recorded path, and both the guard and delete use that directory rather than refusing. This explicitly preserves removability of pre-id-scheme clones (test_a_usable_recorded_path_still_wins), instead of failing closed as the issue requested.
#174 Preserve the behaviour where a workspace whose clone directory has been genuinely removed by hand can still be cleared by dl <ws> rm without --force (no regression of the hand-removed case). ✅

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

@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.

Hey - I've found 2 issues

Prompt for AI Agents
Please address the comments from this code review:

## Individual Comments

### Comment 1
<location path="devlaunch/worktree/workspace_clone.py" line_range="120-123" />
<code_context>
+        of ``dl --ls --json`` -- the same harm :func:`read_clone`'s stat guard
+        exists to prevent, one layer out.
+        """
+        recorded = None if wt_info.local_path is None else Path(wt_info.local_path)
+        if recorded is not None and recorded.is_absolute() and recorded.exists():
+            return recorded
+        try:
</code_context>
<issue_to_address>
**suggestion (bug_risk):** `resolve_clone_path` only checks `exists()` on the recorded path, which might accept non-directory paths.

The code and docs treat the clone location as a directory, but the guard only checks `recorded.exists()`. If the stored path is stale or malformed and points to a regular file, it will still be returned and later code (e.g. `_remove_clone`) may try to operate on it as a directory. Using `recorded.is_dir()` (optionally with `exists()`) would better align with the intended semantics and avoid acting on non-directory paths.

```suggestion
        """
        recorded = None if wt_info.local_path is None else Path(wt_info.local_path)
        if recorded is not None and recorded.is_absolute() and recorded.is_dir():
            return recorded
```
</issue_to_address>

### Comment 2
<location path="test/test_workspace_clone.py" line_range="849-822" />
<code_context>
+        assert resolved == derived
+        assert resolved.is_absolute()
+
+    def test_a_record_with_no_path_at_all_still_resolves(
+        self, clone_manager, mock_repo_manager, tmp_repos_dir
+    ):
+        """`None` used to reach `Path(None)` in the guard, which is a TypeError."""
+        repo_root = tmp_repos_dir / "owner" / "repo"
+        mock_repo_manager.get_repo_path.return_value = repo_root
+        derived = repo_root / leaf("nb4")
+        derived.mkdir(parents=True)
+
+        wt_info = MagicMock()
+        wt_info.owner, wt_info.repo, wt_info.branch = "owner", "repo", "nb4"
+        wt_info.local_path = None
+
+        assert clone_manager.resolve_clone_path(wt_info) == derived
+
+    def test_a_usable_recorded_path_still_wins(
</code_context>
<issue_to_address>
**suggestion (testing):** Consider adding a test for when `resolve_clone_path` must refuse a record and return `None`.

The failure case where `get_workspace_path` raises `ValueError` (unsafe ref) and `resolve_clone_path` returns `None` isn’t currently tested. Please add a unit test that configures `clone_manager.get_workspace_path` to raise `ValueError` for a given `wt_info`, asserts that `resolve_clone_path(wt_info) is None`, and checks that `remove_workspace_by_id` returns `False` without attempting deletion. This will lock in the refusal behaviour and prevent future changes from treating `None` as a valid path.

Suggested implementation:

```python
        old_path = repo_root / "nb4"  # the bare-branch-name leaf
        old_path.mkdir(parents=True)
        assert old_path.name != leaf("nb4")

    def test_unsafe_ref_record_is_refused(
        self, clone_manager, mock_repo_manager, tmp_repos_dir
    ):
        """Unsafe workspace refs must be refused and not treated as valid paths."""
        repo_root = tmp_repos_dir / "owner" / "repo"
        mock_repo_manager.get_repo_path.return_value = repo_root

        wt_info = MagicMock()
        wt_info.workspace_id = "workspace-unsafe"
        wt_info.owner, wt_info.repo, wt_info.branch = "owner", "repo", "nb4"
        wt_info.local_path = None

        # Simulate unsafe ref: resolve_clone_path will internally call get_workspace_path
        # which we configure to fail with ValueError.
        clone_manager.get_workspace_path = MagicMock(side_effect=ValueError("unsafe ref"))

        # The unsafe record must be refused and not treated as a valid path.
        assert clone_manager.resolve_clone_path(wt_info) is None

        # remove_workspace_by_id must also refuse the workspace and not attempt deletion.
        with patch.object(clone_manager, "delete_workspace", autospec=True) as mock_delete:
            assert clone_manager.remove_workspace_by_id(wt_info.workspace_id) is False
            mock_delete.assert_not_called()

```

1. Ensure `patch` is imported at the top of `test/test_workspace_clone.py`, e.g.:
   `from unittest.mock import MagicMock, patch` (or add `patch` to the existing import line).
2. If `clone_manager` uses a different deletion helper than `delete_workspace` inside
   `remove_workspace_by_id`, update the patched attribute in the test to match the actual
   method being called so that `assert_not_called()` accurately verifies that no deletion
   is attempted.
3. If `remove_workspace_by_id` takes a full `wt_info` object instead of `workspace_id`,
   or has a different signature, adjust the call in the test accordingly to match the
   real API while keeping the core assertions (`resolve_clone_path(...) is None` and
   `remove_workspace_by_id(...) is False`).
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +120 to +123
"""
recorded = None if wt_info.local_path is None else Path(wt_info.local_path)
if recorded is not None and recorded.is_absolute() and recorded.exists():
return recorded

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (bug_risk): resolve_clone_path only checks exists() on the recorded path, which might accept non-directory paths.

The code and docs treat the clone location as a directory, but the guard only checks recorded.exists(). If the stored path is stale or malformed and points to a regular file, it will still be returned and later code (e.g. _remove_clone) may try to operate on it as a directory. Using recorded.is_dir() (optionally with exists()) would better align with the intended semantics and avoid acting on non-directory paths.

Suggested change
"""
recorded = None if wt_info.local_path is None else Path(wt_info.local_path)
if recorded is not None and recorded.is_absolute() and recorded.exists():
return recorded
"""
recorded = None if wt_info.local_path is None else Path(wt_info.local_path)
if recorded is not None and recorded.is_absolute() and recorded.is_dir():
return recorded

wt_info.local_path = stale
mock_storage.get_worktree_by_workspace_id.return_value = wt_info

assert clone_manager.resolve_clone_path(wt_info) == derived

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

suggestion (testing): Consider adding a test for when resolve_clone_path must refuse a record and return None.

The failure case where get_workspace_path raises ValueError (unsafe ref) and resolve_clone_path returns None isn’t currently tested. Please add a unit test that configures clone_manager.get_workspace_path to raise ValueError for a given wt_info, asserts that resolve_clone_path(wt_info) is None, and checks that remove_workspace_by_id returns False without attempting deletion. This will lock in the refusal behaviour and prevent future changes from treating None as a valid path.

Suggested implementation:

        old_path = repo_root / "nb4"  # the bare-branch-name leaf
        old_path.mkdir(parents=True)
        assert old_path.name != leaf("nb4")

    def test_unsafe_ref_record_is_refused(
        self, clone_manager, mock_repo_manager, tmp_repos_dir
    ):
        """Unsafe workspace refs must be refused and not treated as valid paths."""
        repo_root = tmp_repos_dir / "owner" / "repo"
        mock_repo_manager.get_repo_path.return_value = repo_root

        wt_info = MagicMock()
        wt_info.workspace_id = "workspace-unsafe"
        wt_info.owner, wt_info.repo, wt_info.branch = "owner", "repo", "nb4"
        wt_info.local_path = None

        # Simulate unsafe ref: resolve_clone_path will internally call get_workspace_path
        # which we configure to fail with ValueError.
        clone_manager.get_workspace_path = MagicMock(side_effect=ValueError("unsafe ref"))

        # The unsafe record must be refused and not treated as a valid path.
        assert clone_manager.resolve_clone_path(wt_info) is None

        # remove_workspace_by_id must also refuse the workspace and not attempt deletion.
        with patch.object(clone_manager, "delete_workspace", autospec=True) as mock_delete:
            assert clone_manager.remove_workspace_by_id(wt_info.workspace_id) is False
            mock_delete.assert_not_called()
  1. Ensure patch is imported at the top of test/test_workspace_clone.py, e.g.:
    from unittest.mock import MagicMock, patch (or add patch to the existing import line).
  2. If clone_manager uses a different deletion helper than delete_workspace inside
    remove_workspace_by_id, update the patched attribute in the test to match the actual
    method being called so that assert_not_called() accurately verifies that no deletion
    is attempted.
  3. If remove_workspace_by_id takes a full wt_info object instead of workspace_id,
    or has a different signature, adjust the call in the test accordingly to match the
    real API while keeping the core assertions (resolve_clone_path(...) is None and
    remove_workspace_by_id(...) is False).

`ty` is in the `ci` task and rejects `Path = None`. It did not catch this
locally because a stray uv `.venv` in the checkout shadowed the pixi
environment, so every run reported 44 unresolved-`pytest` imports and nothing
else -- a local check that fails for a reason unrelated to the code is one
nobody reads.
@codecov

codecov Bot commented Aug 13, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 94.67%. Comparing base (b438d73) to head (51b0fc1).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #181      +/-   ##
==========================================
+ Coverage   94.63%   94.67%   +0.04%     
==========================================
  Files          21       21              
  Lines        2570     2591      +21     
==========================================
+ Hits         2432     2453      +21     
  Misses        138      138              
Files with missing lines Coverage Δ
devlaunch/dl.py 93.46% <100.00%> (+0.02%) ⬆️
devlaunch/worktree/workspace_clone.py 90.90% <100.00%> (+0.97%) ⬆️

Impacted file tree graph

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

blooop added 2 commits August 13, 2026 23:11
The first cut of the resolver used `recorded.exists()`, which reintroduced the
bug `read_clone`'s docstring spends a paragraph on -- one layer out, and in the
same file that documents it.

`Path.exists()` swallows ENOENT, ENOTDIR, EBADF and ELOOP and re-raises
everything else. A clone whose parent is mode 000 therefore raised
`PermissionError` straight out of the resolver on 3.10-3.13, and returned False
on 3.14. One expression, two behaviours, and 3.14's is the dangerous half:
"not there" is the answer that sends this method off to name a *different*
directory, which is the whole defect #174 is about.

This passed locally on 3.14 and failed on all four older legs of the matrix.
Worth recording, because the matrix is regularly proposed for trimming: the
`default` (3.14) leg went green on the broken code and the py310-py313 legs are
what caught it.

`_on_disk` is total over the three answers and lives at module scope rather than
on the class. That is not style: `_clone_manager` in the tests calls the real
`resolve_clone_path` with a MagicMock as `self`, so a `self._on_disk(...)` came
back as a truthy Mock and inverted the rule -- two tests that had been red for
the right reason went green for the wrong one. Module scope is the part of the
method a double cannot stand in front of.
The `ValueError` path and its `None` were reachable and untested — codecov
caught what the suite did not. Writing the tests found a third thing: the
warning read `wt_info.workspace_id`, a field the resolver otherwise never
touches and the test double does not carry. It now names the owner/repo/branch
that actually failed validation, which is both the thing that failed and the
field a hand-edited metadata.json would have to be fixed in.
@blooop
blooop merged commit 68548c0 into main Aug 13, 2026
10 checks passed
@blooop
blooop deleted the fix/174-one-path-for-the-guard-and-the-delete branch August 13, 2026 22:18
@blooop blooop mentioned this pull request Aug 14, 2026
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.

The unsaved-work guard and the delete can name different directories

1 participant