Skip to content

Two defects that break the dev tasks and the e2e suite before dind is involved - #118

Merged
blooop merged 4 commits into
mainfrom
wayfinder/devlaunch-111
Aug 8, 2026
Merged

blooop merged 4 commits into
mainfrom
wayfinder/devlaunch-111

Conversation

@blooop

@blooop blooop commented Aug 8, 2026 •

Copy link
Copy Markdown
Owner

Closes #111.

Two defects that break pixi run test-e2e — and every pixi run dev* task —
before docker-in-docker is anywhere near it. Both reproduce on the host; neither
is a dind problem. Found by the in-container prototype in #96, which had to step
over both to establish that the e2e suite runs inside the devcontainer at all.

They turn out to be the same defect twice: an outcome that could not be read is
being silently read as "nothing is there"
— and "nothing is there" is then acted
on. A listing that failed to parse is not an empty listing. A create that failed
after creating something is not a create that did nothing.

1. dev-add-docker re-added a provider that already existed

dev-add-docker = "devpod provider list | grep -qw docker || devpod provider add docker"

devpod v0.26.1 colourises provider list unconditionally and emits the reset
sequence ESC[m immediately before each cell, so the character in front of
docker is the letter m. grep -w's left word boundary never matches.
Measured on this host: grep -q rc=0, grep -qw rc=1; NO_COLOR=1 does not
help, devpod ignores it.

Blast radius: every task that depends on it — dev, dev-vs, dev-restart,
dev-restart-vs, and test-e2e once it lands — on every machine that has run
once, which after the first run is every machine. They die on
fatal provider docker already exists.

The fix is not dropping -w. That fixes today's rendering and leaves us
parsing a table devpod is free to re-render tomorrow. devpod v0.26.1 has
provider list --output json, verified on the host: an object keyed by provider
name, no ANSI, no column layout. Principle 1 picks the form that has no rendering
to change. The guard moves into devlaunch/devpod_provider.py, which also gives
the decision a seam it can be tested at — it had none as a shell one-liner.

The second half of the fix is the part that generalises: output the guard could
not read used to come back as "no providers registered", and no-providers means
add one, so an unreadable answer quietly became an action. Unreadable is now its
own outcome (UnreadableProviderList) and so is a listing command that exited
non-zero.

2. test_purge_deletes_workspaces skipped on every headless machine, leaking a container each time

It called devpod up ... --id e2e-test-purge without --ide none, unlike
every sibling. devpod built the workspace, then tried to open an editor, found no
xdg-open, and exited 1:

info SSH configuration completed in workspace      # <- the workspace exists
error flavor VS Code is not installed on host device: exec: "xdg-open": ...
fatal code or code-insiders is not installed
EXIT=1

The test read that rc as pytest.skip("Could not create test workspace") — so it
skipped on any headless machine, container or not, while the container it had
just successfully made stayed behind, tracked by nothing. #96 saw the orphaned
e2e-test-purge in docker ps -a after each run.

The missing flag is the smaller failure. The skip is the one that hurts: it
converts a leak into a line of grey text in a green run.

Fix: one create_e2e_workspace helper that every e2e creation site goes
through, so neither mistake is writable again — no IDE ever, registered for
cleanup before creation is attempted rather than after (registering afterwards
registers only on the happy path, the one path that did not need it), and a
non-zero rc fails with devpod's output attached instead of skipping. It takes the
process boundary as a parameter, so its contract is covered by unit tests that
need no daemon.

Two siblings of the same defect went with it. The other creation sites wrapped
their assertions in if result.returncode == 0: — a failed create made the
assertion body vanish and the test pass. And three hand-rolled copies of "list the
workspaces, but only if the listing read" turned an unreadable listing into an
empty one, which the assertion below then reads as "the workspace is gone".

Test-first, and what the red looked like

Each defect was made to go red for its own reason before anything was fixed, by
transcribing the current behaviour into the new seam.

Defect 1 — the old guard, fed devpod's real recorded coloured table:

old guard, fed devpod real coloured table:
  added a provider that already exists? True
  commands: [['devpod','provider','list'], ['devpod','provider','add','docker']]

Defect 2 — the current creation step, against the new tests: FFss. The two
tests asserting "a failed create must not skip" were themselves skipped. A
step that skips cannot be told not to skip.

The fixtures are recordings, not approximations

test/fixtures/devpod/ holds byte-for-byte captures of devpod v0.26.1 on this
host: the colourised table, a listing with docker registered, and one from a
devpod home that has never had a provider added. A hand-written approximation of
ANSI output is exactly how a bug like this survives being fixed once. The
whitespace-tidying pre-commit hooks are excluded from that directory for the same
reason — a hook that tidies the evidence is a hook that edits it. One test asserts
the recording really is colourised and that the character before docker really
is m.

Verified on metal

  • pixi run dev-add-docker against the real devpod home (already registered →
    no-op, rc=0), against an empty scratch DEVPOD_HOME (added, rc=0), and the same
    scratch home a second time (already registered, rc=0). Cold add and idempotence
    both hold; the real devpod home was only ever read.
  • devpod provider list --output json shape confirmed on the host, and devpod up --help confirms --ide "If empty will use vscode locally or in browser" — so
    omitting it is opting into an editor.
  • Suite: 932 passed, 21 e2e deselected. format / ruff / ty / pylint
    10.00 / prek all clean.
  • The e2e suite was not run: it builds real containers on a daemon several
    efforts share. Defect 2's fatal is already recorded on metal in Can devpod up — and dl itself — run from inside the dind devcontainer? #96 and was not
    re-created here.

Overlap with #106

PR #106 (ticket #103) scopes the e2e suite to a per-run DEVPOD_HOME /
DEVPOD_SSH_CONFIG in pytest_configure and is still open. This branch is off
main and does not depend on it. The two touch test/e2e/test_full_workflow.py
from different angles — #106 changes the environment the suite runs in, this
changes how the suite creates a workspace — so expect a textual conflict at the
creation sites and no semantic one: create_e2e_workspace takes env as a
parameter, which is exactly where #106's scoped environment goes.

🤖 Generated with Claude Code

Summary by Sourcery

Introduce a robust DevPod provider guard and shared e2e workspace creation helper to prevent unreadable DevPod outputs and failed workspace creations from being silently treated as no-ops.

Bug Fixes:

  • Fix dev-add-docker task misdetecting an already-registered DevPod docker provider due to ANSI-coloured output and word-boundary grep.
  • Ensure e2e workspace creation failures (e.g., missing IDE on headless hosts) fail tests instead of skipping while leaving leaked containers, and correctly verify workspace presence via readable listings.

Enhancements:

  • Add a Python-based devpod provider management module that uses JSON output, distinguishes unreadable listings, and exposes a CLI entry point.
  • Centralize e2e workspace creation in a tested helper that always disables IDEs, registers workspaces for cleanup before creation, and surfaces DevPod failures with captured output.
  • Tighten e2e purge and lifecycle tests by factoring out workspace ID listing and removing conditional assertions on subprocess return codes.

Build:

  • Update the dev-add-docker pixi task to invoke the new Python DevPod provider module instead of a brittle shell/grep pipeline.

Tests:

  • Add unit tests for the DevPod provider guard using real recorded DevPod outputs to cover registered, empty, colourised, and failing listings.
  • Add unit tests for the e2e workspace creation helper using fake DevPod processes and trackers to verify IDE disabling, pre-registration for cleanup, and failure semantics.
  • Record real DevPod provider listing fixtures (JSON and colourised table) and guard them with tests to ensure their shape and ANSI characteristics remain intact.

Chores:

  • Exclude recorded DevPod fixtures from whitespace and mixed-line-ending pre-commit hooks to keep them byte-for-byte accurate.

Austin Gregg-Smith added 2 commits August 8, 2026 09:58
`dev-add-docker` guarded itself with `grep -qw docker` over `devpod
provider list`. devpod v0.26.1 colourises that table and emits the reset
`ESC[m` immediately before each cell, so the character in front of
`docker` is the letter `m` and `-w`'s left word boundary never matches:
`grep -q` rc=0, `grep -qw` rc=1, and NO_COLOR does not help. Every
machine that had already registered the provider re-ran `provider add`
and got `fatal provider docker already exists`, taking `dev`, `dev-vs`,
`dev-restart` and `dev-restart-vs` down with it.

Dropping `-w` would fix today's rendering and leave us parsing a table
devpod may re-render tomorrow. `provider list --output json` is the same
information with no rendering to change.

The other half is the shape of the bug rather than the bug: output the
guard could not read came back as "no providers", and "no providers"
means "add one" -- an unreadable answer became an action. Unreadable is
now its own outcome.

The fixtures are byte-for-byte recordings of devpod v0.26.1: the
colourised table, a listing with docker registered, and one from a
devpod home that has never had a provider added.
`test_purge_deletes_workspaces` called `devpod up ... --id e2e-test-purge`
without `--ide none`, unlike every sibling. devpod built the workspace,
then tried to open an editor, found no `xdg-open`, and exited 1. The test
read that rc as "could not create test workspace" and skipped -- on any
headless machine, container or not -- leaving the container it had just
successfully made running and tracked by nothing.

Two failures compounding. The missing flag is the smaller one: a create
that failed after creating something is not a create that did nothing,
and skipping a step the test cannot proceed without turns a leak into a
line of grey text.

Both are properties of the creation step, so the step is now one helper
every e2e test goes through: no IDE ever, registered for cleanup before
creation is attempted rather than after, and a non-zero rc fails with
devpod's output attached instead of skipping. It needs no daemon to
test, and the tests for it are unit tests.

The sibling tests wrapped their assertions in `if result.returncode == 0`,
which is the same defect with the volume down -- a failed create made the
body vanish and the test pass. Those conditionals go too, along with the
three hand-rolled copies of "list workspaces, but only if the listing
read". An unreadable listing there was also becoming an empty one, which
an assertion then reads as "the workspace is gone".

@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 8, 2026

Copy link
Copy Markdown

Reviewer's Guide

Refactors DevPod provider detection and e2e workspace creation to use robust, machine-readable interfaces, adds unit-tested seams for both, and removes silent fallbacks where unreadable or failing commands were previously treated as benign empties or skips.

Sequence diagram for dev-add-docker using devpod_provider CLI

sequenceDiagram
    participant PixiTask
    participant devpod_provider_cli as DevpodProviderCLI
    participant Devpod

    PixiTask->>devpod_provider_cli: main(name=docker)
    devpod_provider_cli->>devpod_provider_cli: ensure_provider(name)
    devpod_provider_cli->>Devpod: list_provider_names()
    Devpod-->>devpod_provider_cli: provider_list_json
    devpod_provider_cli->>devpod_provider_cli: parse_provider_names(listing)

    alt docker already registered
        devpod_provider_cli-->>PixiTask: exit 0 (already registered)
    else docker not registered
        devpod_provider_cli->>Devpod: devpod provider add docker
        Devpod-->>devpod_provider_cli: returncode
        alt add succeeded
            devpod_provider_cli-->>PixiTask: exit 0 (added)
        else add failed
            devpod_provider_cli-->>PixiTask: exit 1 (error)
        end
    else listing unreadable
        devpod_provider_cli-->>PixiTask: exit 1 (UnreadableProviderList)
    end
Loading

Sequence diagram for the new e2e workspace creation helper

sequenceDiagram
    actor Pytest
    participant E2EHelper
    participant CleanupRegistry
    participant Devpod

    Pytest->>E2EHelper: create_e2e_workspace(env, workspace_id)
    E2EHelper->>CleanupRegistry: register_workspace_for_cleanup(workspace_id)
    E2EHelper->>Devpod: devpod up . --ide none --id workspace_id
    Devpod-->>E2EHelper: returncode, stdout, stderr

    alt returncode == 0
        E2EHelper-->>Pytest: workspace ready
    else returncode != 0
        E2EHelper-->>Pytest: pytest.fail(devpod_output)
    end
Loading

File-Level Changes

Change Details Files
Replace shell-based DevPod provider guard with a Python module that consumes JSON output, distinguishes unreadable listings, and is invoked by the dev-add-docker task.
  • Introduce devlaunch.devpod_provider module with JSON-based provider listing, UnreadableProviderList error type, and ensure_provider/main entry point.
  • Change pixi dev-add-docker task to invoke python -m devlaunch.devpod_provider docker instead of grepping devpod provider list.
  • Add unit tests for provider parsing and ensure_provider behaviour, using recorded DevPod outputs as fixtures.
devlaunch/devpod_provider.py
pyproject.toml
test/unit/test_devpod_provider.py
test/fixtures/devpod/provider_list.json
test/fixtures/devpod/provider_list_empty.json
test/fixtures/devpod/provider_list_plain.ansi
Centralise e2e DevPod workspace creation in a helper that always disables IDEs, registers cleanup before creation, and fails tests on non-zero exits instead of skipping.
  • Add create_e2e_workspace helper in fixtures.e2e_helpers which calls devpod up with --ide none, tracks workspaces before creation, and uses pytest.fail on failure while capturing devpod output.
  • Add unit tests for create_e2e_workspace using fake trackers/processes to ensure IDE suppression, early cleanup registration, failure semantics, and argument wiring.
  • Refactor e2e tests in test_full_workflow.py to use create_e2e_workspace and a shared workspace_ids helper, removing ad-hoc subprocess logic and skip-on-failure patterns.
test/fixtures/e2e_helpers.py
test/unit/test_e2e_workspace_helper.py
test/e2e/test_full_workflow.py
Preserve raw DevPod output fixtures and rely on machine-readable workspace listings in e2e assertions.
  • Exclude test/fixtures/devpod/ from end-of-file-fixer and mixed-line-ending pre-commit hooks to keep recordings byte-for-byte.
  • Add workspace_ids helper that calls devpod list --output json with check=True and converts unreadable listings into hard failures instead of empty lists.
  • Update purge-related e2e tests to assert workspace presence/absence via workspace_ids, ensuring unreadable listings or command failures surface as test failures.
.pre-commit-config.yaml
test/e2e/test_full_workflow.py
test/fixtures/devpod/provider_list_plain.ansi
test/fixtures/devpod/provider_list.json
test/fixtures/devpod/provider_list_empty.json

Assessment against linked issues

Issue Objective Addressed Explanation
#111 Ensure the devpod provider guard used by dev-add-docker correctly detects an already-registered provider even with colourised output, preferably by using a machine-readable form and adding tests based on real devpod output. ✅
#111 Fix e2e workspace creation (especially test_purge_deletes_workspaces) so that workspaces are created with --ide none, registered for cleanup before creation, and any non-zero devpod up return code fails the test instead of skipping while leaking containers, with tests covering this behaviour. ✅

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 8, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 90.39%. Comparing base (1e321b2) to head (0a6b96f).

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #118      +/-   ##
==========================================
+ Coverage   90.21%   90.39%   +0.17%     
==========================================
  Files          16       17       +1     
  Lines        1942     1978      +36     
==========================================
+ Hits         1752     1788      +36     
  Misses        190      190              
Files with missing lines Coverage Δ
devlaunch/devpod_provider.py 100.00% <100.00%> (ø)

Impacted file tree graph

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

`main` reached devpod through a module-level default, so the entry point
the task invokes was the one piece of the guard no test could touch --
including the case that matters most, a devpod whose answer cannot be
read having to stop the task rather than let it carry on as if the
provider were missing. It takes the process boundary the same way the
rest of the module does.

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

Reviewed at b6bc2705a4910b77abf5510956528148f6acdc51 against merge-base cc1f11a4ca9badb6aa8a7a99d63bad483f2fbf11, by a reviewer that did not see this code written. Two axes, kept separate and not reranked against each other. The Spec axis was run as an independent fresh-context subagent and its findings re-verified here; the Standards axis is direct review. Every load-bearing claim in the PR body was re-measured on this host rather than taken on trust.

Preflight: ref resolves, diff non-empty (589+/106−, 10 files), all checks green (ci py310–py313, prek, codecov patch+project, GitGuardian), MERGEABLE.


What I verified on metal, independently

The PR's factual claims hold. All of them.

Claim Result
devpod provider list --output json exists on v0.26.1 True. rc=0, object keyed by provider name. --output documented in provider list --help as "Can be json or plain". Measured on the shipped skevetter/devpod fork, not from loft-sh docs.
The char before docker is m True. devpod provider list | cat -v → ^B^[[mdocker. grep -q docker rc=0, grep -qw docker rc=1, NO_COLOR=1 … grep -qw rc=1.
Fixtures are byte-for-byte recordings True, and this is the strongest single piece of evidence in the PR. devpod provider list | md5sum = 6591bc70c8ea1db0cd6cdc9ca688dedc = md5sum test/fixtures/devpod/provider_list_plain.ansi. provider list --output json | md5sum = 1784f482e11548957571ae3d746351f8 = provider_list.json. Exact, both.
Empty listing is {} True. Scratch DEVPOD_HOME → {}, rc=0, no trailing newline — matching provider_list_empty.json exactly.
New guard works cold and is idempotent True. Scratch DEVPOD_HOME: first run installed provider: providerName=docker → added; second run → already registered, rc=0. Against the real devpod home: already registered, rc=0, provider set still ['docker'].
New module is 100% covered True and meaningful, not line-touching — 20 passed, devlaunch/devpod_provider.py 36 stmts, 0 miss.
Defect 1 red reproduces True. grep -qw docker against the committed recording → rc=1; grep -q → rc=0.
Defect 2 red is FFss True, reproduced against the PR's own tests with the old creation step transcribed back in.
devpod version range is safe for --output json True, and better than the PR claims: pyproject.toml:30 pins devpod = ">=0.26.1,<0.27", so the JSON flag cannot vanish under the task.

On the new-module trade (defect 1), my honest verdict: the direction is right, the destination is wrong.

Choosing --output json over dropping -w is correct and principle 1 justifies it — a one-character fix would re-break the next time devpod re-renders that table, and the version pin does not protect against a rendering change. That part I would not argue with.

But devlaunch/ is the wrong home. [tool.hatch.build] include = ["devlaunch"] means this module ships in the wheel to every pixi global user of dl, and nothing user-facing imports it — its only caller is a dev-only pixi task. scripts/ already exists in this repo for exactly this. Shipping dev-task machinery in the production package is a principle-2 cost paid by users who will never call it.


Standards

  1. devpod_provider.py bypasses run_devpod, breaking an invariant dl.py documents about itself. (should-fix)
    dl.py:825 says, in its own docstring: "This is dl's only devpod spawn, so it is also the only place that can tell 'devpod is not installed' from 'devpod ran and failed'. The former is raised as DevpodNotInstalled rather than folded into a returncode: callers branch on returncode and would carry on as though devpod had answered." The new module calls subprocess.run directly (devpod_provider.py:57, :81). That sentence is now false, and the guard it describes is gone. Reproduced: with devpod absent, main() raises an uncaught FileNotFoundError traceback rather than the clean error: … / rc=1 its docstring promises. This is the PR's own thesis — an unreadable outcome must be its own outcome — with one case left uncovered.

  2. Speculative surface in a 116-line module doing one thing. (nit, but it is the size question)
    parse_provider_names is public with its own tests though only list_provider_names calls it; the non-dict branch (:46) is unreachable from real devpod output and is covered only by a synthetic listing; argparse (:97) is a full parser for one positional. The run=subprocess.run seam is earned — it is what makes the tests daemon-free — but the rest is a smaller module wearing a larger one's clothes.

  3. main() catches (UnreadableProviderList, RuntimeError) where the first is a subclass of the second (:30, :108) — redundant, and the bare RuntimeError arm will swallow unrelated RuntimeErrors as error: …/rc=1. Also ensure_provider's add-failure path (:81–83) runs without capture_output, so its RuntimeError carries no devpod output — unlike every other error path in the module, which attaches it. (nit)

  4. Fixture protection is real but has one hole. (nit) end-of-file-fixer's exclusion is genuinely load-bearing — I confirmed both .json fixtures end without a trailing newline, exactly as devpod emits them, which that hook would have "fixed". mixed-line-ending's exclusion is defensive only (devpod emits LF). But codespell runs --write-changes with exclude: \.(svg|pyc|lock|json|hrd|pgm)$ (.pre-commit-config.yaml:59-61) — that covers the two .json captures and not provider_list_plain.ansi. A hook that rewrites files in place can still reach the evidence. fix-byte-order-marker likewise still applies. Add test/fixtures/devpod/ to codespell's exclude to make the stated intent true.

  5. test/fixtures/e2e_helpers.py is now two modules in one file — a pytest-fixture module (dl_no_ide, devpod_cleanup) and a plain helper library (create_e2e_workspace, which calls pytest.fail from module scope and takes an untyped cleanup). Divergent Change: it will now be edited for two unrelated reasons. (nit)

  6. workspace_ids() uses check=True (test_full_workflow.py:41) — correct in refusing to read an unreadable listing as empty, which is the whole point, but a raw CalledProcessError is a worse failure message than the explicit failure create_e2e_workspace builds three lines away. Inconsistent with the standard the same PR sets. (nit)

  7. Prose volume. The docstrings here are essays — devpod_provider.py is 23 lines of module docstring before the first import, and create_e2e_workspace's docstring is 25 lines for a 16-line function. They are good essays and they encode the reasoning that would otherwise be lost. But they duplicate the PR body and the ticket almost verbatim, and prose that restates a decision is prose that goes stale when the decision moves. Judgement call; I would keep them, and I flag the ratio.

  8. The same defect the PR eradicates is still live in production code, unfixed. (out of scope for #111 — file a follow-up)
    dl.py:930-937:

    result = run_devpod(["list", "--output", "json"], capture=True)
    if result.returncode != 0 or not result.stdout.strip():
        return []
    try:
        data = json.loads(result.stdout)
    except json.JSONDecodeError:
        logging.error("Failed to parse devpod output")
        return []

    Demonstrated live: a garbage listing → []; a rc=1 listing → []. That is verbatim the shape the PR body names as the thing it is eliminating — "an outcome that could not be read is being silently read as 'nothing is there'" — and purge_all_data() (dl.py:398) consumes it. Not this ticket's job, and the consequence in purge is benign ("No data to purge") rather than destructive. But it is worth saying plainly: the fix landed in a dev-only task and a test helper, while the shipped code still does it.


Spec

Measured against issue #111.

  1. The ticket's named precondition is declared unnecessary. (blocking on merge order, not on content)
    #111: "The e2e scoping merged in #103/PR #106 is what makes exercising this safe; build on it rather than reintroducing an unscoped path." PR #106 is still OPEN, and this PR's body answers "This branch is off main and does not depend on it."
    Measured: grep -rn DEVPOD_HOME over the PR head returns nothing. isolated_devlaunch_env (test/fixtures/git_fixtures.py:20) scopes only XDG_CACHE_HOME. purge_all_data() (dl.py:383) lists via unfiltered devpod list and devpod delete --forces every entry. So dl --purge -y in test_purge_deletes_workspaces is bounded by nothing.
    Half of this refutes itself, and the review says so: test_purge_cleans_cache — untouched by this PR, present verbatim at merge-base — already runs dl --purge -y with no gate at all. The incremental blast radius of #118 is therefore ≈ zero; e2e is also deselected by default (addopts = "-m 'not e2e'"). What is new is that #118 makes the sibling purge test proceed rather than skip, and the PR then invites a reviewer to run that file. On a host without #106's scoping, accepting that invitation costs every workspace on the machine. Land #106 first. Do not merge this ahead of it, and do not follow the PR's "run it once to see 8 passed" suggestion without a scoped DEVPOD_HOME.

  2. "Neither mistake is available to write again" is an overclaim. (should-fix)
    #111: "assert the workspace-creation step is not silently skippable." create_e2e_workspace is a convention, not an enforcement — nothing asserts every creation site routes through it, and the next hand-rolled devpod up reintroduces both mistakes. #106's own reasoning is the counterexample worth quoting: "a fixture is something a test has to ask for, and the test that must not forget is the one nobody has written yet" — which is why it chose pytest_configure. Either soften the claim in the docstring, or enforce it (a lint rule, or an autouse fixture that fails on a raw devpod up).

  3. Defect 1's literal acceptance criterion is substituted, defensibly. (nit)
    #111 asks for "a test that the 'provider already present' case is detected as present, fed devpod's real coloured output as a fixture." No test does that: the coloured recording instead asserts UnreadableProviderList, and "present" is asserted against the JSON capture. The same ticket paragraph authorises the machine-readable form, so this is a stronger answer than the one asked for — but it is a substitution, and worth naming rather than glossing.

  4. Scope growth: in scope, and it is the good kind. #111 frames both defects as one shape — "an outcome that could not be read is being silently read as 'nothing is there'". The if result.returncode == 0: wrappers and the three hand-rolled listing reads are that shape, in the same file, in the tests the ticket is about. Decisive evidence for principle 2: test_full_workflow.py is 61+/105−, net −44 lines. Scope grew; the code shrank. No objection.

  5. The #106 conflict prediction: right conclusion, wrong location. git merge-tree of the two heads gives one conflict, in test/e2e/test_full_workflow.py lines 20–67 — the import header, not the creation sites (#106 never touches them). e2e_helpers.py and conftest.py auto-merge. A naive take-both resolution parses and keeps workspace_ids() plus all four call sites.
    No semantic conflict, and the mechanism is confirmed: #106 mutates os.environ in pytest_configure, which runs before collection, so the call sites' env={**os.environ, "XDG_CACHE_HOME": …} snapshot it afterwards and preserve DEVPOD_HOME/DEVPOD_SSH_CONFIG; workspace_ids() passes no env and inherits. The prediction holds.

  6. Residual verification gap (should-fix, moderate). #111: "Do not run the full e2e suite on the host to verify." The author complied; so did this review. Nobody has yet seen 8 passed in place of 7 passed 1 skipped. The unit seams are genuinely good and the defect-2 fatal is already recorded on metal in #96, so the gap is narrow — but the map's verify on metal principle is unmet for defect 2, and the single run that would close it is exactly the run finding 1 makes dangerous on an unscoped host. Ordering behind #106 closes both at once, which is the cleanest resolution available.


Verdict

Comment — approve on content, blocked on merge order.

This is careful work. The reasoning is sound, the recordings are real (I md5-matched all three against live devpod), the red transcripts reproduce, the fix is verified cold and idempotent on metal, and the test suite shrank while the coverage grew. Defect 1's choice of the machine-readable form over dropping -w is the right call under principle 1 and I would not relitigate it.

Blocking, in order:

  • B1 — Do not merge before PR #106 (Spec 1). The ticket named #106's scoping as the precondition that makes this safe; the PR declares it unnecessary while adding an invitation to run the unscoped suite. The conflict is textual-only and the semantics compose cleanly (Spec 5), so the ordering costs almost nothing.

Non-blocking but I would want them before merge:

  • S1 — route the new module through run_devpod (or catch FileNotFoundError); a missing devpod currently produces a traceback, and dl.py:825's "only devpod spawn" docstring is now untrue.
  • Move the module out of devlaunch/ — it ships to every dl user via [tool.hatch.build] include = ["devlaunch"] and only a dev task calls it. scripts/ is right there.
  • S4 — add test/fixtures/devpod/ to codespell's exclude; --write-changes can still rewrite the .ansi capture.
  • Spec 2 — soften or enforce "neither mistake is available to write again".

Worth a follow-up ticket, not this PR: dl.py:930 still reads an unreadable devpod list as an empty one (Standards 8) — the exact defect this PR was written to eliminate, still in the shipped path.

Host state, before → after this review: devpod list unchanged (devlaunch-main-zovomobo, devlaunch-t1-vebilote, pythontemplate); provider set unchanged (['docker']); no container created or removed; the e2e suite was not run. ~/.ssh/config md5 moved da3e288c… → 6cf324ea… mid-review, but all three DevPod-managed blocks and the user's own homeassistant block are intact and no workspace was lost — attributable to concurrent devpod activity on the shared daemon, not to anything this review ran (writes here went only to a scratch DEVPOD_HOME, and the hash was stable across those).

@blooop
blooop merged commit 580ed21 into main Aug 8, 2026
9 checks passed
@blooop
blooop deleted the wayfinder/devlaunch-111 branch August 8, 2026 10:54
blooop added a commit that referenced this pull request Aug 8, 2026
v0.0.15 was cut from #121 while #106 and #118 were already merged into it, so
its section described only the aid change. Both are now recorded under 0.0.15,
where they shipped, and this PR's fix becomes 0.0.16.
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.

Two defects break pixi run test-e2e before dind is involved

1 participant