Skip to content

test(compose): lock the behaviors of the start path now carried by the plan engine - #14403

Open
ndeloof wants to merge 8 commits into
docker:mainfrom
ndeloof:epic-14081-behavior-tests
Open

ndeloof wants to merge 8 commits into
docker:mainfrom
ndeloof:epic-14081-behavior-tests

Conversation

@ndeloof

@ndeloof ndeloof commented Oct 27, 2026

Copy link
Copy Markdown
Contributor

What this PR does, in one sentence

Locks, with tests only, the behaviors of the old imperative start path that the plan engine now carries, so that #14081 can be closed.

Context

The convergence of the start phase into the plan engine (#14081) replaced the imperative start path step by step. Each step was checked against the old behavior, but mostly by hand and by mutation testing in the PRs themselves. Mutating the production code afterwards showed that many of those behaviors were not pinned by any test: of 167 mutations applied to pkg/compose, 61 survived the test suite of main.

What the PR brings

  • Unit tests for the behaviors that survived: the Start phase's plan shape, the depends_on gate and its health/exit conditions, the executor's guarantees, compose start, the monitor's scope, interactive and detached up, hooks, restart, and the wiring of scale, watch rebuild and run.
  • The five mutations left over from feat(up): interactive up runs on a single plan #14305 are now caught: the running events, Start.Services in the monitor, the tolerance of the Start phase's error once terminated, the cleanup when the daemon time cannot be read, and signal.Notify ordering.
  • With these tests, 161 of the 167 mutations are caught.
  • scale keeps restarting every stopped container of the project, as it does today; a test pins that current behavior, and the comment says that changing it is a decision of its own.

Guardrails: no production code changes, tests are deterministic (synctest for timers, no sleeps), and the new ones were run with -race and shuffling.

The six mutations still alive are the failure-path cleanups of the interactive up (attach or daemon-time failure, which the deferred cancel makes unobservable), the precedence of a dependency error over "no container to start", and wait nodes being reported as aborted. Observing them would need production hooks, which is out of scope here.

Closes #14081

🤖 Generated with Claude Code

The monitor ends with the application it follows: pin that its initial
listing and its event subscription are scoped to the project's own
containers (no one-offs, no hook runners without a config hash), and that a
provider's relay, which never terminates, is not waited for, whether it is
there at the start or created later.

Part of docker#14081.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
… order

Pin what compose start does around its plan: the --wait verification (ready
or not, timeout message, one-shot services satisfied by their completion),
the project rebuilt from the project's non-one-off containers when no
compose file is given, only the selected services started, and the replicas
started in numeric order.

Part of docker#14081.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Pin which dependencies are worth a wait (shouldWaitForDependency), how each
container state reads for service_healthy and
service_completed_successfully (required and optional, dry-run), the
Waiting/Healthy rows of a wait outside a plan, and that the end of a wait's
context wins over a ready tick. Add the plan shapes for a wait following its
dependency's start, a project of scale: 0 services, and the Start phase
covering every service of the project (the current behavior of scale, whose
change would be a decision of its own).

Part of docker#14081.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Pin that every ContainerStart holds startMx, that the start, pre_start,
post_start and hook-runner nodes do nothing once the context is canceled,
that a failure relays untouched down a replica's chain, that the node
bookkeeping records cancellations as failures, that the phase returns the
originating error, that an invalid plan is refused, that a concurrency cap
cannot deadlock the node goroutines, that Starting is reported when the
container starts (not during pre_start), that the hook runner targets the
lowest-numbered replica, and that stop timeouts reach the daemon in whole
seconds.

Part of docker#14081.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Extend the interactive-up daemon fake and pin the behaviors mutation checks
of the interactive-up switchover found unlocked: the monitor's Services
scope, a Start-phase error while terminated, the cleanup when attaching
fails, signal handling starting only after the Create phase, late starters
stopped after the teardown sweep, hook logs reaching the session, the wait
timeout translation, --exit-code-from and the Running events. Add the
wiring of detached up (with its --wait verification), scale, the rebuild of
watch and the dependencies of run onto the plan.

Part of docker#14081.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Pin that restart waits for a restart:true dependency to hold its condition
again before restarting its dependent, and that its timeout reaches the
daemon in whole seconds.

Part of docker#14081.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
Close the started channel before the start event is emitted, so that a late
starter stopped in reaction to it always finds the container started, and
close both ends of the pipe the hook logs test hands to the session.

Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
@ndeloof
ndeloof requested review from a team as code owners October 27, 2026 13:59

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assessment: 🟡 NEEDS ATTENTION

Lower-confidence findings (not posted inline)

  • [low] pkg/compose/executor_start_test.go:544 — bareStartNode helper function is missing t.Helper() (confidence: weak 31/100)

Comment thread pkg/compose/up_phases_test.go Outdated
Signed-off-by: Nicolas De Loof <nicolas.deloof@gmail.com>
@ndeloof
ndeloof requested a review from docker-agent October 27, 2026 14:10

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Assessment: 🟡 NEEDS ATTENTION

Comment thread pkg/compose/up_phases_test.go

@docker-agent docker-agent left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This review covers only the commits since 09d7231bfdd7.

Assessment: 🟢 APPROVE

The single-hunk change to pkg/compose/up_phases_test.go is correct and complete. It resolves the open review thread (PRRT_kwDOAOWUd86wuKdX) about the goroutine potentially calling t.Error after test completion:

  • Buffered channel of capacity 1 (make(chan error, 1)) guarantees the goroutine can always send without blocking, even if the test goroutine has already exited via runtime.Goexit() (timeout path).
  • Goroutine sends exactly once unconditionally — writeErr <- writeStdcopyFrame(...) — so <-writeErr in the test goroutine never blocks forever on the happy path.
  • Timeout path is safe: t.Fatalf calls runtime.Goexit(), which skips the deferred <-writeErr receive; the buffered channel absorbs the goroutine's eventual send with no leak and no panic.
  • Normal path: assert.NilError(t, <-writeErr) is reached after runInteractiveUp returns, correctly propagating any write failure back to the test goroutine.

No new bugs introduced.

@codecov

codecov Bot commented Oct 27, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

This branch has not been deployed

No deployments
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.

Epic: converge the start phase into the plan engine

2 participants