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 }) })