Terminplaner: Tagesjob-Meldung, Betriebe-Zaehlung und Bericht
meldeTagesjob meldet am Ende des Tagesjobs ueber den Melder aus A10a2 die Vorfaelle tagesjob (Fehler oder Abbruch), deckel (Loeschdeckel an 3 Tagesjobs in Folge) und betriebe (ab 20 aktiven Betrieben, D8) und merkt letzterTagesjob (nur Zaehlungen). taeglicherJob liefert geloescht und aktiveBetriebe, tagesbericht haengt "n aktive Betriebe" an. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
689a21f9ac
commit
4601c5ef3b
4 files changed
+471
-34
No files matched your search
@@ -1,3 +1,5 @@
|
|||||||
|
import { config } from '../../config.js'
|
||||||
|
import { Query, countDocuments } from '../appwriteAdmin.js'
|
||||||
import { sendAdminNotice } from '../mailer.js'
|
import { sendAdminNotice } from '../mailer.js'
|
||||||
import { mailBudget, pausiertBis, warteschlangeAlter } from './outbox.js'
|
import { mailBudget, pausiertBis, warteschlangeAlter } from './outbox.js'
|
||||||
import { pruefeNow, pruefeText } from './util.js'
|
import { pruefeNow, pruefeText } from './util.js'
|
||||||
@@ -5,7 +7,8 @@ import { datumKurz, uhrzeit } from './zeit.js'
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Betriebspruefung des Terminplaners (Spezifikation 2026-10-03-terminplaner-tp2-verkauf-design.md, Abschnitt 8, Punkt 5
|
* Betriebspruefung des Terminplaners (Spezifikation 2026-10-03-terminplaner-tp2-verkauf-design.md, Abschnitt 8, Punkt 5
|
||||||
* „Sichtbarkeit“): Das Team soll es erfahren, bevor Gaeste es merken. Der Minutenjob ruft pruefeBetrieb am Ende jedes Laufs auf (Modus off: gar nicht).
|
* „Sichtbarkeit“): Das Team soll es erfahren, bevor Gaeste es merken. Der Minutenjob ruft pruefeBetrieb am Ende jedes Laufs auf
|
||||||
|
* (Modus off: gar nicht), der Tagesjob meldeTagesjob.
|
||||||
*
|
*
|
||||||
* Vorfaelle dieses Bausteins (Schluessel):
|
* Vorfaelle dieses Bausteins (Schluessel):
|
||||||
* budget mindestens 70 % von config.termine.mailsProStunde im Stundenfenster belegt (mailBudget)
|
* budget mindestens 70 % von config.termine.mailsProStunde im Stundenfenster belegt (mailBudget)
|
||||||
@@ -13,7 +16,13 @@ import { datumKurz, uhrzeit } from './zeit.js'
|
|||||||
* eine Wiederholung mit naechsterVersuch in der Zukunft ist nicht faellig und zaehlt nicht
|
* eine Wiederholung mit naechsterVersuch in der Zukunft ist nicht faellig und zaehlt nicht
|
||||||
* pause die Anmeldepause des Mailservers laeuft (pausiertBis)
|
* pause die Anmeldepause des Mailservers laeuft (pausiertBis)
|
||||||
*
|
*
|
||||||
* Gemeinsamer Melder (meldeVorfall, auch fuer die Tagesjob-Vorfaelle von A10a3): je Schluessel
|
* Vorfaelle des Tagesjobs (meldeTagesjob, am Ende von taeglicherJob):
|
||||||
|
* tagesjob der Tagesjob endete mit Fehlern (fehler > 0) oder brach ab
|
||||||
|
* deckel die Loeschlaeufe endeten an 3 Tagesjobs in Folge am Deckel (irgendein Bereich: Fristen, abgeschaltete Funktionen, Zellen)
|
||||||
|
* betriebe mindestens 20 aktive Betriebe (Entscheidung D8: ab 25 wird der Umstieg auf einen EU-Versanddienst geplant)
|
||||||
|
* Daneben merkt meldeTagesjob den Stand des letzten Tagesjobs fuer die Statusroute (letzterTagesjob: nur Zaehlungen).
|
||||||
|
*
|
||||||
|
* Gemeinsamer Melder (meldeVorfall, auch fuer die Tagesjob-Vorfaelle): je Schluessel
|
||||||
* - Warnlog `[termine-betrieb] ...` hoechstens stuendlich (nur Zahlen, nie Adresse oder Name eines Gastes),
|
* - Warnlog `[termine-betrieb] ...` hoechstens stuendlich (nur Zahlen, nie Adresse oder Name eines Gastes),
|
||||||
* - Team-Mail T20 (sendAdminNotice) beim Beginn und danach fruehestens nach 6 Stunden, auch wenn der Vorfall zwischendurch
|
* - Team-Mail T20 (sendAdminNotice) beim Beginn und danach fruehestens nach 6 Stunden, auch wenn der Vorfall zwischendurch
|
||||||
* endete. Die Abkuehlzeit beginnt mit dem Versuch, nicht erst mit dem Erfolg: faellt der Mailserver aus (bei der Anmeldepause
|
* endete. Die Abkuehlzeit beginnt mit dem Versuch, nicht erst mit dem Erfolg: faellt der Mailserver aus (bei der Anmeldepause
|
||||||
@@ -33,17 +42,29 @@ const MAIL_ABKUEHLZEIT_MS = 6 * STUNDE_MS
|
|||||||
const QUEUE_ALTER_MIN = 10
|
const QUEUE_ALTER_MIN = 10
|
||||||
/** Das Budget gilt als fast aufgebraucht ab 7/10 (ganzzahlig gerechnet: benutzt x 10 >= max x 7, kein Fliesskommafehler). */
|
/** Das Budget gilt als fast aufgebraucht ab 7/10 (ganzzahlig gerechnet: benutzt x 10 >= max x 7, kein Fliesskommafehler). */
|
||||||
const budgetErreicht = (benutzt, max) => max > 0 && benutzt * 10 >= max * 7
|
const budgetErreicht = (benutzt, max) => max > 0 && benutzt * 10 >= max * 7
|
||||||
|
/** Der Loeschdeckel gilt als Dauerzustand, wenn so viele Tagesjobs in Folge daran endeten. */
|
||||||
|
const DECKEL_FOLGE_AB = 3
|
||||||
|
/** Ab so vielen aktiven Betrieben (D8) gehen Warnlog und Team-Mail; ab BETRIEBE_UMSTIEG wird der EU-Versanddienst geplant. */
|
||||||
|
const BETRIEBE_AB = 20
|
||||||
|
const BETRIEBE_UMSTIEG = 25
|
||||||
|
|
||||||
/** Kurze Ueberschrift der Team-Mail (Betreff `[Terminplaner] <Ueberschrift>`); ohne Eintrag gilt die allgemeine. */
|
/** Kurze Ueberschrift der Team-Mail (Betreff `[Terminplaner] <Ueberschrift>`); ohne Eintrag gilt die allgemeine. */
|
||||||
const UEBERSCHRIFTEN = new Map([
|
const UEBERSCHRIFTEN = new Map([
|
||||||
['budget', 'Mailbudget fast aufgebraucht'],
|
['budget', 'Mailbudget fast aufgebraucht'],
|
||||||
['queue', 'Mail-Warteschlange staut sich'],
|
['queue', 'Mail-Warteschlange staut sich'],
|
||||||
['pause', 'Mailversand pausiert'],
|
['pause', 'Mailversand pausiert'],
|
||||||
|
['tagesjob', 'Tagesjob mit Fehlern'],
|
||||||
|
['deckel', 'Löschläufe am Deckel'],
|
||||||
|
['betriebe', 'Viele aktive Betriebe'],
|
||||||
])
|
])
|
||||||
const ALLGEMEINE_UEBERSCHRIFT = 'Betriebsmeldung'
|
const ALLGEMEINE_UEBERSCHRIFT = 'Betriebsmeldung'
|
||||||
|
|
||||||
/** schluessel => { logMs, mailMs } (null = noch nie). */
|
/** schluessel => { logMs, mailMs } (null = noch nie). */
|
||||||
const gemerkt = new Map()
|
const gemerkt = new Map()
|
||||||
|
/** Stand des letzten Tagesjobs ({ at, fehler, gedeckelt, geloescht }, nur Zaehlungen) oder null (nach einem Neustart bis zum Tagesjob). */
|
||||||
|
let letzter = null
|
||||||
|
/** Zahl der Tagesjobs in Folge, die in irgendeinem Bereich am Loeschdeckel endeten. */
|
||||||
|
let deckelFolge = 0
|
||||||
|
|
||||||
function merkzettel(schluessel) {
|
function merkzettel(schluessel) {
|
||||||
let zettel = gemerkt.get(schluessel)
|
let zettel = gemerkt.get(schluessel)
|
||||||
@@ -151,7 +172,90 @@ export async function pruefeBetrieb(now) {
|
|||||||
return aktiv
|
return aktiv
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Nur fuer Tests: den gemerkten Zustand (Zeit der letzten Warnung und Mail je Schluessel) zuruecksetzen. */
|
// ---------------------------------------------------------------- Tagesjob
|
||||||
|
|
||||||
|
/** Eine Zaehlung: ganze Zahl ab 0, alles andere (fehlt, Text, negativ, Bruch) zaehlt als 0. */
|
||||||
|
const zaehlung = (wert) => (Number.isInteger(wert) && wert > 0 ? wert : 0)
|
||||||
|
|
||||||
|
/** gedeckelt = { loeschfristen, abgeschaltet, zellen } (Schritte, die am Deckel endeten) => ihre Summe. */
|
||||||
|
const summeGedeckelt = (gedeckelt) =>
|
||||||
|
gedeckelt !== null && typeof gedeckelt === 'object' ? Object.values(gedeckelt).reduce((summe, n) => summe + zaehlung(n), 0) : 0
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Anzahl der aktiven Terminplaner-Betriebe: portalFeatures featureKey termine im Zustand active (ein Aufruf mit limit(1), gelesen wird
|
||||||
|
* total). Entwurf (off), bestellt, im Einbau und abgeschaltet zaehlen nicht.
|
||||||
|
* @returns {Promise<number|null>} null, wenn Appwrite keine Anzahl liefert; wirft bei einem Lesefehler
|
||||||
|
*/
|
||||||
|
export function zaehleAktiveBetriebe() {
|
||||||
|
return countDocuments(config.collections.portalFeatures, [Query.equal('featureKey', 'termine'), Query.equal('state', 'active')])
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Stand des letzten Tagesjobs als Kopie ({ at (ISO), fehler, gedeckelt, geloescht }) oder null, solange seit dem Start keiner gemeldet hat. */
|
||||||
|
export function letzterTagesjob() {
|
||||||
|
return letzter === null ? null : { ...letzter }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meldung am Ende des Tagesjobs (taeglicherJob ruft sie mit seinem Ergebnis): merkt den Stand fuer letzterTagesjob() und meldet ueber
|
||||||
|
* meldeVorfall die Vorfaelle tagesjob (fehler > 0 oder abbruch), deckel (Loeschdeckel an 3 Tagesjobs in Folge) und betriebe (ab 20
|
||||||
|
* aktiven Betrieben). Die Texte tragen nur Zahlen. Wirft nie (ausser mit TypeError bei ungueltigem `now`, wie alle Termine-Bausteine): ein fehlgeschlagener Mailversand steht
|
||||||
|
* im Log, die uebrigen Vorfaelle werden trotzdem gemeldet. Zustand (letzter Lauf, Deckel-Folge) lebt im Speicher dieses Prozesses.
|
||||||
|
* @param {{ fehler?: number, gedeckelt?: { loeschfristen?: number, abgeschaltet?: number, zellen?: number }, geloescht?: number, aktiveBetriebe?: number|null, abbruch?: boolean }} ergebnis
|
||||||
|
* fehler = Zahl der Fehler des Laufs; gedeckelt = Schritte je Bereich, die am Deckel endeten; geloescht = Summe aller Loeschzaehler;
|
||||||
|
* aktiveBetriebe = Zahl oder null (unbekannt); abbruch = der Job brach unerwartet ab (dann fehlen die uebrigen Zahlen und die
|
||||||
|
* Deckel-Folge bleibt unveraendert)
|
||||||
|
* @param {Date} now
|
||||||
|
* @returns {Promise<string[]>} die Schluessel der Vorfaelle dieses Laufs ('tagesjob', 'deckel', 'betriebe')
|
||||||
|
*/
|
||||||
|
export async function meldeTagesjob(ergebnis, now) {
|
||||||
|
const nowMs = pruefeNow(now, 'meldeTagesjob')
|
||||||
|
const gemeldet = []
|
||||||
|
try {
|
||||||
|
const abbruch = ergebnis?.abbruch === true
|
||||||
|
const fehler = zaehlung(ergebnis?.fehler)
|
||||||
|
const gedeckelt = summeGedeckelt(ergebnis?.gedeckelt)
|
||||||
|
const betriebe = Number.isInteger(ergebnis?.aktiveBetriebe) && ergebnis.aktiveBetriebe >= 0 ? ergebnis.aktiveBetriebe : null
|
||||||
|
letzter = { at: new Date(nowMs).toISOString(), fehler, gedeckelt, geloescht: zaehlung(ergebnis?.geloescht) }
|
||||||
|
if (!abbruch) deckelFolge = gedeckelt > 0 ? deckelFolge + 1 : 0
|
||||||
|
|
||||||
|
const vorfaelle = []
|
||||||
|
if (abbruch || fehler > 0) {
|
||||||
|
vorfaelle.push([
|
||||||
|
'tagesjob',
|
||||||
|
abbruch
|
||||||
|
? `Tagesjob abgebrochen (unerwarteter Fehler), ${fehler} Fehler gezählt; Einzelheiten stehen im Log`
|
||||||
|
: `Tagesjob mit ${fehler} Fehler${fehler === 1 ? '' : 'n'}; Einzelheiten stehen im Log`,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
if (deckelFolge >= DECKEL_FOLGE_AB) {
|
||||||
|
vorfaelle.push([
|
||||||
|
'deckel',
|
||||||
|
`Löschläufe endeten an ${deckelFolge} Tagesjobs in Folge am Deckel (im letzten Lauf ${gedeckelt} Schritte); der Rest folgt an den nächsten Tagen, hält der Rückstand an, sind die Löschdeckel zu prüfen`,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
if (betriebe !== null && betriebe >= BETRIEBE_AB) {
|
||||||
|
vorfaelle.push([
|
||||||
|
'betriebe',
|
||||||
|
`${betriebe} aktive Betriebe (Hinweis ab ${BETRIEBE_AB}); ab ${BETRIEBE_UMSTIEG} aktiven Betrieben ist der Umstieg auf einen EU-Versanddienst zu planen`,
|
||||||
|
])
|
||||||
|
}
|
||||||
|
for (const [schluessel, text] of vorfaelle) {
|
||||||
|
gemeldet.push(schluessel)
|
||||||
|
try {
|
||||||
|
await meldeVorfall(schluessel, text, now)
|
||||||
|
} catch (err) {
|
||||||
|
meldeFehler(`meldung:${schluessel}`, err, nowMs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
meldeFehler('meldung', err, nowMs)
|
||||||
|
}
|
||||||
|
return gemeldet
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Nur fuer Tests: den gemerkten Zustand (Zeit der letzten Warnung und Mail je Schluessel, letzter Tagesjob, Deckel-Folge) zuruecksetzen. */
|
||||||
export function resetBetriebForTests() {
|
export function resetBetriebForTests() {
|
||||||
gemerkt.clear()
|
gemerkt.clear()
|
||||||
|
letzter = null
|
||||||
|
deckelFolge = 0
|
||||||
}
|
}
|
||||||
@@ -2,7 +2,7 @@ import { config } from '../../config.js'
|
|||||||
import { Query, deleteDocument, getDocument, listAllDocuments } from '../appwriteAdmin.js'
|
import { Query, deleteDocument, getDocument, listAllDocuments } from '../appwriteAdmin.js'
|
||||||
import { withLocks } from '../keyedLock.js'
|
import { withLocks } from '../keyedLock.js'
|
||||||
import { gibFreiSicher, iso, vertragAm } from './ablauf.js'
|
import { gibFreiSicher, iso, vertragAm } from './ablauf.js'
|
||||||
import { pruefeBetrieb } from './betrieb.js'
|
import { meldeTagesjob, pruefeBetrieb, zaehleAktiveBetriebe } from './betrieb.js'
|
||||||
import {
|
import {
|
||||||
abgelaufeneHaltungen,
|
abgelaufeneHaltungen,
|
||||||
aendere,
|
aendere,
|
||||||
@@ -31,7 +31,9 @@ import { gibZellenFrei, raeumeVerwaisteZellen } from './zellen.js'
|
|||||||
* Betriebspruefung (Budget, Warteschlange, Anmeldepause => Log und Team-Mail, betrieb.js)
|
* Betriebspruefung (Budget, Warteschlange, Anmeldepause => Log und Team-Mail, betrieb.js)
|
||||||
* fuenfMinutenJob alle 5 Minuten Erinnerungen (G05); bestaetigt 1 Stunde nach Ende => erledigt; Anfragen mit Beginn in
|
* fuenfMinutenJob alle 5 Minuten Erinnerungen (G05); bestaetigt 1 Stunde nach Ende => erledigt; Anfragen mit Beginn in
|
||||||
* der Vergangenheit => abgelehnt (vom System, Zellen frei, keine Mail)
|
* der Vergangenheit => abgelehnt (vom System, Zellen frei, keine Mail)
|
||||||
* taeglicherJob taeglich Loeschfristen (11), festgefahrene und alte Mails, abgeschaltete Funktionen, verwaiste Zellen
|
* taeglicherJob taeglich Loeschfristen (11), festgefahrene und alte Mails, abgeschaltete Funktionen, verwaiste Zellen;
|
||||||
|
* zuletzt die aktiven Betriebe zaehlen und den Lauf melden (Fehler, Loeschdeckel in Folge,
|
||||||
|
* ab 20 Betrieben => Log und Team-Mail, meldeTagesjob in betrieb.js)
|
||||||
*
|
*
|
||||||
* Gemeinsame Regeln:
|
* Gemeinsame Regeln:
|
||||||
* - Modus off: jeder Job kehrt sofort zurueck, ohne Abfrage, mit { aus: true }.
|
* - Modus off: jeder Job kehrt sofort zurueck, ohne Abfrage, mit { aus: true }.
|
||||||
@@ -592,35 +594,65 @@ export function loescheAbgeschalteteFunktionen(now) {
|
|||||||
return loescheAbgeschaltete(einzelLauf('abgeschalteteFunktionen', now))
|
return loescheAbgeschaltete(einzelLauf('abgeschalteteFunktionen', now))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Summe aller Loeschzaehler eines Tagesjobs: Buchungen aus den Loeschfristen und aus abgeschalteten Funktionen (Zellen, Outbox und Mails zaehlen nicht). */
|
||||||
|
const buchungenGeloescht = (loeschfristen, abgeschaltet) => Object.values(loeschfristen).reduce((summe, n) => summe + n, 0) + abgeschaltet.buchungen
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Taeglich: festgefahrene Mails beenden (vor dem Aufraeumen, damit sie 48 Stunden lang als Zustellfehler sichtbar bleiben),
|
* Taeglich: festgefahrene Mails beenden (vor dem Aufraeumen, damit sie 48 Stunden lang als Zustellfehler sichtbar bleiben),
|
||||||
* Loeschfristen, abgeschaltete Funktionen, alte Warteschlangen-Eintraege, verwaiste Zellen.
|
* Loeschfristen, abgeschaltete Funktionen, alte Warteschlangen-Eintraege, verwaiste Zellen; zuletzt die aktiven Betriebe zaehlen
|
||||||
|
* (ein nicht lesbarer Zaehler ergibt null und einen Fehler) und den Lauf ans Team melden (meldeTagesjob: Fehler, Loeschdeckel an
|
||||||
|
* 3 Tagen in Folge, ab 20 Betrieben; wirft nie). Bricht der Job unerwartet ab, erfaehrt das Team es ebenfalls, und der Fehler geht
|
||||||
|
* unveraendert an den Aufrufer.
|
||||||
|
* `geloescht` = Summe aller Loeschzaehler (Buchungen aus Loeschfristen und abgeschalteten Funktionen), `aktiveBetriebe` = Zahl oder null.
|
||||||
* @param {Date} now
|
* @param {Date} now
|
||||||
* @returns {Promise<object | { aus: true } | { uebersprungen: true }>}
|
* @returns {Promise<object | { aus: true } | { uebersprungen: true }>}
|
||||||
*/
|
*/
|
||||||
export async function taeglicherJob(now) {
|
export async function taeglicherJob(now) {
|
||||||
return fuehreAus('taeglicherJob', now, async (lauf) => {
|
let aktuellerLauf = null // gesetzt, sobald der Rahmen den Job startet (fuer die Meldung eines Abbruchs)
|
||||||
const festgefahren = await schritt(lauf, 'Festgefahrene Mails', () => beendeFestgefahrene(now), 0)
|
try {
|
||||||
const { gedeckelt: loeschfristenGedeckelt, ...loeschung } = await loeschfristen(lauf, now)
|
return await fuehreAus('taeglicherJob', now, async (lauf) => {
|
||||||
const { gedeckelt: abgeschaltetGedeckelt, ...abgeschaltet } = await schritt(lauf, 'Abgeschaltete Funktionen', () => loescheAbgeschaltete(lauf), {
|
aktuellerLauf = lauf
|
||||||
funktionen: 0,
|
const festgefahren = await schritt(lauf, 'Festgefahrene Mails', () => beendeFestgefahrene(now), 0)
|
||||||
buchungen: 0,
|
const { gedeckelt: loeschfristenGedeckelt, ...loeschung } = await loeschfristen(lauf, now)
|
||||||
gedeckelt: 0,
|
const { gedeckelt: abgeschaltetGedeckelt, ...abgeschaltet } = await schritt(lauf, 'Abgeschaltete Funktionen', () => loescheAbgeschaltete(lauf), {
|
||||||
|
funktionen: 0,
|
||||||
|
buchungen: 0,
|
||||||
|
gedeckelt: 0,
|
||||||
|
})
|
||||||
|
const aufgeraeumt = await schritt(lauf, 'Warteschlange aufräumen', () => raeumeAuf(now), 0)
|
||||||
|
const zellenErgebnis = await schritt(lauf, 'Verwaiste Zellen', () => raeumeVerwaisteZellen(now), { geloescht: 0, gedeckelt: false })
|
||||||
|
if (zellenErgebnis.gedeckelt) {
|
||||||
|
console.warn(`[termine-jobs] ${lauf.job}: Zellen-Sweep endet am Deckel von ${config.termine.zellenDeckel} je Lauf, der Rest folgt an den nächsten Tagen`)
|
||||||
|
}
|
||||||
|
const aktiveBetriebe = await schritt(lauf, 'Aktive Betriebe', () => zaehleAktiveBetriebe(), null)
|
||||||
|
// gedeckelt: Schritte bzw. Funktionen, die am Deckel endeten (zellen: 1, wenn der Zellen-Sweep daran endete); getrennt vom Loeschzaehler, damit die Summe im Tagesbericht stimmt
|
||||||
|
const gedeckelt = { loeschfristen: loeschfristenGedeckelt, abgeschaltet: abgeschaltetGedeckelt, zellen: zellenErgebnis.gedeckelt ? 1 : 0 }
|
||||||
|
const ergebnis = {
|
||||||
|
festgefahren,
|
||||||
|
loeschfristen: loeschung,
|
||||||
|
gedeckelt,
|
||||||
|
abgeschaltet,
|
||||||
|
aufgeraeumt,
|
||||||
|
zellen: zellenErgebnis.geloescht,
|
||||||
|
geloescht: buchungenGeloescht(loeschung, abgeschaltet),
|
||||||
|
aktiveBetriebe,
|
||||||
|
fehler: lauf.fehler,
|
||||||
|
}
|
||||||
|
// Zuletzt: die Meldung sieht den ganzen Lauf; sie wirft nie, ein Fehler dort stuende nur im Log
|
||||||
|
await schritt(lauf, 'Tagesjob-Meldung', () => meldeTagesjob(ergebnis, now), [])
|
||||||
|
return ergebnis
|
||||||
})
|
})
|
||||||
const aufgeraeumt = await schritt(lauf, 'Warteschlange aufräumen', () => raeumeAuf(now), 0)
|
} catch (err) {
|
||||||
const zellenErgebnis = await schritt(lauf, 'Verwaiste Zellen', () => raeumeVerwaisteZellen(now), { geloescht: 0, gedeckelt: false })
|
// Abbruch ausserhalb der Schritte: das Team erfaehrt es; der Fehler (Text im Log des Aufrufers) geht unveraendert weiter
|
||||||
if (zellenErgebnis.gedeckelt) {
|
if (aktuellerLauf) await schritt(aktuellerLauf, 'Tagesjob-Meldung', () => meldeTagesjob({ abbruch: true, fehler: aktuellerLauf.fehler + 1 }, now), [])
|
||||||
console.warn(`[termine-jobs] ${lauf.job}: Zellen-Sweep endet am Deckel von ${config.termine.zellenDeckel} je Lauf, der Rest folgt an den nächsten Tagen`)
|
throw err
|
||||||
}
|
}
|
||||||
// gedeckelt: Schritte bzw. Funktionen, die am Deckel endeten (zellen: 1, wenn der Zellen-Sweep daran endete); getrennt vom Loeschzaehler, damit die Summe im Tagesbericht stimmt
|
|
||||||
const gedeckelt = { loeschfristen: loeschfristenGedeckelt, abgeschaltet: abgeschaltetGedeckelt, zellen: zellenErgebnis.gedeckelt ? 1 : 0 }
|
|
||||||
return { festgefahren, loeschfristen: loeschung, gedeckelt, abgeschaltet, aufgeraeumt, zellen: zellenErgebnis.geloescht, fehler: lauf.fehler }
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Eine Zeile fuers Log nach dem Tagesjob: nur Zahlen, keine personenbezogenen Daten und keine Buchungs- oder Funktions-IDs
|
* Eine Zeile fuers Log nach dem Tagesjob: nur Zahlen, keine personenbezogenen Daten und keine Buchungs- oder Funktions-IDs
|
||||||
* (Spur der unwiderruflichen Loeschungen). null, wenn der Job nicht gelaufen ist (Modus off, uebersprungen).
|
* (Spur der unwiderruflichen Loeschungen). Ganz am Ende steht „n aktive Betriebe“ (fehlt, wenn die Zahl unbekannt ist).
|
||||||
|
* null, wenn der Job nicht gelaufen ist (Modus off, uebersprungen).
|
||||||
* @param {object} ergebnis Rueckgabe von taeglicherJob
|
* @param {object} ergebnis Rueckgabe von taeglicherJob
|
||||||
* @returns {string|null}
|
* @returns {string|null}
|
||||||
*/
|
*/
|
||||||
@@ -628,14 +660,15 @@ export function tagesbericht(ergebnis) {
|
|||||||
if (!ergebnis || ergebnis.aus || ergebnis.uebersprungen) return null
|
if (!ergebnis || ergebnis.aus || ergebnis.uebersprungen) return null
|
||||||
const l = ergebnis.loeschfristen
|
const l = ergebnis.loeschfristen
|
||||||
const a = ergebnis.abgeschaltet
|
const a = ergebnis.abgeschaltet
|
||||||
const gesamt = Object.values(l).reduce((summe, n) => summe + n, 0) + a.buchungen
|
const gesamt = buchungenGeloescht(l, a)
|
||||||
const amDeckel = Object.values(ergebnis.gedeckelt ?? {}).reduce((summe, n) => summe + n, 0)
|
const amDeckel = Object.values(ergebnis.gedeckelt ?? {}).reduce((summe, n) => summe + n, 0)
|
||||||
const deckel = amDeckel > 0 ? `Löschläufe am Deckel: ${amDeckel}, Rest folgt an den nächsten Tagen, ` : ''
|
const deckel = amDeckel > 0 ? `Löschläufe am Deckel: ${amDeckel}, Rest folgt an den nächsten Tagen, ` : ''
|
||||||
|
const betriebe = Number.isInteger(ergebnis.aktiveBetriebe) && ergebnis.aktiveBetriebe >= 0 ? `, ${ergebnis.aktiveBetriebe} aktive Betriebe` : ''
|
||||||
return (
|
return (
|
||||||
`[termine] Tagesjob: ${gesamt} Buchungen gelöscht (verfallen ${l.verfallen}, abgesagt ${l.abgesagt}, abgelehnt ${l.abgelehnt}, ` +
|
`[termine] Tagesjob: ${gesamt} Buchungen gelöscht (verfallen ${l.verfallen}, abgesagt ${l.abgesagt}, abgelehnt ${l.abgelehnt}, ` +
|
||||||
`erledigt ${l.erledigt}, nichterschienen ${l.nichterschienen}, abgeschaltete Funktionen ${a.buchungen}), ` +
|
`erledigt ${l.erledigt}, nichterschienen ${l.nichterschienen}, abgeschaltete Funktionen ${a.buchungen}), ` +
|
||||||
`${a.funktionen} Funktionen bereinigt, ${ergebnis.zellen} Zellen, ${ergebnis.aufgeraeumt} Outbox, ` +
|
`${a.funktionen} Funktionen bereinigt, ${ergebnis.zellen} Zellen, ${ergebnis.aufgeraeumt} Outbox, ` +
|
||||||
`${ergebnis.festgefahren} Mails nicht zugestellt, ${deckel}${ergebnis.fehler} Fehler`
|
`${ergebnis.festgefahren} Mails nicht zugestellt, ${deckel}${ergebnis.fehler} Fehler${betriebe}`
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+162
-2
@@ -6,7 +6,8 @@ import { schemaKeys } from './support/schema.js'
|
|||||||
import { useTestEnv } from './support/testEnv.js'
|
import { useTestEnv } from './support/testEnv.js'
|
||||||
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
import { TERMINE_DATETIMES } from '../server/services/termine/schema.js'
|
||||||
|
|
||||||
// Betriebspruefung des Terminplaners (Budget, Warteschlange, Anmeldepause) mit Abkuehlzeit fuer die Team-Mail (T20):
|
// Betriebspruefung des Terminplaners (Budget, Warteschlange, Anmeldepause) und Tagesjob-Meldung (Fehler, Loeschdeckel, Zahl der
|
||||||
|
// aktiven Betriebe) mit Abkuehlzeit fuer die Team-Mail (T20):
|
||||||
// Nachbau von Appwrite mit Schema, Mails landen in der Senke (ueber einen Fake, der Sendeversuche zaehlt und gezielt
|
// Nachbau von Appwrite mit Schema, Mails landen in der Senke (ueber einen Fake, der Sendeversuche zaehlt und gezielt
|
||||||
// scheitert), alle Zeiten sind feste Parameter.
|
// scheitert), alle Zeiten sind feste Parameter.
|
||||||
const aw = await startAppwriteMock({
|
const aw = await startAppwriteMock({
|
||||||
@@ -28,7 +29,7 @@ const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
|||||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||||
const { loadTemplate } = await import('../server/services/mailTemplates.js')
|
const { loadTemplate } = await import('../server/services/mailTemplates.js')
|
||||||
const { einreihen, warteAufVersand, warteschlangeAlter, pausiertBis, resetOutboxForTests } = await import('../server/services/termine/outbox.js')
|
const { einreihen, warteAufVersand, warteschlangeAlter, pausiertBis, resetOutboxForTests } = await import('../server/services/termine/outbox.js')
|
||||||
const { pruefeBetrieb, meldeVorfall, resetBetriebForTests } = await import('../server/services/termine/betrieb.js')
|
const { pruefeBetrieb, meldeVorfall, meldeTagesjob, letzterTagesjob, zaehleAktiveBetriebe, resetBetriebForTests } = await import('../server/services/termine/betrieb.js')
|
||||||
const { minutenJob, resetJobsForTests } = await import('../server/services/termine/jobs.js')
|
const { minutenJob, resetJobsForTests } = await import('../server/services/termine/jobs.js')
|
||||||
after(() => setMailTransportForTests(null))
|
after(() => setMailTransportForTests(null))
|
||||||
|
|
||||||
@@ -498,3 +499,162 @@ test('minutenJob: eine unlesbare Warteschlange für die Betriebsprüfung ändert
|
|||||||
assert.equal(ergebnis.verfallen, 0)
|
assert.equal(ergebnis.verfallen, 0)
|
||||||
assert.equal(ergebnis.fehler, 1, 'nur der Schritt Warteschlange zählt; die Betriebsprüfung fügt keinen Fehler hinzu')
|
assert.equal(ergebnis.fehler, 1, 'nur der Schritt Warteschlange zählt; die Betriebsprüfung fügt keinen Fehler hinzu')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Tagesjob-Meldung (meldeTagesjob)
|
||||||
|
|
||||||
|
const TAG = 24 * STUNDE
|
||||||
|
const tag = (n) => plus(n * TAG)
|
||||||
|
/** Ergebnis eines Tagesjobs, soweit meldeTagesjob es liest. */
|
||||||
|
const tagesErgebnis = (over = {}) => ({ fehler: 0, gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 }, geloescht: 0, aktiveBetriebe: 0, ...over })
|
||||||
|
const AM_DECKEL = { loeschfristen: 1, abgeschaltet: 0, zellen: 0 }
|
||||||
|
const textAllerMails = () => teamMails().map((mail) => `${mail.subject}\n${mail.text}`).join('\n')
|
||||||
|
|
||||||
|
test('meldeTagesjob: Fehler im Tagesjob ⇒ Vorfall tagesjob mit Log und Team-Mail (nur Zahlen); ohne Fehler kein Vorfall', async () => {
|
||||||
|
assert.deepEqual(await meldeTagesjob(tagesErgebnis(), JETZT), [])
|
||||||
|
assert.equal(teamMails().length, 0)
|
||||||
|
assert.equal(betriebLog().length, 0)
|
||||||
|
|
||||||
|
assert.deepEqual(await meldeTagesjob(tagesErgebnis({ fehler: 2 }), plus(MIN)), ['tagesjob'])
|
||||||
|
assert.equal(teamMails().length, 1)
|
||||||
|
assert.equal(teamMails()[0].subject, '[Terminplaner] Tagesjob mit Fehlern')
|
||||||
|
assert.match(teamMails()[0].text, /2 Fehler/)
|
||||||
|
assert.equal(betriebLogMit(/Tagesjob.*2 Fehler/).length, 1)
|
||||||
|
assert.doesNotMatch(textAllerMails(), /@(?!webklar)|\b[bfcpr]_[a-z0-9]{4,}/i, 'weder Adresse noch Kennung in der Meldung')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob: ein Abbruch des Tagesjobs ist ein Vorfall tagesjob, auch ohne gezählten Fehler im Ergebnis', async () => {
|
||||||
|
assert.deepEqual(await meldeTagesjob({ abbruch: true, fehler: 1 }, JETZT), ['tagesjob'])
|
||||||
|
assert.equal(teamMails().length, 1)
|
||||||
|
assert.match(teamMails()[0].text, /abgebrochen/)
|
||||||
|
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 1, gedeckelt: 0, geloescht: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob: der Vorfall tagesjob folgt der Abkühlzeit (Log stündlich, Mail erst nach 6 Stunden, am nächsten Tag wieder)', async () => {
|
||||||
|
await meldeTagesjob(tagesErgebnis({ fehler: 1 }), JETZT)
|
||||||
|
await meldeTagesjob(tagesErgebnis({ fehler: 1 }), plus(30 * MIN))
|
||||||
|
assert.equal(teamMails().length, 1)
|
||||||
|
assert.equal(betriebLogMit(/Tagesjob/).length, 1)
|
||||||
|
await meldeTagesjob(tagesErgebnis({ fehler: 1 }), plus(2 * STUNDE))
|
||||||
|
assert.equal(teamMails().length, 1, 'innerhalb der 6 Stunden keine zweite Mail')
|
||||||
|
assert.equal(betriebLogMit(/Tagesjob/).length, 2, 'das Log nach einer Stunde wieder')
|
||||||
|
await meldeTagesjob(tagesErgebnis({ fehler: 1 }), tag(1))
|
||||||
|
assert.equal(teamMails().length, 2, 'der Tagesjob am nächsten Tag meldet wieder')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob: Löschdeckel an 2 Tagesjobs in Folge ⇒ kein Vorfall, am 3. ⇒ deckel; die Folge hält an', async () => {
|
||||||
|
const gedeckelt = tagesErgebnis({ gedeckelt: AM_DECKEL })
|
||||||
|
assert.deepEqual(await meldeTagesjob(gedeckelt, tag(0)), [])
|
||||||
|
assert.deepEqual(await meldeTagesjob(gedeckelt, tag(1)), [])
|
||||||
|
assert.equal(teamMails().length, 0)
|
||||||
|
assert.deepEqual(await meldeTagesjob(gedeckelt, tag(2)), ['deckel'])
|
||||||
|
assert.equal(teamMails().length, 1)
|
||||||
|
assert.equal(teamMails()[0].subject, '[Terminplaner] Löschläufe am Deckel')
|
||||||
|
assert.match(teamMails()[0].text, /3 Tagesjobs in Folge/)
|
||||||
|
assert.equal(betriebLogMit(/Deckel/).length, 1)
|
||||||
|
|
||||||
|
assert.deepEqual(await meldeTagesjob(gedeckelt, tag(3)), ['deckel'], 'solange der Rückstand anhält, bleibt es ein Vorfall')
|
||||||
|
assert.equal(teamMails().length, 2, 'am nächsten Tag nach der Abkühlzeit wieder eine Mail')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob: ein Tagesjob ohne Deckel unterbricht die Folge; jeder Bereich (Fristen, abgeschaltete Funktionen, Zellen) zählt', async () => {
|
||||||
|
const frei = tagesErgebnis()
|
||||||
|
const mitDeckel = (gedeckelt) => tagesErgebnis({ gedeckelt })
|
||||||
|
assert.deepEqual(await meldeTagesjob(mitDeckel({ loeschfristen: 1, abgeschaltet: 0, zellen: 0 }), tag(0)), [])
|
||||||
|
assert.deepEqual(await meldeTagesjob(mitDeckel({ loeschfristen: 0, abgeschaltet: 3, zellen: 0 }), tag(1)), [])
|
||||||
|
assert.deepEqual(await meldeTagesjob(frei, tag(2)), [])
|
||||||
|
assert.deepEqual(await meldeTagesjob(mitDeckel({ loeschfristen: 0, abgeschaltet: 0, zellen: 1 }), tag(3)), [])
|
||||||
|
assert.deepEqual(await meldeTagesjob(mitDeckel(AM_DECKEL), tag(4)), [])
|
||||||
|
assert.deepEqual(await meldeTagesjob(mitDeckel({ loeschfristen: 2, abgeschaltet: 1, zellen: 1 }), tag(5)), ['deckel'], 'drei Tage in Folge, jeder mit anderem Bereich')
|
||||||
|
assert.equal(teamMails().length, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob: ein abgebrochener Tagesjob verändert die Deckel-Folge nicht', async () => {
|
||||||
|
const gedeckelt = tagesErgebnis({ gedeckelt: AM_DECKEL })
|
||||||
|
await meldeTagesjob(gedeckelt, tag(0))
|
||||||
|
await meldeTagesjob(gedeckelt, tag(1))
|
||||||
|
await meldeTagesjob({ abbruch: true, fehler: 1 }, tag(2))
|
||||||
|
assert.deepEqual(await meldeTagesjob(gedeckelt, tag(3)), ['deckel'], 'Abbruch unterbricht die Folge nicht und zählt nicht mit')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob: 19 aktive Betriebe ⇒ kein Vorfall, 20 ⇒ betriebe (D8) mit Hinweis auf den Umstieg ab 25; unbekannte Zahl (null) ⇒ nichts', async () => {
|
||||||
|
assert.deepEqual(await meldeTagesjob(tagesErgebnis({ aktiveBetriebe: 19 }), JETZT), [])
|
||||||
|
assert.deepEqual(await meldeTagesjob(tagesErgebnis({ aktiveBetriebe: null }), plus(MIN)), [])
|
||||||
|
assert.equal(teamMails().length, 0)
|
||||||
|
assert.deepEqual(await meldeTagesjob(tagesErgebnis({ aktiveBetriebe: 20 }), plus(2 * MIN)), ['betriebe'])
|
||||||
|
assert.equal(teamMails().length, 1)
|
||||||
|
assert.equal(teamMails()[0].subject, '[Terminplaner] Viele aktive Betriebe')
|
||||||
|
assert.match(teamMails()[0].text, /20 aktive Betriebe/)
|
||||||
|
assert.match(teamMails()[0].text, /25/)
|
||||||
|
assert.equal(betriebLogMit(/20 aktive Betriebe/).length, 1)
|
||||||
|
|
||||||
|
assert.deepEqual(await meldeTagesjob(tagesErgebnis({ aktiveBetriebe: 21 }), plus(3 * MIN)), ['betriebe'])
|
||||||
|
assert.equal(teamMails().length, 1, 'die Abkühlzeit gilt je Vorfall')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob: mehrere Vorfälle zugleich ⇒ je eine Meldung mit eigener Überschrift', async () => {
|
||||||
|
const erg = tagesErgebnis({ fehler: 1, gedeckelt: AM_DECKEL, aktiveBetriebe: 22 })
|
||||||
|
assert.deepEqual(await meldeTagesjob(erg, tag(0)), ['tagesjob', 'betriebe'])
|
||||||
|
assert.deepEqual(await meldeTagesjob(erg, tag(1)), ['tagesjob', 'betriebe'])
|
||||||
|
assert.equal(teamMails().length, 4, 'je Tag eine Mail je Vorfall (Abkühlzeit 6 Stunden)')
|
||||||
|
assert.deepEqual(await meldeTagesjob(erg, tag(2)), ['tagesjob', 'deckel', 'betriebe'], 'am dritten Tag kommt deckel dazu')
|
||||||
|
assert.equal(teamMails().length, 7)
|
||||||
|
assert.deepEqual(
|
||||||
|
[...new Set(teamMails().map((mail) => mail.subject))].sort(),
|
||||||
|
['[Terminplaner] Löschläufe am Deckel', '[Terminplaner] Tagesjob mit Fehlern', '[Terminplaner] Viele aktive Betriebe'],
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('letzterTagesjob: null bis zur ersten Meldung, danach nur Zählungen { at, fehler, gedeckelt, geloescht } als Zahlen (gedeckelt = Summe der Bereiche)', async () => {
|
||||||
|
assert.equal(letzterTagesjob(), null)
|
||||||
|
await meldeTagesjob(tagesErgebnis({ fehler: 3, gedeckelt: { loeschfristen: 2, abgeschaltet: 1, zellen: 1 }, geloescht: 17, aktiveBetriebe: 5 }), JETZT)
|
||||||
|
const letzter = letzterTagesjob()
|
||||||
|
assert.deepEqual(letzter, { at: JETZT.toISOString(), fehler: 3, gedeckelt: 4, geloescht: 17 })
|
||||||
|
for (const schluessel of ['fehler', 'gedeckelt', 'geloescht']) assert.equal(typeof letzter[schluessel], 'number', schluessel)
|
||||||
|
letzter.fehler = 99
|
||||||
|
assert.equal(letzterTagesjob().fehler, 3, 'der Aufrufer bekommt eine Kopie')
|
||||||
|
|
||||||
|
await meldeTagesjob(tagesErgebnis({ geloescht: 4 }), plus(TAG))
|
||||||
|
assert.deepEqual(letzterTagesjob(), { at: plus(TAG).toISOString(), fehler: 0, gedeckelt: 0, geloescht: 4 }, 'immer der letzte Lauf')
|
||||||
|
resetBetriebForTests()
|
||||||
|
assert.equal(letzterTagesjob(), null)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob wirft nie: scheitert die Team-Mail, steht das im Log, der Lauf ist gemerkt und die Mail wird nicht neu versucht', async () => {
|
||||||
|
tr.wirf = (mail) => (ist20(mail) ? new Error('Verbindung abgelehnt') : null)
|
||||||
|
assert.deepEqual(await meldeTagesjob(tagesErgebnis({ fehler: 1 }), JETZT), ['tagesjob'])
|
||||||
|
assert.equal(tr.teamVersuche, 1)
|
||||||
|
assert.equal(teamMails().length, 0)
|
||||||
|
assert.equal(betriebLogMit(/Team-Mail.*nicht gesendet/).length, 1)
|
||||||
|
assert.equal(letzterTagesjob().fehler, 1)
|
||||||
|
await meldeTagesjob(tagesErgebnis({ fehler: 1 }), plus(MIN))
|
||||||
|
assert.equal(tr.teamVersuche, 1)
|
||||||
|
|
||||||
|
config.chatNotify.emails = []
|
||||||
|
assert.deepEqual(await meldeTagesjob(tagesErgebnis({ aktiveBetriebe: 30 }), plus(2 * MIN)), ['betriebe'], 'ohne Empfänger bleibt das Log')
|
||||||
|
assert.equal(betriebLogMit(/30 aktive Betriebe/).length, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('meldeTagesjob: unbrauchbare Ergebnisse ergeben keinen Wurf und keinen Vorfall; eine ungültige Uhrzeit ist ein TypeError', async () => {
|
||||||
|
for (const unbrauchbar of [undefined, null, {}, 'x', { fehler: 'viele', gedeckelt: 'ja', geloescht: -3, aktiveBetriebe: 'zwanzig' }, { fehler: -1, aktiveBetriebe: 2.5 }]) {
|
||||||
|
assert.deepEqual(await meldeTagesjob(unbrauchbar, JETZT), [], JSON.stringify(unbrauchbar))
|
||||||
|
}
|
||||||
|
assert.equal(teamMails().length, 0)
|
||||||
|
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 0, gedeckelt: 0, geloescht: 0 })
|
||||||
|
await assert.rejects(() => meldeTagesjob(tagesErgebnis(), 'jetzt'), TypeError)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('zaehleAktiveBetriebe: zählt nur Termine-Dokumente im Zustand active (kein Entwurf, nicht abgeschaltet, keine andere Funktion); Appwrite 500 wirft', async () => {
|
||||||
|
const doc = (id, over) => aw.seed('portalFeatures', { $id: id, customerId: 'c_k', projectId: `p_${id}`, featureKey: 'termine', state: 'active', ...over })
|
||||||
|
assert.equal(await zaehleAktiveBetriebe(), 0)
|
||||||
|
doc('f_aktiv01')
|
||||||
|
doc('f_aktiv02')
|
||||||
|
doc('f_aus0001', { state: 'disabled' })
|
||||||
|
doc('f_bestellt', { state: 'ordered' })
|
||||||
|
doc('f_einbau01', { state: 'setup' })
|
||||||
|
doc('f_entwurf1', { state: 'off' })
|
||||||
|
doc('f_news0001', { featureKey: 'news' })
|
||||||
|
assert.equal(await zaehleAktiveBetriebe(), 2)
|
||||||
|
|
||||||
|
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt' })
|
||||||
|
await assert.rejects(() => zaehleAktiveBetriebe(), (err) => err.status === 500)
|
||||||
|
})
|
||||||
+147
-7
@@ -28,6 +28,7 @@ const { deleteDocument } = await import('../server/services/appwriteAdmin.js')
|
|||||||
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
const { setMailTransportForTests } = await import('../server/services/mailer.js')
|
||||||
const { normalisiereEinstellung } = await import('../server/services/termine/einstellungen.js')
|
const { normalisiereEinstellung } = await import('../server/services/termine/einstellungen.js')
|
||||||
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
|
const { resetOutboxForTests, warteAufVersand } = await import('../server/services/termine/outbox.js')
|
||||||
|
const { letzterTagesjob, resetBetriebForTests } = await import('../server/services/termine/betrieb.js')
|
||||||
const { neuesToken, verwaltungsToken, buchungsNummer } = await import('../server/services/termine/token.js')
|
const { neuesToken, verwaltungsToken, buchungsNummer } = await import('../server/services/termine/token.js')
|
||||||
const { speichereEinstellung, resetKontextForTests } = await import('../server/services/termine/kontext.js')
|
const { speichereEinstellung, resetKontextForTests } = await import('../server/services/termine/kontext.js')
|
||||||
const { belegeZellen } = await import('../server/services/termine/zellen.js')
|
const { belegeZellen } = await import('../server/services/termine/zellen.js')
|
||||||
@@ -116,6 +117,7 @@ beforeEach(async () => {
|
|||||||
}
|
}
|
||||||
resetKontextForTests()
|
resetKontextForTests()
|
||||||
resetOutboxForTests()
|
resetOutboxForTests()
|
||||||
|
resetBetriebForTests()
|
||||||
resetJobsForTests()
|
resetJobsForTests()
|
||||||
aw.resetListCalls()
|
aw.resetListCalls()
|
||||||
config.termine.mode = 'pilot'
|
config.termine.mode = 'pilot'
|
||||||
@@ -1005,6 +1007,7 @@ test('taeglicherJob: gedeckelt zählt je Schritt und verfälscht den Löschzähl
|
|||||||
const erg = await taeglicherJob(JETZT)
|
const erg = await taeglicherJob(JETZT)
|
||||||
assert.deepEqual(erg.loeschfristen, { verfallen: 2, abgesagt: 2, abgelehnt: 1, erledigt: 0, nichterschienen: 0 }, 'nur Löschzähler')
|
assert.deepEqual(erg.loeschfristen, { verfallen: 2, abgesagt: 2, abgelehnt: 1, erledigt: 0, nichterschienen: 0 }, 'nur Löschzähler')
|
||||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 2, abgeschaltet: 0, zellen: 0 })
|
assert.deepEqual(erg.gedeckelt, { loeschfristen: 2, abgeschaltet: 0, zellen: 0 })
|
||||||
|
assert.equal(erg.geloescht, 5, 'geloescht = Summe aller Löschzähler, gedeckelt zählt nicht mit')
|
||||||
assert.match(tagesbericht(erg), /^\[termine\] Tagesjob: 5 Buchungen gelöscht \(verfallen 2, abgesagt 2, abgelehnt 1, erledigt 0, nichterschienen 0, abgeschaltete Funktionen 0\)/)
|
assert.match(tagesbericht(erg), /^\[termine\] Tagesjob: 5 Buchungen gelöscht \(verfallen 2, abgesagt 2, abgelehnt 1, erledigt 0, nichterschienen 0, abgeschaltete Funktionen 0\)/)
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -1301,7 +1304,7 @@ test('taeglicherJob: gedeckelt zählt je Schritt; der Tagesbericht nennt „Lös
|
|||||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 2, abgeschaltet: 0, zellen: 0 })
|
assert.deepEqual(erg.gedeckelt, { loeschfristen: 2, abgeschaltet: 0, zellen: 0 })
|
||||||
let zeile = tagesbericht(erg)
|
let zeile = tagesbericht(erg)
|
||||||
assert.match(zeile, /^\[termine\] Tagesjob: 4 Buchungen gelöscht \(/)
|
assert.match(zeile, /^\[termine\] Tagesjob: 4 Buchungen gelöscht \(/)
|
||||||
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 2, Rest folgt an den nächsten Tagen, 0 Fehler$/)
|
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 2, Rest folgt an den nächsten Tagen, 0 Fehler, 1 aktive Betriebe$/)
|
||||||
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
||||||
|
|
||||||
config.termine.loeschDeckel = DECKEL_VORHER
|
config.termine.loeschDeckel = DECKEL_VORHER
|
||||||
@@ -1309,7 +1312,7 @@ test('taeglicherJob: gedeckelt zählt je Schritt; der Tagesbericht nennt „Lös
|
|||||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
|
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
|
||||||
zeile = tagesbericht(erg)
|
zeile = tagesbericht(erg)
|
||||||
assert.doesNotMatch(zeile, /Deckel/, 'ohne Deckel bleibt die Zeile unverändert')
|
assert.doesNotMatch(zeile, /Deckel/, 'ohne Deckel bleibt die Zeile unverändert')
|
||||||
assert.match(zeile, /, 0 Mails nicht zugestellt, 0 Fehler$/)
|
assert.match(zeile, /, 0 Mails nicht zugestellt, 0 Fehler, 1 aktive Betriebe$/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('tagesbericht: das Segment zählt die Schritte aller Bereiche und fehlt bei gedeckelt 0', () => {
|
test('tagesbericht: das Segment zählt die Schritte aller Bereiche und fehlt bei gedeckelt 0', () => {
|
||||||
@@ -1320,11 +1323,12 @@ test('tagesbericht: das Segment zählt die Schritte aller Bereiche und fehlt bei
|
|||||||
aufgeraeumt: 7,
|
aufgeraeumt: 7,
|
||||||
zellen: 8,
|
zellen: 8,
|
||||||
fehler: 9,
|
fehler: 9,
|
||||||
|
aktiveBetriebe: 12,
|
||||||
}
|
}
|
||||||
const ohne = tagesbericht({ ...basis, gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 } })
|
const ohne = tagesbericht({ ...basis, gedeckelt: { loeschfristen: 0, abgeschaltet: 0, zellen: 0 } })
|
||||||
assert.equal(
|
assert.equal(
|
||||||
ohne,
|
ohne,
|
||||||
'[termine] Tagesjob: 21 Buchungen gelöscht (verfallen 1, abgesagt 2, abgelehnt 3, erledigt 4, nichterschienen 5, abgeschaltete Funktionen 6), 1 Funktionen bereinigt, 8 Zellen, 7 Outbox, 3 Mails nicht zugestellt, 9 Fehler',
|
'[termine] Tagesjob: 21 Buchungen gelöscht (verfallen 1, abgesagt 2, abgelehnt 3, erledigt 4, nichterschienen 5, abgeschaltete Funktionen 6), 1 Funktionen bereinigt, 8 Zellen, 7 Outbox, 3 Mails nicht zugestellt, 9 Fehler, 12 aktive Betriebe',
|
||||||
)
|
)
|
||||||
const mit = tagesbericht({ ...basis, gedeckelt: { loeschfristen: 2, abgeschaltet: 1, zellen: 1 } })
|
const mit = tagesbericht({ ...basis, gedeckelt: { loeschfristen: 2, abgeschaltet: 1, zellen: 1 } })
|
||||||
assert.equal(mit, ohne.replace(', 9 Fehler', ', Löschläufe am Deckel: 4, Rest folgt an den nächsten Tagen, 9 Fehler'))
|
assert.equal(mit, ohne.replace(', 9 Fehler', ', Löschläufe am Deckel: 4, Rest folgt an den nächsten Tagen, 9 Fehler'))
|
||||||
@@ -1644,6 +1648,8 @@ test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaist
|
|||||||
assert.equal('gedeckelt' in erg.loeschfristen, false, 'gedeckelt steht neben den Löschzählern, nicht darin')
|
assert.equal('gedeckelt' in erg.loeschfristen, false, 'gedeckelt steht neben den Löschzählern, nicht darin')
|
||||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
|
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
|
||||||
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 1 })
|
assert.deepEqual(erg.abgeschaltet, { funktionen: 1, buchungen: 1 })
|
||||||
|
assert.equal(erg.geloescht, 3, 'verfallen 1 + erledigt 1 + abgeschaltete Funktion 1')
|
||||||
|
assert.equal(erg.aktiveBetriebe, 1, 'F1 ist aktiv, F2 abgeschaltet')
|
||||||
assert.equal(erg.aufgeraeumt, 1)
|
assert.equal(erg.aufgeraeumt, 1)
|
||||||
assert.ok(erg.zellen >= 1)
|
assert.ok(erg.zellen >= 1)
|
||||||
|
|
||||||
@@ -1662,7 +1668,7 @@ test('taeglicherJob: Löschfristen, Warteschlange, festgefahrene Mails, verwaist
|
|||||||
const zeile = tagesbericht(erg)
|
const zeile = tagesbericht(erg)
|
||||||
assert.equal(
|
assert.equal(
|
||||||
zeile,
|
zeile,
|
||||||
`[termine] Tagesjob: 3 Buchungen gelöscht (verfallen 1, abgesagt 0, abgelehnt 0, erledigt 1, nichterschienen 0, abgeschaltete Funktionen 1), 1 Funktionen bereinigt, ${erg.zellen} Zellen, 1 Outbox, 1 Mails nicht zugestellt, 0 Fehler`,
|
`[termine] Tagesjob: 3 Buchungen gelöscht (verfallen 1, abgesagt 0, abgelehnt 0, erledigt 1, nichterschienen 0, abgeschaltete Funktionen 1), 1 Funktionen bereinigt, ${erg.zellen} Zellen, 1 Outbox, 1 Mails nicht zugestellt, 0 Fehler, 1 aktive Betriebe`,
|
||||||
)
|
)
|
||||||
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
||||||
})
|
})
|
||||||
@@ -1677,7 +1683,7 @@ test('taeglicherJob: abgeschaltete Funktion am Deckel ⇒ gedeckelt.abgeschaltet
|
|||||||
assert.equal(erg.fehler, 0)
|
assert.equal(erg.fehler, 0)
|
||||||
let zeile = tagesbericht(erg)
|
let zeile = tagesbericht(erg)
|
||||||
assert.match(zeile, /^\[termine\] Tagesjob: 2 Buchungen gelöscht \(.*abgeschaltete Funktionen 2\), 1 Funktionen bereinigt, /)
|
assert.match(zeile, /^\[termine\] Tagesjob: 2 Buchungen gelöscht \(.*abgeschaltete Funktionen 2\), 1 Funktionen bereinigt, /)
|
||||||
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 0 Fehler$/)
|
assert.match(zeile, /, 0 Mails nicht zugestellt, Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 0 Fehler, \d+ aktive Betriebe$/)
|
||||||
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
assert.doesNotMatch(zeile, /\bb_|\bf_|@|Mustermann|Salon/)
|
||||||
|
|
||||||
erg = await taeglicherJob(JETZT)
|
erg = await taeglicherJob(JETZT)
|
||||||
@@ -1718,7 +1724,7 @@ test('taeglicherJob: Zellen-Sweep am Deckel ⇒ gedeckelt.zellen zählt, zellen
|
|||||||
assert.doesNotMatch(warnLog().join('\n'), /\bb_|\bf_|\br_|\bc_/, 'die Warnung nennt keine Kennungen')
|
assert.doesNotMatch(warnLog().join('\n'), /\bb_|\bf_|\br_|\bc_/, 'die Warnung nennt keine Kennungen')
|
||||||
const zeile = tagesbericht(erg)
|
const zeile = tagesbericht(erg)
|
||||||
assert.match(zeile, /, 250 Zellen, /)
|
assert.match(zeile, /, 250 Zellen, /)
|
||||||
assert.match(zeile, /Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 0 Fehler$/)
|
assert.match(zeile, /Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 0 Fehler, 0 aktive Betriebe$/)
|
||||||
|
|
||||||
erg = await taeglicherJob(JETZT)
|
erg = await taeglicherJob(JETZT)
|
||||||
assert.equal(erg.zellen, 250)
|
assert.equal(erg.zellen, 250)
|
||||||
@@ -1761,7 +1767,7 @@ test('taeglicherJob: ist die Liste der abgeschalteten Funktionen nicht lesbar, z
|
|||||||
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
|
assert.deepEqual(erg.gedeckelt, { loeschfristen: 0, abgeschaltet: 0, zellen: 0 })
|
||||||
assert.equal(erg.fehler, 1)
|
assert.equal(erg.fehler, 1)
|
||||||
assert.equal(buchungen().length, 3)
|
assert.equal(buchungen().length, 3)
|
||||||
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, 1 Fehler$/)
|
assert.match(tagesbericht(erg), /, 0 Mails nicht zugestellt, 1 Fehler, \d+ aktive Betriebe$/)
|
||||||
})
|
})
|
||||||
|
|
||||||
test('tagesbericht: keine Zeile, wenn der Job nicht gelaufen ist', () => {
|
test('tagesbericht: keine Zeile, wenn der Job nicht gelaufen ist', () => {
|
||||||
@@ -1770,6 +1776,140 @@ test('tagesbericht: keine Zeile, wenn der Job nicht gelaufen ist', () => {
|
|||||||
assert.equal(tagesbericht(undefined), null)
|
assert.equal(tagesbericht(undefined), null)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
test('tagesbericht: „n aktive Betriebe“ steht ganz am Ende, hinter den Fehlern; ohne Zahl (null oder fehlt) fehlt das Segment', () => {
|
||||||
|
const basis = {
|
||||||
|
festgefahren: 0,
|
||||||
|
loeschfristen: { verfallen: 0, abgesagt: 0, abgelehnt: 0, erledigt: 0, nichterschienen: 0 },
|
||||||
|
gedeckelt: { loeschfristen: 1, abgeschaltet: 0, zellen: 0 },
|
||||||
|
abgeschaltet: { funktionen: 0, buchungen: 0 },
|
||||||
|
aufgeraeumt: 0,
|
||||||
|
zellen: 0,
|
||||||
|
fehler: 2,
|
||||||
|
}
|
||||||
|
assert.match(tagesbericht({ ...basis, aktiveBetriebe: 0 }), /, Löschläufe am Deckel: 1, Rest folgt an den nächsten Tagen, 2 Fehler, 0 aktive Betriebe$/)
|
||||||
|
assert.match(tagesbericht({ ...basis, aktiveBetriebe: 31 }), /, 2 Fehler, 31 aktive Betriebe$/)
|
||||||
|
for (const unbekannt of [null, undefined, 'viele', -1]) {
|
||||||
|
assert.match(tagesbericht({ ...basis, aktiveBetriebe: unbekannt }), /, 2 Fehler$/, String(unbekannt))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
test('taeglicherJob: aktiveBetriebe zählt nur aktive Termine-Dokumente (kein abgeschalteter Betrieb, keine andere Funktion); die Zeile nennt die Zahl ohne Kennungen', async () => {
|
||||||
|
await richteFunktionEin(F1)
|
||||||
|
await richteFunktionEin(F2)
|
||||||
|
await richteFunktionEin(F3, { state: 'disabled', disabledAt: iso(plus(JETZT, -2 * TAG)) }) // erst 2 Tage abgeschaltet: bleibt, zählt nicht
|
||||||
|
aw.seed('portalFeatures', feature('f_p_projekt04_termine', { featureKey: 'news' }))
|
||||||
|
aw.seed('portalFeatures', feature('f_p_projekt05_termine', { state: 'ordered' }))
|
||||||
|
|
||||||
|
const erg = await taeglicherJob(JETZT)
|
||||||
|
assert.equal(erg.aktiveBetriebe, 2)
|
||||||
|
assert.equal(erg.fehler, 0)
|
||||||
|
const zeile = tagesbericht(erg)
|
||||||
|
assert.match(zeile, /, 0 Fehler, 2 aktive Betriebe$/)
|
||||||
|
assert.doesNotMatch(zeile, /\bb_|\bf_|\bp_|\bc_|@|Mustermann|Salon/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('taeglicherJob: ist die Zahl der aktiven Betriebe nicht lesbar, ist aktiveBetriebe null, der Fehler gezählt, der Rest des Laufs fertig und die Zeile ohne Segment', async () => {
|
||||||
|
await legeLoeschFall('b_ver000', 'verfallen', 1)
|
||||||
|
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 2 }) // 1. GET = Funktionen der Löschfristen, 2. = abgeschaltete, 3. = Zählung
|
||||||
|
const erg = await taeglicherJob(JETZT)
|
||||||
|
assert.equal(erg.aktiveBetriebe, null)
|
||||||
|
assert.equal(erg.fehler, 1)
|
||||||
|
assert.equal(erg.loeschfristen.verfallen, 1, 'die Löschung lief vorher')
|
||||||
|
assert.equal(erg.geloescht, 1)
|
||||||
|
assert.doesNotMatch(tagesbericht(erg), /aktive Betriebe/)
|
||||||
|
assert.ok(fehlerLog().some((zeile) => /Aktive Betriebe/.test(zeile) && /Nachbau kaputt/.test(zeile)), 'der Fehler steht im Log')
|
||||||
|
})
|
||||||
|
|
||||||
|
// ---------------------------------------------------------------- Tagesjob-Meldung ans Team (meldeTagesjob)
|
||||||
|
|
||||||
|
const team20 = () => sink.sent.filter((mail) => /^\[Terminplaner\]/.test(mail.subject))
|
||||||
|
|
||||||
|
test('taeglicherJob meldet am Ende: Fehler im Lauf ⇒ Team-Mail „Tagesjob mit Fehlern“, letzterTagesjob trägt nur Zählungen; ein sauberer Lauf meldet nichts', async () => {
|
||||||
|
assert.equal(letzterTagesjob(), null)
|
||||||
|
await legeLoeschFall('b_ver000', 'verfallen', 1)
|
||||||
|
aw.failNext({ collection: 'portalFeatures', method: 'GET', status: 500, message: 'Nachbau kaputt', after: 2 })
|
||||||
|
const erg = await taeglicherJob(JETZT)
|
||||||
|
assert.equal(erg.fehler, 1)
|
||||||
|
assert.equal(team20().length, 1)
|
||||||
|
assert.equal(team20()[0].subject, '[Terminplaner] Tagesjob mit Fehlern')
|
||||||
|
assert.match(team20()[0].text, /1 Fehler\b/)
|
||||||
|
assert.doesNotMatch(team20()[0].text, /Nachbau kaputt|\bb_|\bf_|@/, 'weder Fehlertext noch Kennungen noch Adressen in der Mail')
|
||||||
|
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 1, gedeckelt: 0, geloescht: 1 })
|
||||||
|
|
||||||
|
const spaeter = plus(JETZT, TAG)
|
||||||
|
const sauber = await taeglicherJob(spaeter)
|
||||||
|
assert.equal(sauber.fehler, 0)
|
||||||
|
assert.equal(team20().length, 1, 'ein sauberer Lauf schickt nichts')
|
||||||
|
assert.deepEqual(letzterTagesjob(), { at: spaeter.toISOString(), fehler: 0, gedeckelt: 0, geloescht: 0 })
|
||||||
|
})
|
||||||
|
|
||||||
|
test('taeglicherJob: Löschdeckel an 3 Tagesläufen in Folge ⇒ Team-Mail „Löschläufe am Deckel“; ein Lauf ohne Deckel beginnt die Folge neu', async () => {
|
||||||
|
config.termine.loeschDeckel = 2
|
||||||
|
for (let i = 0; i < 12; i += 1) await legeLoeschFall(`b_ver${nr(i)}`, 'verfallen', 1 + i)
|
||||||
|
|
||||||
|
assert.deepEqual((await taeglicherJob(plus(JETZT, 0 * TAG))).gedeckelt, { loeschfristen: 1, abgeschaltet: 0, zellen: 0 })
|
||||||
|
await taeglicherJob(plus(JETZT, 1 * TAG))
|
||||||
|
assert.equal(team20().length, 0, 'nach zwei Tagen noch nichts')
|
||||||
|
await taeglicherJob(plus(JETZT, 2 * TAG))
|
||||||
|
assert.equal(team20().length, 1)
|
||||||
|
assert.equal(team20()[0].subject, '[Terminplaner] Löschläufe am Deckel')
|
||||||
|
assert.match(team20()[0].text, /3 Tagesjobs in Folge/)
|
||||||
|
assert.equal(letzterTagesjob().gedeckelt, 1)
|
||||||
|
|
||||||
|
config.termine.loeschDeckel = DECKEL_VORHER
|
||||||
|
await taeglicherJob(plus(JETZT, 3 * TAG)) // räumt den Rest ohne Deckel
|
||||||
|
assert.equal(letzterTagesjob().gedeckelt, 0)
|
||||||
|
assert.equal(buchungen().length, 0)
|
||||||
|
config.termine.loeschDeckel = 2
|
||||||
|
for (let i = 0; i < 6; i += 1) await legeLoeschFall(`b_neu${nr(i)}`, 'verfallen', 1 + i)
|
||||||
|
await taeglicherJob(plus(JETZT, 4 * TAG))
|
||||||
|
await taeglicherJob(plus(JETZT, 5 * TAG))
|
||||||
|
assert.equal(team20().length, 1, 'zwei neue Deckeltage sind keine Folge von drei')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('taeglicherJob: ab 20 aktiven Betrieben ⇒ Team-Mail „Viele aktive Betriebe“, bei 19 nicht', async () => {
|
||||||
|
for (let i = 1; i <= 19; i += 1) aw.seed('portalFeatures', feature(`f_p_betrieb${nr(i)}_termine`))
|
||||||
|
const erg19 = await taeglicherJob(JETZT)
|
||||||
|
assert.equal(erg19.aktiveBetriebe, 19)
|
||||||
|
assert.equal(team20().length, 0)
|
||||||
|
|
||||||
|
aw.seed('portalFeatures', feature('f_p_betrieb020_termine'))
|
||||||
|
const erg20 = await taeglicherJob(plus(JETZT, TAG))
|
||||||
|
assert.equal(erg20.aktiveBetriebe, 20)
|
||||||
|
assert.equal(team20().length, 1)
|
||||||
|
assert.equal(team20()[0].subject, '[Terminplaner] Viele aktive Betriebe')
|
||||||
|
assert.match(team20()[0].text, /20 aktive Betriebe/)
|
||||||
|
assert.match(tagesbericht(erg20), /, 0 Fehler, 20 aktive Betriebe$/)
|
||||||
|
})
|
||||||
|
|
||||||
|
test('taeglicherJob: bricht der Job unerwartet ab, erfährt das Team es (Vorfall tagesjob), der Fehler geht weiter an den Aufrufer und die Überlappungssperre ist frei', async () => {
|
||||||
|
Object.defineProperty(config.termine, 'loeschDeckel', {
|
||||||
|
configurable: true,
|
||||||
|
enumerable: true,
|
||||||
|
get() {
|
||||||
|
throw new Error('Unerwartet kaputt')
|
||||||
|
},
|
||||||
|
})
|
||||||
|
try {
|
||||||
|
await assert.rejects(() => taeglicherJob(JETZT), /Unerwartet kaputt/)
|
||||||
|
} finally {
|
||||||
|
Object.defineProperty(config.termine, 'loeschDeckel', { configurable: true, enumerable: true, writable: true, value: DECKEL_VORHER })
|
||||||
|
}
|
||||||
|
assert.equal(team20().length, 1)
|
||||||
|
assert.equal(team20()[0].subject, '[Terminplaner] Tagesjob mit Fehlern')
|
||||||
|
assert.match(team20()[0].text, /abgebrochen/)
|
||||||
|
assert.doesNotMatch(team20()[0].text, /Unerwartet kaputt/, 'der Fehlertext steht im Log des Aufrufers, nicht in der Mail')
|
||||||
|
assert.deepEqual(letzterTagesjob(), { at: JETZT.toISOString(), fehler: 1, gedeckelt: 0, geloescht: 0 })
|
||||||
|
assert.equal((await taeglicherJob(plus(JETZT, MIN))).uebersprungen, undefined, 'danach läuft der Job wieder')
|
||||||
|
})
|
||||||
|
|
||||||
|
test('taeglicherJob im Modus off und bei Überlappung: keine Tagesjob-Meldung, letzterTagesjob bleibt leer', async () => {
|
||||||
|
config.termine.mode = 'off'
|
||||||
|
assert.deepEqual(await taeglicherJob(JETZT), { aus: true })
|
||||||
|
assert.equal(letzterTagesjob(), null)
|
||||||
|
assert.equal(team20().length, 0)
|
||||||
|
})
|
||||||
|
|
||||||
test('taeglicherJob: festgefahrene Mail wird fehler und erst 48 Stunden später gelöscht (nicht im selben Lauf)', async () => {
|
test('taeglicherJob: festgefahrene Mail wird fehler und erst 48 Stunden später gelöscht (nicht im selben Lauf)', async () => {
|
||||||
legeEintrag('o_alt', { createdAt: iso(plus(JETZT, -49 * STUNDE)), naechsterVersuch: iso(plus(JETZT, -48 * STUNDE)) })
|
legeEintrag('o_alt', { createdAt: iso(plus(JETZT, -49 * STUNDE)), naechsterVersuch: iso(plus(JETZT, -48 * STUNDE)) })
|
||||||
legeEintrag('o_neu', { createdAt: iso(plus(JETZT, -47 * STUNDE)), naechsterVersuch: iso(plus(JETZT, 10 * MIN)) })
|
legeEintrag('o_neu', { createdAt: iso(plus(JETZT, -47 * STUNDE)), naechsterVersuch: iso(plus(JETZT, 10 * MIN)) })
|
||||||
|
|||||||
Reference in new issue
Block a user