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 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..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 @@ -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,26 @@ describe("DockerComposeEnvironment", { timeout: 180_000 }, () => { await checkEnvironmentContainerIsHealthy(startedEnvironment, await composeContainerName("container")); }); + 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"); + + await using startedEnvironment = await new DockerComposeEnvironment(fixtures, "docker-compose.yml") + .withWaitStrategy(unmatchedWaitStrategyName, 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: "${unmatchedWaitStrategyName}"` + ) + ) + ).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.` + ); + } + } }