Improve deploy errors and use bottom job tray as sole feedback.
CI / test (push) Successful in 9m56s
CI / test (push) Successful in 9m56s
Map Docker deploy failures to actionable how-to-fix messages, keep full detail through RPC sanitization, and stop top-center toasts from doubling the live job tray so operators only see one notification per action.
This commit is contained in:
@@ -0,0 +1,88 @@
|
||||
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('...'))
|
||||
})
|
||||
@@ -74,3 +74,26 @@ test('presentError dedupes identical messages', (t) => {
|
||||
presentError(err, 'deployContainer', { showAlert })
|
||||
t.is(called, 1)
|
||||
})
|
||||
|
||||
test('explainError surfaces How to fix from deploy messages', (t) => {
|
||||
const err = new Error(
|
||||
'Port conflict while starting "web" — Host port 8080 is already bound. — How to fix: Change the host port mapping.'
|
||||
)
|
||||
err.code = 'DOCKER_CONFLICT'
|
||||
const info = explainError(err, 'deployContainer')
|
||||
t.ok(/port/i.test(info.title + info.message))
|
||||
t.ok(/host port|change/i.test(info.recovery))
|
||||
t.is(info.silent, false)
|
||||
})
|
||||
|
||||
test('presentError can suppress toast when job drawer owns UI', (t) => {
|
||||
let opts = null
|
||||
const showAlert = (_type, _text, o) => {
|
||||
opts = o
|
||||
}
|
||||
const err = new Error('deploy boom unique suppress toast')
|
||||
presentError(err, 'deployContainer', { showAlert, force: true, toast: false, tray: false })
|
||||
t.ok(opts)
|
||||
t.is(opts.toast, false)
|
||||
t.is(opts.tray, false)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
import test from 'brittle'
|
||||
import {
|
||||
markFeedbackShown,
|
||||
wasFeedbackRecentlyShown,
|
||||
showAlert,
|
||||
} from '../libs/uiUtils.js'
|
||||
|
||||
test('markFeedbackShown dedupes subsequent showAlert', (t) => {
|
||||
const msg = `unique-feedback-dedupe-${Date.now()}`
|
||||
markFeedbackShown('success', msg)
|
||||
t.ok(wasFeedbackRecentlyShown('success', msg))
|
||||
|
||||
// showAlert should no-op (no throw) when recently shown
|
||||
showAlert('success', msg)
|
||||
t.pass()
|
||||
})
|
||||
|
||||
test('wasFeedbackRecentlyShown is case/whitespace tolerant via key', (t) => {
|
||||
const base = ` Case Mix Feedback ${Date.now()} `
|
||||
markFeedbackShown('danger', base)
|
||||
t.ok(wasFeedbackRecentlyShown('danger', base.trim()))
|
||||
})
|
||||
Reference in New Issue
Block a user