|
| 1 | +import * as Command from "@effect/platform/Command" |
| 2 | +import * as CommandExecutor from "@effect/platform/CommandExecutor" |
| 3 | +import * as FileSystem from "@effect/platform/FileSystem" |
| 4 | +import * as Path from "@effect/platform/Path" |
| 5 | +import { describe, expect, it } from "@effect/vitest" |
| 6 | +import { Effect, Layer } from "effect" |
| 7 | +import * as Inspectable from "effect/Inspectable" |
| 8 | +import * as Sink from "effect/Sink" |
| 9 | +import * as Stream from "effect/Stream" |
| 10 | + |
| 11 | +import { inspectControllerImageRevision } from "../../src/docker-git/controller-image-revision.js" |
| 12 | + |
| 13 | +type TestCommandResult = { |
| 14 | + readonly exitCode: number |
| 15 | + readonly stderr: string |
| 16 | + readonly stdout: string |
| 17 | +} |
| 18 | + |
| 19 | +const emptyCommandResult: TestCommandResult = { |
| 20 | + exitCode: 0, |
| 21 | + stderr: "", |
| 22 | + stdout: "" |
| 23 | +} |
| 24 | + |
| 25 | +const encodeText = (value: string): Uint8Array => new TextEncoder().encode(value) |
| 26 | + |
| 27 | +const textStream = (value: string) => value.length === 0 ? Stream.empty : Stream.succeed(encodeText(value)) |
| 28 | + |
| 29 | +/** |
| 30 | + * Builds a completed process for controller image revision shell tests. |
| 31 | + * |
| 32 | + * @param result - Command result emitted by the fake process. |
| 33 | + * @returns A completed Effect platform process. |
| 34 | + * @pure true |
| 35 | + * @effect none |
| 36 | + * @invariant The process is already stopped and its exit code is deterministic. |
| 37 | + * @precondition `result.stdout` and `result.stderr` are finite strings. |
| 38 | + * @postcondition Consumers observe exactly the provided stdout, stderr, and exit code. |
| 39 | + * @complexity O(n) time and O(n) space where n = |stdout| + |stderr|. |
| 40 | + * @throws Never |
| 41 | + */ |
| 42 | +// CHANGE: model Docker CLI process output without touching the host Docker daemon |
| 43 | +// WHY: image revision fallback invariants must be unit-testable without external services |
| 44 | +// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать" |
| 45 | +// REF: CodeRabbit PR #344 review 4349211730 |
| 46 | +// SOURCE: n/a |
| 47 | +// FORMAT THEOREM: process(result).stdout = result.stdout and process(result).exit = result.exitCode |
| 48 | +// PURITY: CORE |
| 49 | +// EFFECT: none |
| 50 | +// INVARIANT: fake process is not running after construction |
| 51 | +// COMPLEXITY: O(n) |
| 52 | +const completedProcess = (result: TestCommandResult): CommandExecutor.Process => ({ |
| 53 | + [CommandExecutor.ProcessTypeId]: CommandExecutor.ProcessTypeId, |
| 54 | + [Inspectable.NodeInspectSymbol]: () => ({ _tag: "TestProcess" }), |
| 55 | + exitCode: Effect.succeed(CommandExecutor.ExitCode(result.exitCode)), |
| 56 | + isRunning: Effect.succeed(false), |
| 57 | + kill: () => Effect.void, |
| 58 | + pid: CommandExecutor.ProcessId(0), |
| 59 | + stderr: textStream(result.stderr), |
| 60 | + stdin: Sink.drain, |
| 61 | + stdout: textStream(result.stdout), |
| 62 | + toJSON: () => ({ _tag: "TestProcess" }), |
| 63 | + toString: () => "TestProcess" |
| 64 | +}) |
| 65 | + |
| 66 | +type TestCommandHandler = (command: Command.StandardCommand) => TestCommandResult |
| 67 | + |
| 68 | +/** |
| 69 | + * Creates a command-executor layer backed by a pure command handler. |
| 70 | + * |
| 71 | + * @param handler - Total handler for standard commands. |
| 72 | + * @returns Layer providing CommandExecutor. |
| 73 | + * @pure true |
| 74 | + * @effect none |
| 75 | + * @invariant Every started command maps to exactly one completed fake process. |
| 76 | + * @precondition The handler is total for all commands issued by the test subject. |
| 77 | + * @postcondition Command effects never reach the real operating system. |
| 78 | + * @complexity O(1) layer construction. |
| 79 | + * @throws Never |
| 80 | + */ |
| 81 | +// CHANGE: provide typed Effect dependency injection for Docker command tests |
| 82 | +// WHY: controller image revision inspection is a shell effect and must be tested through its service boundary |
| 83 | +// QUOTE(ТЗ): "комментарии ребита надо было тоже поддержать" |
| 84 | +// REF: CodeRabbit PR #344 review 4349211730 |
| 85 | +// SOURCE: n/a |
| 86 | +// FORMAT THEOREM: start(command) = completedProcess(handler(command)) |
| 87 | +// PURITY: SHELL |
| 88 | +// EFFECT: Layer<CommandExecutor> |
| 89 | +// INVARIANT: no command escapes the fake executor |
| 90 | +// COMPLEXITY: O(1) |
| 91 | +const commandExecutorLayer = (handler: TestCommandHandler) => |
| 92 | + Layer.succeed( |
| 93 | + CommandExecutor.CommandExecutor, |
| 94 | + CommandExecutor.makeExecutor((command) => { |
| 95 | + const standardCommand = Command.flatten(command)[0] |
| 96 | + return Effect.succeed(completedProcess(handler(standardCommand))) |
| 97 | + }) |
| 98 | + ) |
| 99 | + |
| 100 | +describe("controller image revision", () => { |
| 101 | + it.effect("falls back to null when compose image resolution is ambiguous", () => |
| 102 | + Effect.gen(function*(_) { |
| 103 | + const revision = yield* _( |
| 104 | + inspectControllerImageRevision().pipe( |
| 105 | + Effect.provide( |
| 106 | + commandExecutorLayer((command) => |
| 107 | + command.command === "docker" && command.args.includes("--images") |
| 108 | + ? { exitCode: 0, stderr: "", stdout: "app-api:latest\nanother-image:latest\n" } |
| 109 | + : emptyCommandResult |
| 110 | + ) |
| 111 | + ), |
| 112 | + Effect.provide(FileSystem.layerNoop({})), |
| 113 | + Effect.provide(Path.layer) |
| 114 | + ) |
| 115 | + ) |
| 116 | + |
| 117 | + expect(revision).toBeNull() |
| 118 | + })) |
| 119 | +}) |
0 commit comments