Files
tickte-system/tests/unit/workersHarness.test.mjs
T
ClaudeandClaude Sonnet 5.5 aceb558564 Workers P2: Prüfstand um Ausfall-Szenario erweitert (503, 401-Folge, hängende Verbindung, versteckter Tab)
Endprüfung P2. Die Zustände des Boards bei Ausfall des Dienstes und der Wechsel zwischen sichtbarem und verstecktem Tab
waren nur über Quelltext-Muster geprüft. Der Prüfstand (?fault=none|down|session|hang) schaltet jetzt den echten
API-Client hinter eine Fetch-Weiche (scripted-fetch.js, mit Node-Tests). Szenario F in shot.cjs prüft im Browser:
Hinweis mit „Neu laden“ bei 503 beim Öffnen und Rückkehr, Warnzeile „Verbindung gestört“ im Betrieb, versteckter Tab
(keine Abrufe, keine Bilder, beim Zurückkehren sofort beides), dauerhaftes 401 (höchstens ein frisches JWT, Meldung
„Anmeldung abgelaufen“, Erholung ohne neues Anmelden) und eine Antwort, die nie kommt (Zeitgrenze). Das Handy-Szenario
prüft zusätzlich Auswahl aus der Liste (Karte im Fenster, Fokus) und Fokus nach „Karte schließen“.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-18 05:41:03 +00:00

173 lines
10 KiB
JavaScript

