import assert from 'node:assert/strict' import { chmod, mkdir, mkdtemp, readFile, rm, writeFile } from 'node:fs/promises' import { existsSync } from 'node:fs' import { delimiter, join, resolve } from 'node:path' import { tmpdir } from 'node:os' import { spawnSync } from 'node:child_process' import test from 'node:test' const root = resolve(import.meta.dirname, '..') const script = resolve(root, 'deploy/scripts/deploy-blue-green-service.sh') const shell = process.platform === 'win32' ? 'C:/Program Files/Git/bin/bash.exe' : '/bin/sh' const dockerMock = `#!/usr/bin/env sh set -eu printf '%s\\n' "$*" >> "$MOCK_LOG" command="$1" shift case "$command" in network) [ "$1" = inspect ] && { echo '{}'; exit 0; } exit 0 ;; compose) container_name='' previous='' for argument in "$@"; do if [ "$previous" = --name ]; then container_name="$argument"; fi previous="$argument" if [ "$argument" = run ]; then action=run fi done if [ "\${action:-}" = run ]; then case "$container_name" in *-worker) : > "$MOCK_WORKER_EXISTS" ;; *) : > "$MOCK_CANDIDATE_EXISTS" ;; esac exit 0 fi exit 0 ;; inspect) target='' for argument in "$@"; do target="$argument"; done case "$target" in firstleg-production-file) [ ! -f "$MOCK_LEGACY_REMOVED" ] ;; firstleg-production-file-green) [ -f "$MOCK_CANDIDATE_EXISTS" ] || exit 1 case "$*" in *State.Health*) printf '%s\\n' "\${MOCK_CANDIDATE_HEALTH:-healthy}" ;; *Config.Image*) printf '%s\\n' 'registry.local/group/file:newtag' ;; esac ;; firstleg-production-file-green-worker) [ -f "$MOCK_WORKER_EXISTS" ] || exit 1 case "$*" in *State.Health*) printf '%s\\n' healthy ;; *Config.Image*) printf '%s\\n' 'registry.local/group/file:newtag' ;; esac ;; *) exit 1 ;; esac ;; stop) target='' for argument in "$@"; do target="$argument"; done [ "$target" = firstleg-production-file ] && : > "$MOCK_LEGACY_STOPPED" ;; rm) target='' for argument in "$@"; do target="$argument"; done case "$target" in firstleg-production-file) : > "$MOCK_LEGACY_REMOVED" ;; firstleg-production-file-green) rm -f "$MOCK_CANDIDATE_EXISTS" ;; firstleg-production-file-green-worker) rm -f "$MOCK_WORKER_EXISTS" ;; esac ;; logs) echo 'candidate logs' ;; *) echo "unexpected docker command: $command $*" >&2; exit 2 ;; esac ` async function runBlueGreen(health) { const temp = await mkdtemp(join(tmpdir(), 'blue-green-runtime-')) const bin = join(temp, 'bin') const docker = join(bin, 'docker') const sleep = join(bin, 'sleep') const envFile = join(temp, 'deployment.env') const compose = join(temp, 'compose.yml') const stateRoot = join(temp, 'state') const log = join(temp, 'docker.log') await mkdir(bin) await mkdir(stateRoot) await writeFile(docker, dockerMock, 'utf8') await writeFile(sleep, '#!/usr/bin/env sh\nexit 0\n', 'utf8') await chmod(docker, 0o755) await chmod(sleep, 0o755) await writeFile(envFile, 'FILE_EXPORT_CONSUMER_ENABLED=true\n', 'utf8') await writeFile(compose, 'services: {}\n', 'utf8') await writeFile(log, '', 'utf8') const paths = { candidate: join(temp, 'candidate'), stopped: join(temp, 'legacy-stopped'), removed: join(temp, 'legacy-removed'), worker: join(temp, 'worker') } const result = spawnSync(shell, [ script.replaceAll('\\', '/'), 'production', 'registry.local/group', 'newtag', 'file', envFile.replaceAll('\\', '/'), compose.replaceAll('\\', '/'), 'firstleg-production' ], { cwd: root, encoding: 'utf8', env: { ...process.env, PATH: `${bin}${delimiter}${process.env.PATH ?? ''}`, BLUE_GREEN_STATE_ROOT: stateRoot, BLUE_GREEN_STABILIZATION_SECONDS: '0', BLUE_GREEN_DRAIN_SECONDS: '0', DEPLOY_HEALTH_TIMEOUT_SECONDS: '2', DEPLOY_HEALTH_POLL_SECONDS: '1', MOCK_LOG: log, MOCK_CANDIDATE_EXISTS: paths.candidate, MOCK_WORKER_EXISTS: paths.worker, MOCK_LEGACY_STOPPED: paths.stopped, MOCK_LEGACY_REMOVED: paths.removed, MOCK_CANDIDATE_HEALTH: health } }) const output = `${result.stdout}\n${result.stderr}` const dockerLog = await readFile(log, 'utf8') const stateFile = join(stateRoot, 'firstleg-production-file.active') const state = existsSync(stateFile) ? await readFile(stateFile, 'utf8') : '' const evidence = { ...result, output, dockerLog, state, stopped: existsSync(paths.stopped), removed: existsSync(paths.removed), worker: existsSync(paths.worker) } await rm(temp, { recursive: true, force: true }) return evidence } test('healthy candidate becomes active only after health and old instance drains', async (context) => { if (!existsSync(shell)) context.skip(`shell unavailable: ${shell}`) const result = await runBlueGreen('healthy') assert.equal(result.status, 0, result.output) assert.equal(result.stopped, true) assert.equal(result.removed, true) assert.equal(result.worker, true) assert.match(result.state, /^green\|firstleg-production-file-green\|19206\|registry\.local\/group\/file:newtag\|firstleg-production-file-green-worker/) const run = result.dockerLog.indexOf('compose --project-name firstleg-production') const stop = result.dockerLog.indexOf('stop --time 45 firstleg-production-file') assert.ok(run >= 0 && run < stop) assert.match(result.dockerLog, /network connect --alias file --alias file-service firstleg-production-routing firstleg-production-file-green/) }) test('unhealthy candidate is removed while old instance and state remain untouched', async (context) => { if (!existsSync(shell)) context.skip(`shell unavailable: ${shell}`) const result = await runBlueGreen('unhealthy') assert.equal(result.status, 1, result.output) assert.equal(result.stopped, false) assert.equal(result.removed, false) assert.equal(result.worker, false) assert.equal(result.state, '') assert.doesNotMatch(result.dockerLog, /stop --time .*firstleg-production-file/) })