Skip to content

Give the test suite a devpod namespace of its own - #106

Merged
blooop merged 3 commits into
mainfrom
wayfinder/devlaunch-103
Aug 8, 2026
Merged

blooop merged 3 commits into
mainfrom
wayfinder/devlaunch-103

Conversation

@blooop

@blooop blooop commented Aug 8, 2026

Copy link
Copy Markdown
Owner

Closes #103.

The footgun

pytest -m e2e on a developer's machine deletes their real devpod workspaces. Three facts compose:

  • dl --purge deletes every workspace devpod list returns — it has no notion of "only mine".
  • An e2e test exercises exactly that, for real.
  • devpod list reads ~/.devpod, which no XDG_* variable relocates. The suite already sets XDG_CACHE_HOME on those calls; it buys nothing here.

The -m 'not e2e' default in pyproject.toml was the only thing standing in the way, and a default is not a safeguard — a misconfigured editor, a -m e2e, or an agent that did not read the marker config is enough.

Measured on the host, at the exact seam that does the deleting — dl's own list_workspaces(), the list purge_all_data() iterates:

what --purge would delete
unscoped ['devlaunch-main-zovomobo', 'devlaunch-t1-vebilote', 'pythontemplate']
scoped []

The seam, and why this one

Scope the process environment once in pytest_configure, before collection, pointing DEVPOD_HOME at a directory created for this run.

The ticket offered three candidates. This one was picked because it is scoping rather than guarding:

  • vs. a fixture that refuses to run destructive e2e unless pointed at a scoped home — a fixture is something a test has to ask for, and the test that must not forget is the one nobody has written yet. pytest_configure covers every subprocess the session spawns, because they all inherit this process's environment. That includes the devpod list inside dl --purge, which is the whole ballgame.
  • vs. asserting devpod list is empty before any --force delete — an assertion at each call site is exactly the thing the next new test can omit. Same objection, worse ergonomics.
  • vs. per-run unique workspace ids — this subsumes it. A namespace created fresh per run makes the hardcoded e2e-test-* constants private to the run already, so two concurrent runs stop force-deleting each other's workspaces. Adding unique ids on top would be a second concept overlapping the first (principle 2).

No new type: the alternative shape was a helper that builds the subprocess environment, but a helper can be bypassed by any subprocess.run that passes os.environ directly, and the process environment cannot. The weaker-but-unbypassable option wins.

