fix(daemon): require every Compose replica to be healthy (#218)

Signed-off-by: Rudy Celekli <[email protected]>
This commit is contained in:
Rudy Mizrahi Celekli authored and GitHub committed 2026-09-30 20:45:17 -07:00
1 parent b4a05d279a
commit ed9dc62f5a
2 files changed
+32 -2

No files matched your search

@@ -44,8 +44,12 @@ export async function evaluateWaitTargets(
detail: ok ? null : `TCP probe failed: ${target.tcp}`,
});
} else if (target.condition === "healthy" && target.service) {
// Compose health check — look at compose ps output
const svc = composeStatuses?.find((s) => s.name === target.service);
// Every replica must be healthy. An unhealthy replica takes priority
// over a starting replica, regardless of Compose's row order.
const replicas = composeStatuses?.filter((s) => s.name === target.service) ?? [];
const svc = replicas.find((s) => s.health !== "healthy" && s.health !== "starting")
?? replicas.find((s) => s.health === "starting")
?? replicas[0];
if (!svc) {
results.push({ target, status: "unhealthy", detail: `Service '${target.service}' not found in compose status` });
} else if (svc.health === "healthy") {
@@ -0,0 +1,26 @@
import { describe, expect, it } from "vitest";
import { ComposeServicesAdapter } from "../src/adapters/compose-services-adapter.js";
import { evaluateWaitTargets } from "../src/domain/services-readiness.js";
describe("Compose replica readiness", () => {
it.each([
[["healthy", "healthy"], "healthy"],
[["healthy", "unhealthy"], "unhealthy"],
[["unhealthy", "healthy"], "unhealthy"],
[["healthy", "starting"], "pending"],
[["starting", "healthy"], "pending"],
[["starting", "unhealthy"], "unhealthy"],
[["healthy", ""], "unhealthy"],
[[], "unhealthy"],
] as const)("aggregates replicas %j as %s", async (healths, expected) => {
// The adapter retains one status per Compose ps JSON row, including replicas.
const output = healths.map((Health, index) => JSON.stringify({
Service: "fixture", Name: `owned-fixture-${index + 1}`, State: "running", Status: "Up", Health,
})).join("\n");
const adapter = new ComposeServicesAdapter(async () => output);
const status = await adapter.status({ composeFile: "/owned/compose.yaml", projectName: "owned" });
const result = await evaluateWaitTargets([{ service: "fixture", condition: "healthy" }], adapter, status.services);
expect(result[0]!.status).toBe(expected);
if (expected === "unhealthy" && healths.length === 0) expect(result[0]!.detail).toContain("not found");
});
});