Release rolling / release (push) Successful in 9m13s
Lifecycle RPCs now use a 120s operation timeout, remove cleans up stats/terminal/log streams then SIGKILLs before force-remove, and the UI awaits the request instead of a fragile 30s wait race.
126 lines
4.4 KiB
JavaScript
126 lines
4.4 KiB
JavaScript
import test from 'brittle'
|
|
import {
|
|
unwrapError,
|
|
explainError,
|
|
presentError,
|
|
formatResponseError,
|
|
normalizeRpcError,
|
|
isBackgroundMethod,
|
|
} from '../client/errors.js'
|
|
|
|
test('unwrapError peels REQUEST_ERROR wrapper to cause', (t) => {
|
|
const cause = new Error('Docker socket not found')
|
|
cause.code = 'ENGINE_DOWN'
|
|
const outer = new Error('REQUEST_ERROR: Request failed')
|
|
outer.code = 'REQUEST_ERROR'
|
|
outer.cause = cause
|
|
const root = unwrapError(outer)
|
|
t.is(root.message, 'Docker socket not found')
|
|
t.is(root.code, 'ENGINE_DOWN')
|
|
})
|
|
|
|
test('explainError uses unwrapped message not Request failed', (t) => {
|
|
const cause = new Error('permission denied while trying to connect to Docker')
|
|
const outer = new Error('REQUEST_ERROR: Request failed')
|
|
outer.code = 'REQUEST_ERROR'
|
|
outer.cause = cause
|
|
const info = explainError(outer, 'listContainers')
|
|
t.ok(info.message.toLowerCase().includes('docker') || info.message.toLowerCase().includes('permission'))
|
|
t.absent(/request failed/i.test(info.message))
|
|
})
|
|
|
|
test('generic Request failed without cause is quiet for background methods', (t) => {
|
|
const err = new Error('REQUEST_ERROR: Request failed')
|
|
err.code = 'REQUEST_ERROR'
|
|
const info = explainError(err, 'listContainers')
|
|
t.ok(info.silent || isBackgroundMethod('listContainers'))
|
|
})
|
|
|
|
test('formatResponseError suppresses bare Request failed spam', (t) => {
|
|
const formatted = formatResponseError('REQUEST_ERROR: Request failed', 'listVolumes')
|
|
t.ok(formatted)
|
|
t.ok(formatted.silent === true || !/REQUEST_ERROR/i.test(formatted.message))
|
|
})
|
|
|
|
test('normalizeRpcError sets method and readable message', (t) => {
|
|
const cause = new Error('Invalid arguments: name required')
|
|
cause.code = 'INVALID_ARGS'
|
|
const outer = new Error('Request failed')
|
|
outer.code = 'REQUEST_ERROR'
|
|
outer.cause = cause
|
|
const n = normalizeRpcError(outer, 'createVolume')
|
|
t.is(n.method, 'createVolume')
|
|
t.ok(n.message.includes('name') || n.message.includes('Invalid'))
|
|
})
|
|
|
|
test('presentError silent skips showAlert', (t) => {
|
|
let called = 0
|
|
const showAlert = () => {
|
|
called += 1
|
|
}
|
|
const err = new Error('REQUEST_ERROR: Request failed')
|
|
err.code = 'REQUEST_ERROR'
|
|
presentError(err, 'ping', { showAlert, silent: true })
|
|
t.is(called, 0)
|
|
})
|
|
|
|
test('presentError dedupes identical messages', (t) => {
|
|
let called = 0
|
|
const showAlert = () => {
|
|
called += 1
|
|
}
|
|
const err = new Error('Something unique for dedupe test xyz')
|
|
presentError(err, 'deployContainer', { showAlert, force: true })
|
|
presentError(err, 'deployContainer', { showAlert })
|
|
t.is(called, 1)
|
|
})
|
|
|
|
test('PEER_NOT_FOUND is not misclassified as image not found', (t) => {
|
|
const err = new Error('PEER_NOT_FOUND: Peer not found')
|
|
err.code = 'PEER_NOT_FOUND'
|
|
const info = explainError(err, 'connect')
|
|
t.is(info.code, 'PEER_NOT_FOUND')
|
|
t.ok(/server not found|peer not found/i.test(info.title + info.message))
|
|
t.absent(/image not found/i.test(info.title))
|
|
})
|
|
|
|
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)
|
|
})
|
|
|
|
test('RPC timeout is operation timeout not connection problem', (t) => {
|
|
const err = new Error('timeout of 30000ms exceeded')
|
|
err.code = 'TIMEOUT_EXCEEDED'
|
|
const info = explainError(err, 'removeContainer')
|
|
t.is(info.code, 'TIMEOUT_EXCEEDED')
|
|
t.ok(/timed out|timeout/i.test(info.title + info.message))
|
|
t.absent(/connection problem/i.test(info.title))
|
|
t.ok(/refresh|retry|docker/i.test(info.recovery))
|
|
})
|
|
|
|
test('timeout message without code is not connection problem', (t) => {
|
|
const err = new Error('timeout of 120000ms exceeded')
|
|
const info = explainError(err, 'removeContainer')
|
|
t.ok(/timed out|timeout/i.test(info.title))
|
|
t.absent(/connection problem/i.test(info.title))
|
|
})
|