Endprüfung Nachrunde R7: Test der Zeitgrenze (D14) für reply, approve-preview und start-build wiederhergestellt

Der Commit 1536a3b hatte den Test „reply, approve-preview und start-build warten länger als die übrigen
Pult-Aktionen“ gelöscht. Der neue Test prüft gegen den heutigen Code (ACTION_TIMEOUT_MS 65 s,
REQUEST_TIMEOUT_MS 20 s, Optionen timeoutMs / actionTimeoutMs) mit den Uhren von node:test statt echter
Wartezeit: keine Abhängigkeit von der Last, Grenzen auf die Millisekunde (eine Millisekunde vor der Frist
läuft der Abruf, mit ihr endet er). Die drei langsamen Aktionen nehmen eine Antwort zwischen kurzer und langer
Frist an, claim/sent/discard/done/paste und das Lesen enden mit der kurzen Frist (DOWN_TEXT, status 0, Signal
abgebrochen), ein hängender Abruf endet nach der langen Frist. Derselbe Ablauf mit Standard- und mit
eingespeisten Fristen; ein zweiter Test pinnt, dass auch der Wiederholungsversuch nach einer 401 die Frist
der Aktion behält.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-18 23:54:10 +00:00
1 parent c48d1834f4
commit 41ae7ffd3f
1 file changed
+106
+106
View File
@@ -207,6 +207,112 @@ test('Zeitgrenze: Standardwerte 20 s und 2 min, für langsame Pult-Aktionen 65 s
assert.ok(Object.isFrozen(UNCERTAIN_STATUS))
})
// P4-T32 (D14), Nachrunde R7: Antworten, Freigeben und Bauen laufen im Dienst länger (SMTP, Warteschlange); bricht der Client nach 20 s ab, sähe der
// Mensch einen Fehler, obwohl die Mail hinausgeht. Nur diese drei Aktionen bekommen die lange Frist (65 s), alles andere bleibt bei der kurzen (20 s).
// Mit den Uhren von node:test statt echter Wartezeit: kein Warten, keine Abhängigkeit von der Last des Rechners, und die Grenzen sind auf die Millisekunde
// genau prüfbar (eine Millisekunde vor der Frist läuft der Abruf noch, mit ihr endet er). setImmediate bleibt echt und lässt die Promise-Ketten durchlaufen.
const flush = () => new Promise((resolve) => setImmediate(resolve))
const bodyFor = (action) => (action === 'reply' || action === 'paste' ? { text: 'x' } : undefined)
// Dienst, der erst nach ms antwortet (Uhr von node:test); merkt sich die Signale der Abrufe
function slowService(ms, signals = []) {
return (url, init) => {
signals.push(init.signal)
return new Promise((resolve) => setTimeout(() => resolve({ ok: true, status: 200, json: async () => ({ item: { $id: 'p1' } }) }), ms))
}
}
// Ergebnis eines Abrufs, der noch läuft, ohne auf ihn zu warten: 'offen', 'ok' oder der Fehler
function watch(promise) {
const w = { state: 'offen', error: null }
promise.then(() => { w.state = 'ok' }, (e) => { w.state = 'fehler'; w.error = e })
return w
}
// Standardfristen (20 s / 65 s) und eingespeiste Fristen (timeoutMs / actionTimeoutMs) müssen sich gleich verhalten
for (const [label, options] of [
['Standardfristen', {}],
['eingespeiste Fristen', { timeoutMs: 300, actionTimeoutMs: 4000 }],
]) {
test(`Zeitgrenze (D14): reply, approve-preview und start-build warten länger als die übrigen Pult-Aktionen (${label})`, async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] })
const short = options.timeoutMs ?? REQUEST_TIMEOUT_MS
const long = options.actionTimeoutMs ?? ACTION_TIMEOUT_MS
assert.ok(short < long)
const between = Math.floor((short + long) / 2) // die Antwort kommt nach der kurzen, aber vor der langen Frist
const run = async (action, { service = slowService(between) }) => {
const api = createWorkersApi({ getJwt, fetchImpl: service, ...options })
const w = watch(action === 'read' ? api.pult() : api.pultAction('p1', action, bodyFor(action)))
await flush() // JWT geholt, Anfrage gesendet, Frist und Antwort stehen auf der Uhr
return { w, api, tick: async (ms) => { t.mock.timers.tick(ms); await flush() } }
}
// die drei langsamen Aktionen: die Antwort nach der kurzen Frist kommt noch an
for (const action of ['reply', 'approve-preview', 'start-build']) {
const { w, tick } = await run(action, {})
await tick(short) // die kurze Frist ist erreicht: der Abruf läuft weiter
assert.equal(w.state, 'offen', `${action}: nach der kurzen Frist noch offen`)
await tick(between - short) // die Antwort trifft ein
assert.equal(w.state, 'ok', `${action}: Antwort nach der kurzen Frist, vor der langen`)
}
// alle übrigen Aktionen und das Lesen behalten die kurze Frist: eine Millisekunde davor offen, mit ihr „nicht erreichbar“ (status 0), Signal abgebrochen
for (const action of ['claim', 'sent', 'discard', 'done', 'paste', 'read']) {
const signals = []
const { w, tick } = await run(action, { service: slowService(between, signals) })
await tick(short - 1)
assert.equal(w.state, 'offen', `${action}: eine Millisekunde vor der kurzen Frist`)
assert.equal(signals[0].aborted, false, action)
await tick(1)
assert.equal(w.state, 'fehler', `${action}: mit der kurzen Frist`)
assert.equal(w.error.message, DOWN, action)
assert.equal(w.error.status, 0, action)
assert.equal(signals[0].aborted, true, `${action}: das Signal ist abgebrochen`)
}
// auch die lange Frist endet: ein hängender Abruf meldet nach ihr die Dienst-Meldung (status 0) und bricht ab, keine Millisekunde früher
for (const action of ['reply', 'approve-preview', 'start-build']) {
const signals = []
const hang = (url, init) => { signals.push(init.signal); return new Promise(() => {}) }
const { w, tick } = await run(action, { service: hang })
await tick(long - 1)
assert.equal(w.state, 'offen', `${action}: eine Millisekunde vor der langen Frist`)
assert.equal(signals[0].aborted, false, action)
await tick(1)
assert.equal(w.state, 'fehler', `${action}: mit der langen Frist`)
assert.deepEqual([w.error.message, w.error.status], [DOWN, 0], action)
assert.equal(signals[0].aborted, true, `${action}: das Signal ist abgebrochen`)
}
})
}
test('Zeitgrenze (D14): auch der Wiederholungsversuch nach einer 401 behält die lange Frist bei den langsamen Aktionen, die kurze bei den übrigen', async (t) => {
t.mock.timers.enable({ apis: ['setTimeout'] })
const MIDDLE = Math.floor((REQUEST_TIMEOUT_MS + ACTION_TIMEOUT_MS) / 2)
const slowAfter401 = () => {
let n = 0
return (url, init) => {
n += 1
if (n === 1) return Promise.resolve({ ok: false, status: 401, json: async () => ({}) })
return slowService(MIDDLE)(url, init) // zweiter Versuch: Antwort nach der kurzen (20 s), vor der langen Frist (65 s)
}
}
const tick = async (ms) => { t.mock.timers.tick(ms); await flush() }
for (const action of ['reply', 'approve-preview', 'start-build']) {
const w = watch(createWorkersApi({ getJwt, fetchImpl: slowAfter401() }).pultAction('p1', action, bodyFor(action)))
await flush()
await tick(MIDDLE)
assert.equal(w.state, 'ok', action)
}
for (const action of ['claim', 'paste']) {
const w = watch(createWorkersApi({ getJwt, fetchImpl: slowAfter401() }).pultAction('p1', action, bodyFor(action)))
await flush()
await tick(REQUEST_TIMEOUT_MS)
assert.equal(w.state, 'fehler', action)
assert.deepEqual([w.error.message, w.error.status], [DOWN, 0], action)
}
})
// Endprüfung P4 (W1, D14): eine Fehlerseite des Proxys (502/504) bei reply, approve-preview und start-build heißt nicht „nicht erreichbar“:
// der Dienst arbeitet weiter, wenn nginx aufgibt, die Mail kann schon hinausgegangen sein. Der Client selbst (read) bleibt bei DOWN_TEXT
// und dem echten Status; die Aktions-Meldung (actionErrorText) entscheidet nach Aktion und Status.