Verified on metal, not read off docs

  1. DEVPOD_HOME=<scratch> devpod list --output json returns [] on a host with three real workspaces.

  2. DEVPOD_HOME alone is not enough. devpod up writes its Host <id>.devpod block to the real ~/.ssh/config regardless — but --ssh-config has an env twin, DEVPOD_SSH_CONFIG, so the same seam covers it with one more variable. This answers an open question on the map (Develop devlaunch inside its own devcontainer, on an isolated Docker daemon #93): devpod resolves the ssh config against neither $HOME-via-DEVPOD_HOME nor DEVPOD_HOME — it is ~/.ssh/config unless DEVPOD_SSH_CONFIG says otherwise.

    With one qualifier, added in review and re-measured on devpod v0.26.1. The two variables do not have the same reach. --devpod-home is a persistent root flag, so DEVPOD_HOME covers every subcommand (devpod delete --help lists it). --ssh-config is registered on devpod up alone — counted on this host, up: 1, ssh: 0, delete: 0, stop: 0 — so DEVPOD_SSH_CONFIG redirects only the subcommand that writes ssh config. That is the one that does the damage, so the suite is covered; but DEVPOD_SSH_CONFIG is not a global, and the module docstring and the unit test now say so rather than leaving a reader to assume otherwise.

  3. A fresh DEVPOD_HOME has no providers at all, so devpod up in it fails until docker is installed there. Hence test/e2e/conftest.py: autouse, session-scoped, so no new e2e test can forget it and no unit run pays for it.

  4. The destructive path itself, end to end. dl --purge -y was run for real against a synthetic scoped namespace holding one workspace. It deleted exactly that one; all three of the host's real workspaces survived.

Tests

test what it would have caught
test_devpod_in_this_session_cannot_see_the_developers_workspaces (e2e) the bug itself. Spawns a real devpod list the way the suite spawns everything and asserts it is disjoint from the ids read straight off ~/.devpod on disk. Went red naming exactly the three real workspaces, green after the fix. Lives in the e2e directory so the assertion runs in the session that could do the damage.
test_devpod_commands_are_pointed_away_from_the_developers_devpod_home (unit) the scoping being removed or pointed somewhere under $HOME.
test_devpod_up_is_pointed_away_from_the_developers_ssh_config (unit) fact 2 above being forgotten — DEVPOD_HOME set but the ssh config still the developer's. Named for devpod up because that is the only subcommand DEVPOD_SSH_CONFIG reaches.
test_each_run_gets_a_devpod_home_of_its_own (unit) the per-run directory being "simplified" into a fixed path, which would restore the concurrent-run collision the hardcoded ids cause.
docker_provider_in_scoped_devpod_home (e2e fixture) the scoping regressing underneath the suite's own one unconditional write to a devpod home. devpod provider add docker --use rewrites the default provider of whichever home is live; the fixture refuses to run it unless DEVPOD_HOME is set.

The three unit tests run on the default suite, not under -m e2e — so removing the scoping breaks an ordinary pixi run test rather than waiting for the run that would do the damage.

Each red state was reproduced on this host, by mutation, and restored. Commenting out scope_devpod_to_this_run() in pytest_configure turns the first two unit tests red on assert None for DEVPOD_HOME / DEVPOD_SSH_CONFIG, and turns the e2e provider fixture into an error that names the write it refused. Replacing the per-run mkdtemp with a fixed path turns test_each_run_gets_a_devpod_home_of_its_own red on assert first != second with the two identical paths printed. The third test drives scope_devpod_to_this_run itself, twice, rather than the directory-making it happens to use — an earlier revision asserted tempfile.mkdtemp's own contract and stayed green under both mutations, which is why the function it called (make_scoped_devpod_home) no longer exists.

The scratch directory is deliberately not cleaned up: it holds the metadata devpod needs to find and delete the containers a run created, so removing it after a crashed run would orphan those containers with no way to reach them.

Deliberately not fixed

Production dl --purge still deletes every workspace devpod list returns. Per the ticket's scope note, this PR makes the test suite safe; whether --purge should learn a narrower blast radius in production is a separate question, raised on #103 for the map to graduate. No devlaunch/ source file is touched by this PR.

Not done

The full e2e suite was not run on this host. Under this change it would be safe to, but it builds real containers on a Docker daemon another effort is concurrently using. The destructive path was verified directly instead (item 4 above). Full default suite: 811 passed. Lint 10.00/10.

🤖 Generated with Claude Code

Summary by Sourcery

Scope all DevPod interactions in the test suite to a per-run namespace to prevent destructive operations from touching developers' real workspaces.

New Features:

  • Add an e2e test that verifies DevPod in the test session cannot see the developer's real DevPod workspaces.

Bug Fixes:

  • Prevent dl --purge and other DevPod commands run by the test suite from deleting or modifying real developer workspaces and SSH config.

Enhancements:

  • Introduce a DevPod scoping helper that configures DEVPOD_HOME and DEVPOD_SSH_CONFIG to a run-specific directory during pytest configuration.
  • Add unit tests that enforce DevPod scoping and per-run isolation of DevPod home directories.
  • Automatically install the docker DevPod provider into the scoped DevPod home for e2e tests so they can run against a usable environment.

Tests:

  • Add unit and e2e tests that ensure DevPod state and SSH config used by the suite are isolated from the developer's environment and that each test run gets its own DevPod namespace.

Running the e2e suite deleted the developer's real devpod workspaces.
Three facts composed into it: `dl --purge` deletes every workspace
`devpod list` returns, an e2e test exercises exactly that for real, and
`devpod list` reads ~/.devpod, which no XDG_* variable relocates. The
suite's existing XDG isolation therefore bought nothing against it, and
the `-m 'not e2e'` default was the only thing in the way.

Scope the process environment once, before collection, rather than add a
fixture or a before-delete assertion. Everything the session spawns
inherits it, including the `devpod list` inside `--purge`, so there is
nothing a future test can forget to ask for. A per-run directory also
makes the hardcoded `e2e-test-*` ids private to the run, so concurrent
runs stop force-deleting each other's workspaces -- one mechanism
instead of two overlapping ones.

DEVPOD_HOME alone is not enough: devpod resolves its ssh config against
the real $HOME regardless, so DEVPOD_SSH_CONFIG is scoped too. And a
fresh devpod home has no providers at all, so the e2e directory seeds
the docker provider into it.

Production `--purge` behaviour is deliberately unchanged.

@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

This PR scopes all DevPod-related operations in the test suite to a per-run, scratch devpod namespace by setting DEVPOD_HOME and DEVPOD_SSH_CONFIG once in pytest_configure, and adds tests to ensure the suite can no longer see or mutate a developer’s real DevPod state while keeping e2e DevPod usage functional.

File-Level Changes

Change Details Files
Scope DevPod state for the entire pytest session via environment variables set in pytest_configure.
  • Extend pytest_configure to invoke a scoping helper before registering markers.
  • Ensure all subprocesses spawned by the suite inherit DEVPOD_HOME and DEVPOD_SSH_CONFIG to redirect DevPod state and SSH config away from the developer’s real home.
test/conftest.py
test/fixtures/devpod_scoping.py
Introduce a fixture module that creates a per-run DevPod home and points DevPod commands at it.
  • Add make_scoped_devpod_home to create a unique temporary devpod home directory for each run under the system temp dir.
  • Add scope_devpod_to_this_run to set DEVPOD_HOME and DEVPOD_SSH_CONFIG in os.environ and return the created home.
  • Document the rationale for scoping vs guarding and why the scoped directory is not cleaned up.
test/fixtures/devpod_scoping.py
Ensure the e2e DevPod tests can use the scoped DevPod home by installing the docker provider once per e2e session.
  • Add an e2e-level autouse, session-scoped fixture that runs devpod provider add docker --use if devpod is available.
  • Avoid affecting non-e2e tests by limiting provider installation to the e2e conftest.
test/e2e/conftest.py
Add an e2e regression test that proves the test session cannot see the developer’s real DevPod workspaces.
  • Add a helper to read workspace IDs directly from ~/.devpod/contexts//workspaces/ instead of using devpod list.
  • Add TestSuiteIsolationE2E with test_devpod_in_this_session_cannot_see_the_developers_workspaces that compares devpod list output against the real workspace IDs and asserts disjointness.
test/e2e/test_full_workflow.py
Add unit tests that enforce correct scoping of DevPod home, SSH config, and per-run isolation.
  • Add tests that assert DEVPOD_HOME and DEVPOD_SSH_CONFIG are set and do not point under the real $HOME (including ~/.devpod and ~/.ssh/config).
  • Add a test that verifies make_scoped_devpod_home creates distinct paths for multiple invocations to avoid namespace sharing between concurrent runs.
  • Run these unit tests in the default suite so removing scoping breaks ordinary test runs rather than only e2e runs.
test/unit/test_devpod_scoping.py

Assessment against linked issues

Issue Objective Addressed Explanation
#103 Ensure that running the e2e test suite (including dl --purge) cannot delete or otherwise touch the developer's real DevPod workspaces or SSH config on the host.
#103 Prevent concurrent e2e runs from deleting each other’s DevPod workspaces due to shared hardcoded workspace IDs, by giving each run its own isolated namespace.

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 89.96%. Comparing base (b304cb6) to head (615df48).
⚠️ Report is 26 commits behind head on main.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main     #106      +/-   ##
==========================================
+ Coverage   89.24%   89.96%   +0.72%     
==========================================
  Files          12       15       +3     
  Lines        1636     1893     +257     
==========================================
+ Hits         1460     1703     +243     
- Misses        176      190      +14     

see 7 files with indirect coverage changes

Impacted file tree graph

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

@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 in fresh context against two independent axes. The fixed point is b304cb6...7f90ada (5 files, +189/-1, all under test/). Checks green across py310–py313, prek, codecov, GitGuardian. Both axes verified their load-bearing claims on metal on this host, per the map's extra principle.

The core fix is sound and should land. Everything below is about the tests that guard it, not about the seam.


Standards

Tooling passes: pixi run test811 passed, 9 deselected; pixi run lint (ruff + ty + pylint) → clean, pylint 10.00/10, no working-tree drift.

  • test_each_run_gets_a_devpod_home_of_its_own does not test this PR's codeblockingtest/unit/test_devpod_scoping.py:47-57. The red state was reproduced by commenting out scope_devpod_to_this_run() in test/conftest.py: the first two unit tests go red for exactly their stated reason (assert None on DEVPOD_HOME / DEVPOD_SSH_CONFIG) — those two are genuine. This third one stays green. It calls make_scoped_devpod_home(tmp_path) twice and asserts first != second, which is tempfile.mkdtemp's own contract. It would still pass if scope_devpod_to_this_run() hardcoded a fixed path — which is precisely the regression the PR description says it catches ("the per-run directory being 'simplified' into a fixed path"). That claim is false as written. Principle 4: a test must fail when the behavior is absent. Either assert across two calls of scope_devpod_to_this_run() itself (saving/restoring os.environ), or delete the test.
  • The two-function split is a Middle Manshould-fixtest/fixtures/devpod_scoping.py:31-33. make_scoped_devpod_home is a one-line pass-through to mkdtemp whose only production caller is the line directly below it; it exists so the test above can call it, and that test is the one that proves nothing. Principle 2: deleting beats adding.
  • Swallowed setup failure in the e2e provider fixtureshould-fixtest/e2e/conftest.py:22-26. capture_output=True with check=False and the result discarded. If devpod provider add docker --use fails, every e2e test fails later against a provider-less devpod home with an unrelated error, and the stderr explaining it was captured and thrown away. Best-effort teardown is defensible; a best-effort precondition is not. check=True, or pytest.fail(result.stderr).
  • The new fixture writes to whatever devpod home is live, unguardedshould-fixtest/e2e/conftest.py:22. devpod provider add docker --use is an unconditional mutation, and --use changes the default provider. Its safety rests entirely on the scoping this PR introduces, with nothing asserting the scoping is actually in effect. If the scoping ever regresses, the PR's own new fixture is the thing that reaches into the developer's real ~/.devpod. One assert os.environ.get("DEVPOD_HOME") at the top makes that unrepresentable (principle 3) — cheap, and it is the one place a guard is worth more than a scope.
  • The isolation e2e assertion is vacuous where ~/.devpod is absentnittest/e2e/test_full_workflow.py:75. The glob was verified correct against this host (~/.devpod/contexts/default/workspaces/{pythontemplate,devlaunch-t1-vebilote,devlaunch-main-zovomobo}), so it is not vacuous here. But in CI or DinD, where ~/.devpod does not exist, real_devpod_workspace_ids() returns set() and isdisjoint passes unconditionally. Skip or assert the precondition.
  • Two rival notions of "devpod is available"nittest/e2e/conftest.py:19 uses shutil.which, test_full_workflow.py:21 uses devpod version. Duplicated Code / Divergent Change: they can disagree.
  • DEVPOD_HOME_VAR / DEVPOD_SSH_CONFIG_VAR do not earn their keepnitdevpod_scoping.py:27-28. Used only inside their own module; the one cross-module consumer (test/unit/test_devpod_scoping.py:22,38) uses bare string literals, unlike gh_auth.DISABLE_VAR which test/conftest.py:44 genuinely imports. Speculative Generality — use them or delete them.
  • test/fixtures/devpod_scoping.py defines no fixturenit. Every sibling in test/fixtures/ exports at least one @pytest.fixture; this is a plain module. Mysterious Name / misplacement.

No environment conflict exists: the only other global-os.environ writers in the tree are git_fixtures.py:38-59 (XDG_CACHE_HOME, save/restore) and test_workspace_id.py:384 (monkeypatch.setenv("HOME")), neither touching DEVPOD_*. No pytest-xdist, and coverage run -m pytest is in-process, so every documented entry point (test, coverage, ci, ci-no-cover) reaches pytest_configure. Prose-length test names match the existing convention — not an outlier.


Spec

Core requirement: MET. "Running the e2e suite on a host deletes that host's real devpod workspaces. Make it impossible." pytest_configure (test/conftest.py:88) sets DEVPOD_HOME before collection; every subprocess the session spawns inherits it, including the devpod list inside dl --purge (devlaunch/dl.py:335-339).

  • DEVPOD_HOME and DEVPOD_SSH_CONFIG are both real, not inventedsatisfied, verified on metal. devpod versionv0.26.1. Global flags: --devpod-home string … You can also use DEVPOD_HOME. devpod up --help: --ssh-config string The path to the ssh config to modify, if empty will use ~/.ssh/config. You can also use DEVPOD_SSH_CONFIG. Spellings match devpod_scoping.py:24-25 exactly.
  • No path spawns devpod with a replaced environmentsatisfied. This was the biggest risk to the whole seam. run_devpod(args, env=...) has exactly one caller passing envworkspace_ssh (dl.py:1010), fed by gh_auth.ssh_args_and_env(), which returns {**os.environ, TOKEN_VAR: token} (gh_auth.py:171). Every e2e env= is {**os.environ, "XDG_CACHE_HOME": …}; DLRunner defaults to dict(os.environ) (e2e_helpers.py:49). The one fully-replaced env (test_dl.py:2934) points PATH at an empty dir with no devpod reachable. The scoping survives everywhere. "makes the bad outcome unrepresentable rather than merely unlikely" — met at this seam.
  • The red test's red state is safesatisfied. "The failing test cannot be 'run --purge and see what dies' — the red state of that test IS the damage." The e2e test only runs devpod list and asserts disjointness (test_full_workflow.py:55-75). Independently re-run here by node id under -m e2e: passed in 1.52s, and this host's three real workspaces were byte-identical before and after.
  • DEVPOD_SSH_CONFIG is subcommand-scoped, not globalshould-fix, and it qualifies a map answer. DEVPOD_HOME is a persistent root flag and covers every subcommand. DEVPOD_SSH_CONFIG is registered only on up (up: 1, ssh: 0, delete: 0, stop: 0, machine ssh: 0). up is the writer, so the harm named in the ticket is covered — but devpod ssh and devpod delete still resolve the real ~/.ssh/config. Map #93's open question "whether devpod writes its ~/.ssh/config blocks relative to $HOME or to DEVPOD_HOME" is answered correctly, but the breadcrumb on #103 states it without the qualifier. The unit test's name — test_devpod_ssh_config_is_pointed_away_from_the_developers_ssh_config — promises more than the variable delivers. Narrow the docstring to devpod up, and add the qualifier to the map, or a reader in a year takes the broader claim at face value.
  • Subsumption of the per-run-id defect holds, with one unverified residualnit. Teardown's devpod delete --force (e2e_helpers.py:160-168) now sees only its own namespace, so concurrent runs cannot delete each other's workspaces — the ticket's second, milder instance is genuinely subsumed rather than merely deferred. Outside the namespace: docker container names are random (#95 metal), metadata.json/XDG is per-test tmp_path. Unverified: two concurrent runs both build id e2e-test-purge, whose image tag carries the id plus a hash — not checked whether the hash differs per source path. Not destructive of workspaces either way.
  • Non-cleanup justification is true on this host, but broader than its own rationalenit. /tmp is on / (not tmpfs), /usr/lib/tmpfiles.d/tmp.conf has D /tmp 1777 root root 30d, and systemd-tmpfiles-clean.timer is active — so "a stale directory the OS will reap" is accurate. Six devlaunch-testrun-* dirs exist now, 4K–28K each. Two caveats worth a line in the docstring: (a) the rationale is about containers a run created, but a dir is created by every pytest process, including the ~811-test unit-only runs that never spawn devpod and leave a 4K empty dir behind; (b) the 30-day reaper is itself the orphaning event the docstring is trying to avoid — non-cleanup defers the problem rather than removing it.
  • No scope creep; production finding reportedsatisfied. "Whether dl --purge itself should learn a narrower blast radius in production … is not this ticket." Zero devlaunch/ files touched. The production finding (--purge would have deleted pythontemplate; the WorkspaceId/metadata.json ownership predicate already exists and is unused) is posted on #103 for the map to graduate.

Not in the ticket, but adjacent and now known-incomplete: CLAUDE.md's scratch-XDG paragraph tells a developer to point XDG_CACHE_HOME/XDG_CONFIG_HOME at a scratch dir before running dl-next. This PR establishes that this does not scope devpod list, so a manual dl-next --purge still deletes everything. The suite is now safe; the documented manual workflow is not. One sentence naming DEVPOD_HOME/DEVPOD_SSH_CONFIG would close it — out of scope here, worth a follow-up.


Verdict

Request changes — material but small and mechanical. The seam is the right one, it was chosen for the right reason, and it was verified on metal at the exact place that does the deleting. Nothing here argues for a different design.

Blocking:

  1. test_each_run_gets_a_devpod_home_of_its_own (test/unit/test_devpod_scoping.py:47-57) is green with the mechanism removed. It asserts tempfile.mkdtemp's contract, not this PR's. Rewrite it against scope_devpod_to_this_run() or delete it — and either way correct the PR description's claim that it catches the fixed-path regression. Deleting it also removes the only reason make_scoped_devpod_home exists.

Should-fix before merge:

  1. test/e2e/conftest.py:22 — assert DEVPOD_HOME is set before running devpod provider add docker --use. It is the one new unconditional write to a devpod home in this diff, and today nothing stops it landing in the developer's real one.
  2. test/e2e/conftest.py:22-26 — stop swallowing the provider-install failure; check=True or pytest.fail(result.stderr).
  3. test/unit/test_devpod_scoping.py:31-43 — narrow the ssh-config claim to devpod up, which is the only subcommand DEVPOD_SSH_CONFIG is registered on, and carry the qualifier to the #93 map answer.

Nits (2–8 in the axes above) are optional.

blooop added 2 commits August 8, 2026 09:49
Review found the per-run-directory test green with the mechanism removed:
it asserted `tempfile.mkdtemp`'s contract, not this suite's. It now drives
`scope_devpod_to_this_run` itself, twice, so it goes red when the per-run
directory becomes a fixed path -- the regression it exists to catch. That
leaves `make_scoped_devpod_home` with no caller of its own, so it is inlined
back into the one line below it.

The e2e provider fixture is the suite's one unconditional write to a devpod
home, and `--use` rewrites the default provider. It now asserts DEVPOD_HOME
is set before writing, so a regression in the scoping cannot turn it into the
thing that reaches into the developer's real ~/.devpod, and it raises the
install failure instead of capturing the stderr and discarding it.

`DEVPOD_SSH_CONFIG` is registered on `devpod up` alone (measured on devpod
v0.26.1: `ssh`, `delete` and `stop` do not take `--ssh-config`), so the test
is renamed to claim only what the variable delivers.

Also: the e2e isolation assertion skips where there is no real ~/.devpod to
be disjoint from rather than passing vacuously; the two rival devpod
availability checks collapse into one in `fixtures/e2e_helpers`; the
DEVPOD_*_VAR constants are used by their cross-module consumer instead of
bare literals; and the module moves out of `test/fixtures/`, which is for
modules that define fixtures.
The rationale is about containers a run created, but most runs are unit runs
that never spawn devpod and leave an empty directory; and the /tmp reaping it
leans on is the orphaning event it is avoiding, deferred rather than removed.
@blooop

blooop commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

This was generated by AI during review follow-up.

Addressed on f8c551a+615df48. No devlaunch/ source file is touched; the seam is unchanged, which matches the verdict that nothing here argued for a different design.

1 (blocking) — test_each_run_gets_a_devpod_home_of_its_own did not test this PR's code

Rewritten to drive scope_devpod_to_this_run itself, twice. Deleting it was the other legitimate answer, and I did not take it: per-run uniqueness is the load-bearing half of #103's second objective (concurrent runs force-deleting each other's workspaces via the hardcoded e2e-test-* ids), and that objective is claimed as subsumed by this seam rather than deferred. A subsumption claim with no test behind it is the same defect one level up. So the test stays, but it now exercises the function whose behaviour the claim is about.

tempfile.tempdir (public API) sends the two throwaway homes under tmp_path; monkeypatch.setenv restores the session's real scoping, since the function under test writes to os.environ by design.

Both red states reproduced on this host, then restored. Mutation A — # scope_devpod_to_this_run() in pytest_configure:

FAILED test_devpod_commands_are_pointed_away_from_the_developers_devpod_home
FAILED test_devpod_up_is_pointed_away_from_the_developers_ssh_config
E       AssertionError: the suite must set DEVPOD_HOME; without it devpod reads ~/.devpod
E       assert None
========================= 2 failed, 1 passed in 0.23s ==========================

Mutation B — the per-run mkdtemp replaced by Path(tempfile.gettempdir()) / "devlaunch-testrun", i.e. exactly the "simplified into a fixed path" regression:

>       assert first != second
E       AssertionError: assert PosixPath('/tmp/pytest-of-ags/pytest-43/test_each_run_gets_a_devpod_ho0/devlaunch-testrun')
                            != PosixPath('/tmp/pytest-of-ags/pytest-43/test_each_run_gets_a_devpod_ho0/devlaunch-testrun')
========================= 1 failed, 2 passed in 0.21s ==========================

The old version stayed green under both. The new one is red under B, which is the mutation it exists to catch.

Middle Man: gone. With no test calling it, make_scoped_devpod_home had no caller but the line below it, so it is inlined back into that line.

2 (should-fix) — the provider fixture wrote to whatever devpod home was live

assert os.environ.get(DEVPOD_HOME_VAR) is now the first statement in the fixture, before anything runs. Proved on metal too, since a guard nobody has seen fire is a guard nobody has tested — under mutation A, with the e2e node id run for real:

E       AssertionError: refusing to add a devpod provider: DEVPOD_HOME is unset,
        so `--use` would rewrite the default provider in the developer's real ~/.devpod
E       assert None
ERROR test/e2e/test_full_workflow.py::TestSuiteIsolationE2E::test_devpod_in_this_session_cannot_see_the_developers_workspaces

Nothing was written: this host's ~/.devpod tree is byte-identical (md5sum over every file, before and after the whole session) and all six DevPod blocks in ~/.ssh/config are unchanged. The three real workspaces — devlaunch-main-zovomobo, devlaunch-t1-vebilote, pythontemplate — are still there.

3 (should-fix) — swallowed precondition failure

text=True and pytest.fail(...) naming the devpod home and echoing result.stderr. check=True would have raised CalledProcessError with the stderr still buried in an attribute; pytest.fail puts it in the report, which is where someone reading a wall of unrelated e2e failures needs it.

4 (should-fix) — DEVPOD_SSH_CONFIG is subcommand-scoped

The test is renamed test_devpod_up_is_pointed_away_from_the_developers_ssh_config and its docstring now states the limit explicitly. Re-measured here rather than taken from the review, on devpod v0.26.1:

up       ssh-config:1
ssh      ssh-config:0
delete   ssh-config:0
stop     ssh-config:0
devpod delete --help | grep -c -- '--devpod-home'   ->  1

So DEVPOD_HOME is persistent-root and covers everything; DEVPOD_SSH_CONFIG covers the one subcommand that writes. The qualifier is carried into the module docstring and into the PR description's item 2, which is where the map answer was stated without it. No attempt to extend the variable's reach — that is devpod's behaviour, not ours.

Nits

  • Vacuous e2e assertion — the test now skips when real_devpod_workspace_ids() is empty, which covers both a missing ~/.devpod and an empty one. Still passes on this host in 12.5s (run by node id; the full e2e suite was not run, same reason as before).
  • Two rival availability checks — collapsed. devpod_available() moved to fixtures/e2e_helpers.py and both call sites import it; the shutil.which variant is gone.
  • Unused *_VAR constants — the cross-module consumer now imports and uses them instead of bare literals, in all three unit tests and the e2e fixture.
  • test/fixtures/ module that defines no fixture — moved to test/devpod_scoping.py. test/ is already on sys.path for the whole suite, so the import is a rename and nothing else.
  • Non-cleanup rationale broader than its own justification — both caveats named in the docstring: most runs are unit runs that leave an empty directory, and the reaping the rationale leans on is the orphaning event it is avoiding, deferred rather than removed.

Deliberately not changed

  • The seam. Unchanged, per the verdict.
  • Production dl --purge. Still out of scope; the finding stands on The e2e suite can delete a developer's real devpod workspaces #103 for the map to graduate.
  • The e2e-test-purge image-tag hash residual. Left unverified. Checking it means building images on a Docker daemon another effort is using right now, and it is not destructive of workspaces either way — the namespace fix is what the ticket claimed, and that part is now tested.
  • CLAUDE.md's scratch-XDG paragraph. Correctly identified as now known-incomplete, but it is a production-workflow doc fix, not this ticket; worth its own follow-up.

pixi run test → 811 passed, 9 deselected. pixi run lint → clean, pylint 10.00/10. pixi run prek → all hooks pass, no working-tree drift.

@blooop

blooop commented Aug 8, 2026

Copy link
Copy Markdown
Owner Author

This was generated by AI during review verification.

Focused verification pass on 615df48. Every claim below was re-measured on this host independently; the author's transcripts were not taken on trust. Nothing in the working tree or on the host was left changed — worktree clean at 615df48, grep -rn MUTATION test/ clean.

Independent mutation transcripts

Baselinetest/unit/test_devpod_scoping.py3 passed in 0.15s.

Mutation Ascope_devpod_to_this_run() commented out in pytest_configure:

FAILED test_devpod_commands_are_pointed_away_from_the_developers_devpod_home
FAILED test_devpod_up_is_pointed_away_from_the_developers_ssh_config
E   AssertionError: the suite must set DEVPOD_SSH_CONFIG; without it `devpod up` edits ~/.ssh/config
E   assert None
2 failed, 1 passed in 0.18s

Mutation B — per-run mkdtemp replaced with Path(tempfile.gettempdir()) / "devlaunch-testrun":

>       assert first != second
E       AssertionError: assert PosixPath('/tmp/pytest-of-ags/pytest-57/test_each_run_gets_a_devpod_ho0/devlaunch-testrun')
                            != PosixPath('/tmp/pytest-of-ags/pytest-57/test_each_run_gets_a_devpod_ho0/devlaunch-testrun')
1 failed, 2 passed in 0.18s

Mutation C (mine, not the author's) — a sneakier shape: keep the per-run directory unique but write a fixed DEVPOD_HOME. This is the mutation that would slip past a test asserting only on the return value:

>       assert first_env != second_env
E       AssertionError: assert '/tmp/devlaunch-fixed' != '/tmp/devlaunch-fixed'
1 failed, 2 passed in 0.18s

So the second assertion is load-bearing, not decoration, and tests 1–2 correctly stay green under B and C — they are about location, not uniqueness. Each mutation restored with git checkout -- and re-verified green.

Per finding

1 (was blocking) — VERIFIED, and the keep-over-delete call is right. The rewritten test drives scope_devpod_to_this_run() twice and pins both halves of what it returns: the path (mutation B) and the environment it writes (mutation C). It is no longer restating mkdtemp's contract, and I could not find an incidental reason it passes. I also checked the failure mode its predecessor had — order dependence, since pytest-randomly is active by default and this is the one test in the file that mutates os.environ. It is clean: running the three node ids with the uniqueness test first passes, and five random-ordered runs of the file pass. monkeypatch.setenv genuinely restores the session's real scoping. On keep-vs-delete: the subsumption argument holds up. #103's second objective is claimed as subsumed, and the thing doing the subsuming is per-run namespace freshness — this is the only test that pins it, so deleting it would have left a claim with nothing behind it. make_scoped_devpod_home is gone (grep across the tree returns nothing).

2 — VERIFIED, guard proven to fire before any write. Rather than infer it from statement order, I imported the fixture with DEVPOD_HOME popped from the environment and subprocess.run replaced by a function that raises loudly if reached. The guard fired first: AssertionError: refusing to add a devpod provider: DEVPOD_HOME is unset, so --use would rewrite the default provider in the developer's real ~/.devpod. subprocess.run was never reached.

3 — VERIFIED, and pytest.fail behaves sanely in a session fixture. Checked against a synthetic session-scoped autouse fixture: pytest reports ERROR at setup per test with the message body — including the embedded stderr lines — rendered in the report. This is the property check=True would not have given, so the choice is justified. One cosmetic consequence worth knowing: the failure repeats once per collected test rather than once per session.

4 — VERIFIED, re-measured here rather than read off the reply. devpod v0.26.1 on this host:

up       ssh-config:1 devpod-home:1
ssh      ssh-config:0 devpod-home:1
delete   ssh-config:0 devpod-home:1
stop     ssh-config:0 devpod-home:1

Matches the docstring and the renamed test exactly. The qualifier is carried in three places, all checked: the module docstring, the test docstring, and — the part the review actually asked for — the map answer on #93, which now reads up: 1, ssh: 0, delete: 0, stop: 0 instead of the unqualified claim.

5 (nits) — all four VERIFIED by inspection, not by the list. Vacuous assertion now pytest.skips on an empty real_devpod_workspace_ids(). One devpod_available() remains in the tree (test/fixtures/e2e_helpers.py:15); no shutil.which variant survives; all seven call sites import it. DEVPOD_HOME_VAR/DEVPOD_SSH_CONFIG_VAR are imported and used by both cross-module consumers — no bare literals left. test/fixtures/devpod_scoping.py is gone; test/devpod_scoping.py imports cleanly and is not itself collected (it matches neither test_*.py nor *_test.py).

Did the fixes introduce anything new

Nothing blocking. Import surface is intact — 820 tests collect with no errors, the default suite is 811 passed, 9 deselected, and lint is clean at pylint 10.00/10. Two things worth naming, neither worth holding the PR for:

  • monkeypatch.setattr(tempfile, "tempdir", str(tmp_path)) contains the two throwaway homes only because scope_devpod_to_this_run calls mkdtemp() with no dir=. If that ever gains an explicit dir=, the test keeps passing but starts leaking two real directories into /tmp per run. Silent, not incorrect.
  • test/devpod_scoping.py now occupies a fairly generic top-level name in the suite's import namespace, since test/ is on sys.path. No collision today; it is the cost of the move that fixed nit 4, and the move was the right call.

PR description

The false claim is gone. The table row for test_each_run_gets_a_devpod_home_of_its_own is now true — mutation B is exactly the regression it names, and it goes red. The description also states the history rather than quietly editing it.

Host state — before and after this pass

before after
~/.ssh/config md5 6cf324ea9be3bfb569e7942d0f07ca0b 6cf324ea9be3bfb569e7942d0f07ca0b
~/.devpod (md5 over every file) ff044f9b0dab73de0546c812a8307d87 ff044f9b0dab73de0546c812a8307d87
devpod list devlaunch-main-zovomobo, devlaunch-t1-vebilote, pythontemplate identical

The e2e isolation test was run by node id (1 passed in 1.88s) with the real session fixture active — including its devpod provider add docker --use — and the developer's devpod tree came out byte-identical. The full e2e suite was not run, and no devpod delete was issued.

Verdict

Approve. All five findings are genuinely addressed, and the one that was blocking is addressed in the harder of the two available ways — the rewritten test now fails under three separate mutations of the behavior it claims to pin, including one the author did not try. CI green on all nine checks at 615df48.

@blooop
blooop merged commit 1e321b2 into main Aug 8, 2026
9 checks passed
@blooop
blooop deleted the wayfinder/devlaunch-103 branch August 8, 2026 10:51
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.
blooop added a commit that referenced this pull request Aug 8, 2026
`test-e2e` carried `depends-on = ["dev-add-docker"]`, ported from #69 where it
predated #106. The suite repoints DEVPOD_HOME at a fresh mkdtemp in
pytest_configure, before collection, so the provider that edge registers lands
in the ambient ~/.devpod -- a namespace the run never reads. It bought the suite
nothing and wrote to the developer's own devpod home as a side effect of running
tests, which is the class of thing #106 existed to stop.
test/e2e/conftest.py's session-scoped autouse fixture is what actually covers
the e2e path, by installing the provider into the run's own home.

Three more corrections that came out of the same review:

- The addopts comment said the suite "creates and deletes workspaces in
  whichever devpod namespace it can reach". Untrue post-#106: it always scopes
  its own, unconditionally, before collection.
- The marker prose lived twice, in `[tool.pytest.ini_options] markers` and in
  addinivalue_line calls that duplicate it wholesale. Deleting the copies
  removes the second sentence rather than keeping it in sync; --strict-markers
  still collects 22 e2e tests.
- strip_jsonc_comments rejects trailing commas, which the spec allows, so
  "a devcontainer runtime has to be able to parse this file" overclaimed. The
  docstring now says what the helper is and names the stricter-than-runtime
  trade; the test is renamed after what it asserts.

And postCreateCommand now says what its third link costs: an unreadable
provider listing fails container creation, deliberately, because the
alternative is a container that looks fine and fails on the first dl command.
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 e2e suite can delete a developer's real devpod workspaces

1 participant