From 41ae7ffd3fd3dabaf4463c191d9197d84ceec044 Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 18 Oct 2026 23:54:10 +0000 Subject: [PATCH] =?UTF-8?q?Endpr=C3=BCfung=20Nachrunde=20R7:=20Test=20der?= =?UTF-8?q?=20Zeitgrenze=20(D14)=20f=C3=BCr=20reply,=20approve-preview=20u?= =?UTF-8?q?nd=20start-build=20wiederhergestellt?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- tests/unit/workersApi.test.mjs | 106 +++++++++++++++++++++++++++++++++ 1 file changed, 106 insertions(+) diff --git a/tests/unit/workersApi.test.mjs b/tests/unit/workersApi.test.mjs index 6f03754..ffb787e 100644 --- a/tests/unit/workersApi.test.mjs +++ b/tests/unit/workersApi.test.mjs @@ -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.