import test from 'brittle' import { extractDockerMessage, extractDockerStatus, formatDeployError, sanitizeClientError, } from '../server/utils/dockerErrors.js' test('extractDockerMessage prefers json.message', (t) => { const err = new Error('(HTTP code 409) unexpected - short') err.json = { message: 'port is already allocated' } err.statusCode = 409 t.is(extractDockerMessage(err), 'port is already allocated') t.is(extractDockerStatus(err), 409) }) test('formatDeployError port conflict is actionable', (t) => { const err = new Error('(HTTP code 500) server error - Bind for 0.0.0.0:8080 failed: port is already allocated') err.statusCode = 500 const out = formatDeployError(err, { stage: 'start', containerName: 'web', image: 'nginx:alpine', }) t.ok(/port conflict/i.test(out.message) || /port/i.test(out.message)) t.ok(/how to fix/i.test(out.message)) t.ok(/8080|host port/i.test(out.message)) }) test('formatDeployError name conflict', (t) => { const err = new Error( 'Conflict. The container name "/api" is already in use by container "abc123". You have to remove (or rename) that container to be able to reuse that name.' ) err.statusCode = 409 const out = formatDeployError(err, { stage: 'create', containerName: 'api', image: 'node:20' }) t.ok(/already taken|already/i.test(out.message)) t.ok(/how to fix/i.test(out.message)) t.ok(/different name|remove/i.test(out.message)) }) test('formatDeployError image not found', (t) => { const err = new Error('manifest for totally-fake/image:nope not found: manifest unknown') err.statusCode = 404 const out = formatDeployError(err, { stage: 'pull', containerName: 'x', image: 'totally-fake/image:nope', }) t.ok(/not found/i.test(out.message)) t.ok(/how to fix/i.test(out.message)) t.ok(/image name and tag/i.test(out.message)) }) test('formatDeployError bind mount path', (t) => { const err = new Error( 'invalid mount config for type "bind": bind source path does not exist: /does/not/exist' ) const out = formatDeployError(err, { stage: 'create', containerName: 'db', image: 'postgres' }) t.ok(/volume|bind|mount/i.test(out.message)) t.ok(/how to fix/i.test(out.message)) t.ok(/host path|named volume/i.test(out.message)) }) test('sanitizeClientError keeps long deploy messages', (t) => { const long = 'Port conflict while starting "web" — Host port 8080 is already bound by another process or container. — How to fix: Change the host port in Port mappings, stop the other container using that port, or remove the conflicting publish rule. — (Docker HTTP 500)' const err = new Error(long) err.code = 'DOCKER_CONFLICT' const safe = sanitizeClientError(err) t.ok(safe.length > 200) t.ok(/how to fix/i.test(safe)) t.absent(/an error occurred\. please try again/i.test(safe)) }) test('sanitizeClientError redacts secrets but keeps detail', (t) => { const err = new Error('login failed password=supersecret token=abc123 for registry') const safe = sanitizeClientError(err) t.ok(/REDACTED/.test(safe)) t.absent(/supersecret/.test(safe)) t.ok(/login failed/i.test(safe)) }) test('sanitizeClientError does not nuke long messages', (t) => { const msg = 'x'.repeat(250) + ' useful-detail' const safe = sanitizeClientError(new Error(msg)) t.ok(safe.length > 200) t.ok(safe.includes('useful') || safe.endsWith('…') || safe.endsWith('...')) })