|
1 | 1 | import assert from "node:assert/strict" |
| 2 | +import { spawnSync } from "node:child_process" |
2 | 3 | import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises" |
3 | 4 | import { createServer } from "node:net" |
4 | 5 | import { tmpdir } from "node:os" |
5 | 6 | import { join } from "node:path" |
6 | 7 | import { runRecipeBuildCommand } from "../packages/cli/src/commands/recipe-build.ts" |
7 | | -import { parseLoopbackPort, provisionRuntimeServices, provisionRuntimeServicesForRecipe, RuntimeServiceProvisionError, runtimeServiceEvidenceFromError, runtimeServicePlan, waitForMysqlProtocol, type RuntimeServiceDependencies } from "../packages/cli/src/runtime-services.ts" |
| 8 | +import { executeRuntimeServiceProcess, parseLoopbackPort, provisionRuntimeServices, provisionRuntimeServicesForRecipe, RuntimeServiceProvisionError, runtimeServiceEvidenceFromError, runtimeServicePlan, waitForMysqlProtocol, type RuntimeServiceDependencies } from "../packages/cli/src/runtime-services.ts" |
8 | 9 | import { planWorkspaceRecipe } from "../packages/cli/src/recipe-dry-run.ts" |
9 | 10 | import { validateWorkspaceRecipeSemantics } from "../packages/cli/src/recipe-validation.ts" |
10 | 11 | import { buildWordPressPhpunitRecipe } from "../packages/runtime-core/src/recipe-builders.ts" |
@@ -139,6 +140,67 @@ await provisioned.release() |
139 | 140 | await provisioned.release() |
140 | 141 | assert.equal(calls.filter((call) => call.args[0] === "rm").length, 1, "release is idempotent") |
141 | 142 |
|
| 143 | +const missingProviderDependencies: RuntimeServiceDependencies = { |
| 144 | + ...dependencies, |
| 145 | + execute: async (_command, args, options) => await executeRuntimeServiceProcess("wp-codebox-provider-command-that-does-not-exist", args, options), |
| 146 | +} |
| 147 | +await assert.rejects(provisionRuntimeServices([service], { dependencies: missingProviderDependencies }), (error: unknown) => { |
| 148 | + assert.ok(error instanceof RuntimeServiceProvisionError) |
| 149 | + assert.deepEqual(error.evidence[0]?.diagnostic, { |
| 150 | + code: "provider-unavailable", |
| 151 | + command: "docker", |
| 152 | + cause: { code: "ENOENT", message: "Provider command executable was not found" }, |
| 153 | + }) |
| 154 | + return true |
| 155 | +}) |
| 156 | +const missingProviderChild = spawnSync(process.execPath, ["--import", "tsx", "--input-type=module", "-e", ` |
| 157 | + import { executeRuntimeServiceProcess } from "./packages/cli/src/runtime-services.ts"; |
| 158 | + await executeRuntimeServiceProcess("wp-codebox-provider-command-that-does-not-exist", [], { timeout: 300_000 }).catch(() => undefined); |
| 159 | +`], { cwd: process.cwd(), encoding: "utf8", timeout: 5_000 }) |
| 160 | +assert.equal(missingProviderChild.error, undefined, `spawn ENOENT child exits without retaining its process timeout: ${missingProviderChild.error?.message ?? ""}`) |
| 161 | +assert.equal(missingProviderChild.status, 0, missingProviderChild.stderr) |
| 162 | + |
| 163 | +const failedStartDependencies: RuntimeServiceDependencies = { |
| 164 | + ...dependencies, |
| 165 | + async execute(command, args, options) { |
| 166 | + if (args[0] === "run") throw new Error("provider start failed") |
| 167 | + return dependencies.execute(command, args, options) |
| 168 | + }, |
| 169 | +} |
| 170 | +await assert.rejects(provisionRuntimeServices([service], { dependencies: failedStartDependencies }), (error: unknown) => error instanceof RuntimeServiceProvisionError && error.evidence[0]?.diagnostic?.code === "provision-failed") |
| 171 | + |
| 172 | +const failedReadinessDependencies: RuntimeServiceDependencies = { |
| 173 | + ...dependencies, |
| 174 | + async waitForReady() { throw new Error("provider readiness failed") }, |
| 175 | +} |
| 176 | +await assert.rejects(provisionRuntimeServices([service], { dependencies: failedReadinessDependencies }), (error: unknown) => error instanceof RuntimeServiceProvisionError && error.evidence[0]?.diagnostic?.code === "readiness-failed") |
| 177 | + |
| 178 | +const unavailableDuringReadinessDependencies: RuntimeServiceDependencies = { |
| 179 | + ...dependencies, |
| 180 | + async execute(command, args, options) { |
| 181 | + if (args[0] === "exec" || args[0] === "rm") return await executeRuntimeServiceProcess("wp-codebox-provider-command-that-does-not-exist", args, options) |
| 182 | + return dependencies.execute(command, args, options) |
| 183 | + }, |
| 184 | +} |
| 185 | +await assert.rejects(provisionRuntimeServices([service], { dependencies: unavailableDuringReadinessDependencies }), (error: unknown) => { |
| 186 | + assert.ok(error instanceof RuntimeServiceProvisionError) |
| 187 | + assert.equal(error.evidence[0]?.diagnostic?.code, "provider-unavailable", "cleanup failure does not replace the primary provider failure") |
| 188 | + assert.equal(error.evidence[0]?.teardown, "failed", "failed cleanup remains visible in service evidence") |
| 189 | + return true |
| 190 | +}) |
| 191 | + |
| 192 | +let ordinaryReadinessAttempts = 0 |
| 193 | +const retryingReadinessDependencies: RuntimeServiceDependencies = { |
| 194 | + ...dependencies, |
| 195 | + async execute(command, args, options) { |
| 196 | + if (args[0] === "exec" && ordinaryReadinessAttempts++ === 0) throw new Error("provider readiness command failed") |
| 197 | + return dependencies.execute(command, args, options) |
| 198 | + }, |
| 199 | +} |
| 200 | +const retriedReadiness = await provisionRuntimeServices([service], { dependencies: retryingReadinessDependencies }) |
| 201 | +assert.equal(ordinaryReadinessAttempts, 2, "ordinary Docker readiness failures remain retryable") |
| 202 | +await retriedReadiness.release() |
| 203 | + |
142 | 204 | const emptyRootCalls: Array<{ args: string[]; env?: NodeJS.ProcessEnv }> = [] |
143 | 205 | const emptyRootDependencies: RuntimeServiceDependencies = { |
144 | 206 | ...dependencies, |
|
0 commit comments