From 23f91df0850e5d90eea9a799d80b88008e626b2e Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 17 Feb 2026 13:26:28 +0000 Subject: [PATCH 1/4] Warn on unmatched compose wait strategy names --- docs/features/compose.md | 6 ++++-- .../docker-compose-environment.test.ts | 19 ++++++++++++++++++- .../docker-compose-environment.ts | 18 ++++++++++++++++++ 3 files changed, 40 insertions(+), 3 deletions(-) diff --git a/docs/features/compose.md b/docs/features/compose.md index 2f22ddef7..6d68b604d 100644 --- a/docs/features/compose.md +++ b/docs/features/compose.md @@ -29,16 +29,18 @@ Provide a list of service names to only start those services: ```js const environment = await new DockerComposeEnvironment(composeFilePath, composeFile) - .up(["redis-1", "postgres-1"]); + .up(["redis", "postgres"]); ``` ### With wait strategy +`withWaitStrategy` expects **container names**, not service names. With Docker Compose v2, the default container name for the first replica is usually `-1`. + ```js const environment = await new DockerComposeEnvironment(composeFilePath, composeFile) .withWaitStrategy("redis-1", Wait.forLogMessage("Ready to accept connections")) .withWaitStrategy("postgres-1", Wait.forHealthCheck()) - .up(); + .up(["redis", "postgres"]); ``` ### With a default wait strategy diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts index 9a077642b..a6bc8e519 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts @@ -1,5 +1,5 @@ import path from "path"; -import { RandomUuid } from "../common"; +import { log, RandomUuid } from "../common"; import { randomUuid } from "../common/uuid"; import { PullPolicy } from "../utils/pull-policy"; import { @@ -131,6 +131,23 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => { await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container")); }); + it("should warn when no started containers match configured wait strategy names", async () => { + const warnSpy = vi.spyOn(log, "warn"); + + await using startedEnvironment = await new DockerComposeEnvironment(fixtures, "docker-compose.yml") + .withWaitStrategy("container", Wait.forLogMessage("Listening on port 8080")) + .up(["container"]); + + await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container")); + + const warningMessages = warnSpy.mock.calls.map(([message]) => message); + expect( + warningMessages.some((warningMessage) => + warningMessage.includes(`No containers were started for the configured wait strategy names: "container"`) + ) + ).toBe(true); + }); + it("should support failing health check wait strategy", async () => { await expect( new DockerComposeEnvironment(fixtures, "docker-compose-with-healthcheck-unhealthy.yml") diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts index 40273b48b..3f7357148 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.ts @@ -150,6 +150,13 @@ export class DockerComposeEnvironment { ); log.info(`Started containers "${startedContainerNames.join('", "')}"`); + const startedContainerNameSet = new Set( + startedContainers.map((startedContainer) => + parseComposeContainerName(this.projectName, startedContainer.Names[0]) + ) + ); + this.warnForUnusedWaitStrategies(startedContainerNameSet); + const startedGenericContainers = ( await Promise.all( startedContainers.map(async (startedContainer) => { @@ -207,4 +214,15 @@ export class DockerComposeEnvironment { environment: this.environment, }); } + + private warnForUnusedWaitStrategies(startedContainerNames: Set): void { + const unusedWaitStrategyContainerNames = Object.keys(this.waitStrategy).filter( + (configuredContainerName) => !startedContainerNames.has(configuredContainerName) + ); + if (unusedWaitStrategyContainerNames.length > 0) { + log.warn( + `No containers were started for the configured wait strategy names: "${unusedWaitStrategyContainerNames.join('", "')}". Wait strategies are matched against container names (for example "redis-1"), not service names.` + ); + } + } } From 5f37da55c11beadd2b8af6287321954e19787f6b Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 17 Feb 2026 14:49:55 +0000 Subject: [PATCH 2/4] Stabilize compose wait strategy warning test --- .../docker-compose-environment.test.ts | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts index a6bc8e519..3fb1346dd 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts @@ -132,10 +132,11 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => { }); it("should warn when no started containers match configured wait strategy names", async () => { + const unmatchedWaitStrategyName = "non-existent-container-name"; const warnSpy = vi.spyOn(log, "warn"); await using startedEnvironment = await new DockerComposeEnvironment(fixtures, "docker-compose.yml") - .withWaitStrategy("container", Wait.forLogMessage("Listening on port 8080")) + .withWaitStrategy(unmatchedWaitStrategyName, Wait.forLogMessage("Listening on port 8080")) .up(["container"]); await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container")); @@ -143,7 +144,9 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => { const warningMessages = warnSpy.mock.calls.map(([message]) => message); expect( warningMessages.some((warningMessage) => - warningMessage.includes(`No containers were started for the configured wait strategy names: "container"`) + warningMessage.includes( + `No containers were started for the configured wait strategy names: "${unmatchedWaitStrategyName}"` + ) ) ).toBe(true); }); From b5b66771ed8ddd425af08cdb154ab4b372d40466 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 17 Feb 2026 15:55:43 +0000 Subject: [PATCH 3/4] Make compose warning assertion test sequential --- .../docker-compose-environment.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts index 3fb1346dd..5360bb633 100644 --- a/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts +++ b/packages/testcontainers/src/docker-compose-environment/docker-compose-environment.test.ts @@ -131,7 +131,7 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => { await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container")); }); - it("should warn when no started containers match configured wait strategy names", async () => { + it.sequential("should warn when no started containers match configured wait strategy names", async () => { const unmatchedWaitStrategyName = "non-existent-container-name"; const warnSpy = vi.spyOn(log, "warn"); From f76a4171982311e76a2a9abd91634dd1459e1f49 Mon Sep 17 00:00:00 2001 From: Cristian Greco Date: Tue, 17 Feb 2026 15:58:31 +0000 Subject: [PATCH 4/4] Document Vitest concurrency and sequential test guidance --- AGENTS.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index 06b0861c7..8ecbccbc4 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -12,6 +12,10 @@ It captures practical rules that prevent avoidable CI and PR churn. - If new learnings or misunderstandings are discovered, propose an `AGENTS.md` update in the same PR. - Tests should verify observable behavior changes, not only internal/config state. - Example: for a security option, assert a real secure/insecure behavior difference. +- Vitest runs tests concurrently by default (`sequence.concurrent: true` in `vitest.config.ts`). + - Tests that rely on shared/global mocks (for example `vi.spyOn` on shared loggers/singletons) can be flaky due to interleaving or automatic mock resets. + - Prefer asserting observable behavior instead of shared global mock state when possible. + - If a test must depend on shared/global mock state, use `it.sequential(...)` or `describe.sequential(...)`. ## Permission and Escalation