Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | /** * FakeDockerManager — in-memory test double for DockerManager. * * Implements the same public API surface as the real DockerManager * (createContainer, startContainer, executeCommand, stopContainer, * removeContainer, pullImage, copyFromContainer) without touching * the real Docker daemon or the filesystem. * * Usage in tests: * * const fake = new FakeDockerManager(); * fake.setCommandResult('echo hi', { exitCode: 0, output: 'hi' }); * const id = await fake.createContainer({ image: 'node:20', ... }); * await fake.startContainer(id); * const result = await fake.executeCommand(id, 'echo hi'); * expect(result.exitCode).toBe(0); */ import type { ContainerConfig, ExecutionOptions, ExecutionResult } from '../../docker.js'; // --------------------------------------------------------------------------- // Internal state shape stored per container // --------------------------------------------------------------------------- type ContainerState = 'created' | 'running' | 'stopped' | 'removed'; interface ContainerRecord { config: ContainerConfig; state: ContainerState; id: string; } // --------------------------------------------------------------------------- // Configurable command result type // --------------------------------------------------------------------------- export interface FakeCommandResult { exitCode: number; output: string; error?: string; } // --------------------------------------------------------------------------- // FakeDockerManager // --------------------------------------------------------------------------- export class FakeDockerManager { // Map of containerId -> record private containers = new Map<string, ContainerRecord>(); // Set of images that have been pulled private pulledImages = new Set<string>(); // Per-command overrides keyed by the command string. // When a command matches a key the configured result is returned; // otherwise the default result is used. private commandOverrides = new Map<string, FakeCommandResult>(); // Default result returned by executeCommand when no override matches. private defaultCommandResult: FakeCommandResult = { exitCode: 0, output: '' }; // Track every copy operation for assertion in tests. private copyLog: Array<{ containerId: string; containerPath: string; hostPath: string }> = []; // Counter used to generate deterministic container IDs in tests. private idCounter = 0; // --------------------------------------------------------------------------- // Configuration helpers (call these from test setup) // --------------------------------------------------------------------------- /** * Override the result returned when executeCommand is called with the exact * command string. Pass undefined to remove the override. */ setCommandResult(command: string, result: FakeCommandResult | undefined): void { if (result === undefined) { this.commandOverrides.delete(command); } else { this.commandOverrides.set(command, result); } } /** * Set the result returned by executeCommand when no per-command override * matches. Defaults to { exitCode: 0, output: '' }. */ setDefaultCommandResult(result: FakeCommandResult): void { this.defaultCommandResult = result; } /** * Reset all internal state (containers, pulled images, overrides, copy log). * Call this in beforeEach to ensure test isolation. */ reset(): void { this.containers.clear(); this.pulledImages.clear(); this.commandOverrides.clear(); this.copyLog = []; this.idCounter = 0; this.defaultCommandResult = { exitCode: 0, output: '' }; } // --------------------------------------------------------------------------- // Inspection helpers (for assertions in tests) // --------------------------------------------------------------------------- /** Returns true if the container ID is known (not removed). */ hasContainer(containerId: string): boolean { return this.containers.has(containerId); } /** Returns the state of a container, or undefined if it does not exist. */ getContainerState(containerId: string): ContainerState | undefined { return this.containers.get(containerId)?.state; } /** Returns the config used to create a container, or undefined. */ getContainerConfig(containerId: string): ContainerConfig | undefined { return this.containers.get(containerId)?.config; } /** Returns all containers currently tracked (not yet removed). */ getActiveContainers(): ReadonlyMap<string, ContainerRecord> { return this.containers; } /** Returns whether an image has been pulled via pullImage(). */ isImagePulled(image: string): boolean { return this.pulledImages.has(image); } /** Returns the log of all copyFromContainer calls in call order. */ getCopyLog(): ReadonlyArray<{ containerId: string; containerPath: string; hostPath: string }> { return this.copyLog; } // --------------------------------------------------------------------------- // Public API — mirrors DockerManager method signatures exactly // --------------------------------------------------------------------------- /** * Resolves immediately. Marks the image as pulled so tests can assert * pullImage() was called. */ async pullImage(image: string): Promise<void> { this.pulledImages.add(image); } /** * Creates an in-memory container record in the 'created' state and returns * its ID. The ID follows the same "buildhive-<n>" pattern as the real * implementation (predictable in tests). */ async createContainer(containerConfig: ContainerConfig): Promise<string> { const containerId = `buildhive-fake-${++this.idCounter}`; this.containers.set(containerId, { id: containerId, config: containerConfig, state: 'created', }); return containerId; } /** * Transitions the container from 'created' to 'running'. * Throws if the container does not exist or is not in 'created' state. */ async startContainer(containerId: string): Promise<void> { const record = this.requireContainer(containerId); if (record.state !== 'created') { throw new Error( `FakeDockerManager: cannot start container ${containerId} — current state is '${record.state}'` ); } record.state = 'running'; } /** * Executes a command against a running container. * * - If a per-command override has been registered via setCommandResult() the * override result is returned. * - Otherwise the default result (exitCode: 0, output: '') is returned. * - The optional onOutput callback is invoked with the output string when * output is non-empty, mirroring the streaming behaviour of the real impl. * - Throws if the container is not in 'running' state. */ async executeCommand( containerId: string, command: string, options: ExecutionOptions = {} ): Promise<ExecutionResult> { const record = this.requireContainer(containerId); if (record.state !== 'running') { throw new Error( `FakeDockerManager: cannot execute command in container ${containerId} — current state is '${record.state}'` ); } const override = this.commandOverrides.get(command); const result: ExecutionResult = override ? { ...override } : { ...this.defaultCommandResult }; // Mirror the real impl: call onOutput if there is output to stream. if (options.onOutput && result.output) { options.onOutput(result.output); } return result; } /** * Transitions the container to 'stopped' state. * Silently succeeds if the container is already stopped (matches the real * impl's defensive behaviour). */ async stopContainer(containerId: string, _timeout?: number): Promise<void> { const record = this.requireContainer(containerId); if (record.state !== 'removed') { record.state = 'stopped'; } } /** * Stops the container (if running) and removes it from the internal map. * Does NOT throw if the container is not found — mirrors the real impl's * "don't throw from cleanup" contract. */ async removeContainer(containerId: string): Promise<void> { const record = this.containers.get(containerId); if (!record) { return; // already removed — no-op } record.state = 'removed'; this.containers.delete(containerId); } /** * Records the copy operation in the copy log so tests can assert it was * called with the correct paths. Throws if the container is not running. */ async copyFromContainer( containerId: string, containerPath: string, hostPath: string ): Promise<void> { const record = this.requireContainer(containerId); if (record.state !== 'running') { throw new Error( `FakeDockerManager: cannot copy from container ${containerId} — current state is '${record.state}'` ); } this.copyLog.push({ containerId, containerPath, hostPath }); } // --------------------------------------------------------------------------- // Private helpers // --------------------------------------------------------------------------- private requireContainer(containerId: string): ContainerRecord { const record = this.containers.get(containerId); if (!record) { throw new Error(`FakeDockerManager: container not found: ${containerId}`); } return record; } } |