import test from 'node:test'
import assert from 'node:assert/strict'
import { loadModule } from './load.mjs'
// Prüfstand der Hustler-Seite: die Skript-API (gleiche Form wie createWorkersApi) für die manuelle Playwright-Abnahme.
// Alles mit einer eigenen Uhr: keine echte Zeit, kein Netz.
const { createScriptedApi, SCRIPT } = await loadModule('scripts/workers-harness/scripted-api.js')
const { EVENT_KINDS } = await loadModule('src/lib/workersEvents.js')
const { SEARCH_ROOMS, ROLES, roleOf } = await loadModule('src/lib/workersRoster.js')
const ms = (iso) => Date.parse(iso)
test('Skript deckt alle 26 Ereignisarten und mindestens 15 Worker ab; erstes Ereignis nicht vor 2 s', () => {
assert.deepEqual(new Set(SCRIPT.map((s) => s.event.kind)), new Set(EVENT_KINDS))
assert.ok(new Set(SCRIPT.map((s) => s.event.workerId).filter((w) => w !== 'system')).size >= 15)
assert.ok(Math.min(...SCRIPT.map((s) => s.at)) >= 2)
})
test('scripted api: 19 Worker (kim, pia, finn nicht angeschlossen); events liefert wie der Dienst ab after - 5 s, nie aus der Zukunft; Pause folgt', async () => {
let t = 0
const api = createScriptedApi({ now: () => t })
const s = await api.state()
assert.equal(s.workers.length, 19)
assert.deepEqual(s.workers.filter((w) => !w.connected).map((w) => w.workerId).sort(), ['finn', 'kim', 'pia'])
// Grau-Regel (P1-E9): nur Such-Räume können nicht angeschlossen sein, die anderen elf sind es immer
assert.ok(s.workers.filter((w) => !w.connected).every((w) => SEARCH_ROOMS.includes(roleOf(w.workerId).room)))
assert.deepEqual(s.workers.map((w) => w.workerId), ROLES.map((r) => r.workerId))
assert.ok(new Set(s.workers.map((w) => w.status)).size >= 3) // gemischte Status
const base = ms(s.lastEventTs)
assert.ok(Number.isFinite(base))
assert.equal((await api.events(null)).events.length, 0) // nichts fällig bei 0 s
// PF-09: Antwort nach P1-Semantik (ts >= after - 5000, aufsteigend, nur Fälliges); die Entdopplung übernimmt der Feed (T05)
t = 10000
const first = (await api.events(s.lastEventTs)).events
assert.ok(first.length > 0)
assert.ok(first.every((e) => ms(e.ts) <= base + 10000)) // nichts aus der Zukunft
assert.ok(first.every((e, i) => i === 0 || ms(first[i - 1].ts) <= ms(e.ts))) // aufsteigend
const last = first.at(-1)
const again = (await api.events(last.ts)).events
assert.ok(again.some((e) => e.id === last.id)) // Überlappung: das letzte Ereignis kommt wieder
assert.ok(again.every((e) => ms(e.ts) >= ms(last.ts) - 5000 && ms(e.ts) <= base + 10000))
assert.equal(new Set(again.map((e) => e.id)).size, again.length) // innerhalb einer Antwort nie doppelt
assert.equal((await api.events(null, 2)).events.length, 2) // limit: die ältesten
assert.deepEqual((await api.events(null, 2)).events.map((e) => e.id), first.slice(0, 2).map((e) => e.id))
t = 30000 // später: Neues kommt dazu
const later = (await api.events(last.ts)).events
assert.ok(later.some((e) => !first.some((f) => f.id === e.id)))
assert.ok(later.every((e) => ms(e.ts) <= base + 30000))
await api.pause(true)
assert.equal((await api.state()).paused, true)
await api.pause(false)
assert.equal((await api.state()).paused, false)
assert.equal(s.sendingHalted, false) // Standard: nicht gestoppt
assert.equal(s.sendingHaltedReason, '')
// H4: gestoppt, Grund sichtbar, Freigabe über saveSettings
const h = createScriptedApi({ now: () => 0, halted: true })
const hs = await h.state()
assert.equal(hs.sendingHalted, true)
assert.match(hs.sendingHaltedReason, /Bounces/)
assert.equal((await h.settings()).sendingHalted, true)
const saved = await h.saveSettings({ sendingHalted: false })
assert.equal(saved.sendingHalted, false)
assert.equal(saved.sendingHaltedReason, '')
assert.deepEqual(h.saved, [{ sendingHalted: false }])
assert.equal((await h.state()).sendingHalted, false)
assert.equal((await h.settings()).sendingHalted, false)
assert.equal((await h.settings()).sendingHaltedReason, '')
})
test('scripted api: Spur mit 12 Ereignissen (unsicherer und sicherer Link), Tageswerte, Standard-Einstellungen, Zustand folgt den Ereignissen', async () => {
let t = 0
const api = createScriptedApi({ now: () => t })
const tr = await api.trace('kim')
assert.equal(tr.worker.workerId, 'kim')
assert.equal(tr.worker.name, 'Kim')
assert.equal(tr.worker.modelId, 'claude-haiku-5-5')
assert.equal(tr.events.length, 12)
assert.ok(tr.events.some((e) => e.link === 'javascript:alert(1)'))
assert.ok(tr.events.some((e) => e.link === 'https://example.org/fund'))
assert.ok(tr.events.every((e, i) => i === 0 || ms(tr.events[i - 1].ts) >= ms(e.ts))) // neueste zuerst
assert.ok(Number.isFinite(tr.today.tokens.in) && Number.isFinite(tr.today.tokens.out) && Number.isFinite(tr.today.costUsd))
assert.ok(Object.keys((await api.trace('rita')).today.counters).length > 0)
assert.deepEqual(tr.today.tokens, { in: 18400, out: 2300 })
assert.equal((await api.trace('bob')).worker.modelId, 'claude-opus-5-5')
assert.equal((await api.trace('bea')).worker.modelId, 'claude-sonnet-5-5')
const st = await api.settings() // Standardwerte aus Übersicht §4.1
assert.deepEqual(
[st.dailyBudgetUsd, st.monthlyBudgetUsd, st.warnPct, st.mailColdPerDay, st.mailReplyCapPerDay, st.sendWindowStart, st.sendWindowEnd, st.sendWeekdaysOnly],
[6, 150, 80, 3, 15, '09:00', '16:00', true])
assert.deepEqual([st.runsPerNight, st.candidatesPerRun, st.previewGate, st.previewGateN, st.retentionDays, st.offerProductKey], [1, 10, 'erste_n', 10, 90, 'hosting'])
assert.deepEqual(st.regions, ['Rheinland-Pfalz'])
assert.equal(st.privacyInteressentenLive, false)
assert.equal(st.sendingHalted, false)
assert.equal(st.sendingHaltedReason, '')
assert.ok(st.offerText.length >= 1 && st.offerText.length <= 400)
assert.deepEqual(st.connectedRooms.filter((r) => !SEARCH_ROOMS.includes(r)), []) // nur Such-Räume
assert.ok(!st.connectedRooms.includes('kleinanzeigen') && !st.connectedRooms.includes('portale') && !st.connectedRooms.includes('foren'))
const copy = await api.settings()
copy.dailyBudgetUsd = 99 // Antworten sind Kopien: der Speicher des Prüfstands bleibt heil
assert.equal((await api.settings()).dailyBudgetUsd, 6)
await api.saveSettings({ dailyBudgetUsd: 4 })
assert.equal((await api.settings()).dailyBudgetUsd, 4)
assert.deepEqual(api.saved, [{ dailyBudgetUsd: 4 }])
// Der Zustand schreibt die fälligen Ereignisse fort (wie der Dienst), lastEventTs rückt auf das jüngste fällige Ereignis
const s0 = await api.state()
t = 60000
const s1 = await api.state()
assert.ok(ms(s1.lastEventTs) > ms(s0.lastEventTs))
assert.equal(s1.workers.length, 19)
assert.deepEqual(s1.workers.filter((w) => !w.connected).map((w) => w.workerId).sort(), ['finn', 'kim', 'pia'])
assert.ok(s1.workers.filter((w) => !w.connected).every((w) => w.status === 'grau'))
assert.ok(s1.workers.find((w) => w.workerId === 'otto').task !== s0.workers.find((w) => w.workerId === 'otto').task)
for (const key of ['newAbosMonth', 'mrrNewEur', 'dealsMonth', 'openThreads', 'pultOpen', 'botLeads', 'apiTodayUsd', 'apiMonthUsd', 'dailyBudgetUsd', 'monthlyBudgetUsd']) assert.ok(Number.isFinite(s1.kpis[key]), key)
assert.deepEqual(await api.pult('offen'), { items: [] })
assert.deepEqual(await api.botleads({}), { items: [], total: 0 })
})
// ---- Endprüfung P2: Fetch-Weiche des Prüfstands (echter API-Client gegen die Skript-API, mit Ausfall-Schalter) ----
const { createScriptedFetch } = await loadModule('scripts/workers-harness/scripted-fetch.js')
const { createWorkersApi } = await loadModule('src/lib/workersApi.js')
function wired(fault = null, extra = {}) {
const control = { fault, calls: [], jwts: [] }
const scripted = createScriptedApi({ now: () => 0 })
const api = createWorkersApi({
getJwt: async (o) => {
control.jwts.push(o?.fresh === true)
return 'harness-jwt'
},
fetchImpl: createScriptedFetch(scripted, control),
...extra,
})
return { control, scripted, api }
}
test('Fetch-Weiche gesund: der echte API-Client erreicht Zustand, Ereignisse, Spur, Pause und Einstellungen der Skript-API', async () => {
const { control, scripted, api } = wired()
assert.equal((await api.state()).workers.length, 19)
assert.deepEqual(await api.events(null, 5), { events: [] })
assert.equal((await api.trace('kim')).worker.name, 'Kim')
assert.deepEqual(await api.pause(true), { paused: true })
assert.equal((await api.state()).paused, true)
assert.equal((await api.settings()).paused, true)
await api.saveSettings({ sendingHalted: false })
assert.deepEqual(scripted.saved, [{ sendingHalted: false }])
await assert.rejects(api.trace('gibt-es-nicht'), { message: 'Diesen Hustler gibt es nicht.', status: 404 })
assert.deepEqual(control.calls.map((c) => c.path).slice(0, 3), ['/state', '/events', '/worker/kim/trace'])
assert.ok(control.calls.every((c) => c.jwt === 'harness-jwt'))
})
test('Fetch-Weiche down: 503 ohne JSON ergibt die Dienst-Meldung, session: 401 ergibt die Anmelde-Meldung, danach nur ein createJWT je Sperre', async () => {
const down = wired('down')
await assert.rejects(down.api.state(), { message: 'Der Hustler-Dienst ist gerade nicht erreichbar.', status: 503 })
const session = wired('session')
for (let i = 0; i < 6; i += 1) await assert.rejects(session.api.events(null), { message: 'Anmeldung abgelaufen. Bitte neu anmelden.', status: 401 })
assert.equal(session.control.jwts.filter(Boolean).length, 1)
assert.equal(session.control.calls.length, 7) // 1 Versuch + 1 Wiederholung, dann je Aufruf eine Anfrage
// Schalter zurück: der Dienst nimmt das JWT wieder an, ohne dass etwas neu erzeugt wird
session.control.fault = null
assert.equal((await session.api.state()).workers.length, 19)
assert.equal(session.control.jwts.filter(Boolean).length, 1)
})
test('Fetch-Weiche hang: die Antwort kommt nie, der API-Client gibt nach der Zeitgrenze auf', async () => {
const { api } = wired('hang', { timeoutMs: 40 })
await assert.rejects(api.state(), { message: 'Der Hustler-Dienst ist gerade nicht erreichbar.', status: 0 })
})