dev_container: Prefer canonical compose project on multi-match - #7
Merged
antont merged 3 commits intoApr 19, 2026
Merged
Conversation
Scaffolds a test for the multi-match upgrade scenario where one of the
duplicate containers lives under the canonical (reference-CLI-matching)
compose project and the other under a legacy name. The tiebreak logic
that would prefer the canonical container does not exist yet, so
`check_for_existing_container_prefers_canonical_compose_project` fails
with `MultipleMatchingContainers`. The companion safety-net test
(`check_for_existing_container_errors_when_none_canonical`) passes
already and guards against regressions in the zero-canonical case.
This commit is deliberately test-only on the logic layer; the prod-code
changes are purely structural and required to let the tests compile:
- Extend `DockerConfigLabels` with `compose_project: Option<String>`
serde-renamed to `com.docker.compose.project`. All existing inspect
struct literals (3 in docker.rs tests, 8 in devcontainer_manifest.rs
tests) get `compose_project: None`.
- Add `FakeDocker::inspect_overrides: Mutex<HashMap<String, DockerInspect>>`
and `add_inspect_override` setter. `FakeDocker::inspect` consults
overrides before its hardcoded pattern matching, so tests can control
each multi-match candidate's `com.docker.compose.project` label
without disturbing any existing test fixture.
RED output (`cargo test -p dev_container --lib check_for_existing_container`):
running 3 tests
test devcontainer_manifest::test::check_for_existing_container_errors_when_multiple_match ... ok
test devcontainer_manifest::test::check_for_existing_container_errors_when_none_canonical ... ok
test devcontainer_manifest::test::check_for_existing_container_prefers_canonical_compose_project ... FAILED
---- devcontainer_manifest::test::check_for_existing_container_prefers_canonical_compose_project stdout ----
thread 'devcontainer_manifest::test::check_for_existing_container_prefers_canonical_compose_project' panicked at crates/dev_container/src/devcontainer_manifest.rs:5043:13:
expected Ok(Some(canonical)), got Err(MultipleMatchingContainers(["canonical_id", "legacy_id"]))
test result: FAILED. 2 passed; 1 failed
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Introduce `pick_canonical_container`, a thin recovery layer above `find_process_by_filters`. When the multi-match detection from zed-industries#54068 trips, inspect each candidate and prefer the one whose `com.docker.compose.project` label equals `self.project_name()`. Zero or ≥2 canonical matches still fall through to the `MultipleMatchingContainers` error, preserving the safety net; only the unambiguous-recovery case is intercepted. This makes the compose-project-name fix from PR #6 a transparent upgrade: users migrating past v0.231.x to v0.232+ on an existing Zed-managed project had one container under the legacy `safe_id_lower(name)` project. After the derivation change new Zed creates one under the canonical `${folder}_devcontainer`. Without the tiebreak, the label-based lookup sees both and errors out; with it, Zed reuses the canonical one and logs the orphan's id so users can clean up on their own schedule. The multi-match detection itself (`parse_find_process_output`, `MultipleMatchingContainers`, its Display impl, the pass-through arm in `start_dev_container_with_config`) stays byte-identical with zed-industries#54068. Updates the pre-existing `check_for_existing_container_errors_when_multiple_match` test to supply non-canonical inspect overrides, so it still exercises the safety-net fall-through now that the path inspects each candidate. running 3 tests test devcontainer_manifest::test::check_for_existing_container_errors_when_multiple_match ... ok test devcontainer_manifest::test::check_for_existing_container_errors_when_none_canonical ... ok test devcontainer_manifest::test::check_for_existing_container_prefers_canonical_compose_project ... ok test result: ok. 78 passed; 0 failed (full crate) Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Locks in the `com.docker.compose.project` serde rename on `DockerConfigLabels`. The multi-match tiebreak in `check_for_existing_container` reads this field from real `docker inspect` output, but every tiebreak test plants the value via `FakeDocker::add_inspect_override` — nothing exercises the deserialization path. Without this test, breaking the rename fails closed silently (tiebreak sees `None`, falls through to `MultipleMatchingContainers`) and no existing test catches it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Owner
Author
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Stacking
Each PR is separately reviewable. The detection layer from zed-industries#54068 stays byte-identical (
parse_find_process_output, theMultipleMatchingContainersvariant, its Display, the pass-through arm instart_dev_container_with_config). The tiebreak is a new match arm insidecheck_for_existing_container, backed by a newpick_canonical_containerhelper.How it works
pick_canonical_containerinspects each id, partitions into{canonical, others}bycom.docker.compose.project, returnsOk(Some(canonical))on unique canonical, falls through toMultipleMatchingContainers(ids)otherwise. Log message on recovery names both the canonical id and the orphans.Prerequisite:
DockerConfigLabelsgained anOption<String> compose_projectfield serde-renamed tocom.docker.compose.project. All existing inspect struct literals updated withcompose_project: None.Test plan
cargo test -p dev_container --lib check_for_existing_container— 3 passed (safety net, tiebreak, none-canonical-error).cargo test -p dev_container --lib— 78 passed (was 76 on dev_container: Align compose project name with reference CLI and recover from mixed-version duplicates #6's head; +2 new tests)../script/clippy -p dev_container— clean.MultipleMatchingContainersoutput before the logic exists; GREEN commit adds the helper and routes through it.Live reproduction TODO (defer to approval): against
/Users/antont/src/devcontainer-compose-test, manually create a legacy-named container undercompose_duplicate_reproand a CLI-named container underdevcontainer-compose-test_devcontainer, then open in Zed — expect the canonical one reused and the orphan id named in the log.Release Notes:
Multiple dev container matcheserror when a workspace has two containers with identical dev container labels but different compose projects (e.g. one left by the devcontainer CLI and one by Zed's Rust implementation). Zed now reuses the container under the canonical${folder}_devcontainercompose project and logs the orphan; two non-canonical matches still surface the error.