AGB-Kopie: Quelle docs/rechtliches/agb.md wortgleich mit der Website (Fassung 2026-09-30), Tests gegen AGB.tsx
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
445d234695
commit
edccd4d8af
2 files changed
+226
No files matched your search
@@ -349,3 +349,80 @@ test('belehrung_gleich_funktionen: Belehrung und Formular sind wörtlich die Bel
|
||||
assert.equal(absaetze.join('\n\n'), widerrufsbelehrungFunktion())
|
||||
assert.deepEqual(B.abschnitte.map((a) => a.ueberschrift), ['Widerrufsrecht', 'Folgen des Widerrufs'])
|
||||
})
|
||||
|
||||
// ---------------------------------------------------------------------------------------------------------- AGB-Kopie
|
||||
|
||||
const AGB_MD = 'docs/rechtliches/agb.md'
|
||||
// Quelle der Website-AGB auf dem Server (Repository webklar.com). Fehlt die Datei (anderer Rechner, Container), entfällt nur der Vergleich mit der Website.
|
||||
const AGB_WEBSITE = process.env.WEBKLAR_AGB_QUELLE || '/home/webklar/apps/webklar.com/build/src/pages/AGB.tsx'
|
||||
// SHA-256 des normierten Klartexts der Website-AGB vom 30.09.2026 (Commit 72f278b der Website; Titelblock und Nr. 1 bis 14). Ändert sich der Text der Kopie,
|
||||
// braucht sie eine neue Fassung: AGB_FASSUNG hochziehen, agb.md ändern, Seite und PDF neu erzeugen, diesen Hash eintragen.
|
||||
const AGB_TEXT_SHA256 = 'f3374352ef97e6050bb0908c1b4abd806f759851b5b85fa9d030512d46b36659'
|
||||
|
||||
/** Textknoten der Kopie: alles nach der Fassungszeile, ohne Überschriftszeichen, Aufzählungszeichen, Absatznummern „(1)“ und Fettdruck. */
|
||||
function agbKnotenAusMarkdown(md) {
|
||||
const zeilen = md.replace(/\r/g, '').split('\n')
|
||||
const start = zeilen.findIndex((z) => z.startsWith('**Fassung '))
|
||||
assert.ok(start > 0, 'agb.md: Fassungszeile fehlt')
|
||||
return zeilen
|
||||
.slice(start + 1)
|
||||
.filter((z) => z.trim() !== '')
|
||||
.map((z) => z.replace(/^#{1,3} /, '').replace(/^- /, '').replace(/^\(\d+\) /, '').replace(/\*\*/g, '').replace(/\s+/g, ' ').trim())
|
||||
}
|
||||
|
||||
/** Textknoten der Website-Seite AGB.tsx: vom Seitentitel bis zum Ende des Vertragstexts, ohne JSX-Kommentare und Markup. */
|
||||
function agbKnotenAusWebsite(tsx) {
|
||||
const von = tsx.indexOf('<h1 className="wk-seite__titel">')
|
||||
const bis = tsx.indexOf('</article>')
|
||||
assert.ok(von > 0 && bis > von, 'AGB.tsx: Aufbau hat sich geändert (Seitentitel oder article nicht gefunden)')
|
||||
return tsx
|
||||
.slice(von, bis)
|
||||
.replace(/\{\/\*[\s\S]*?\*\/\}/g, ' ')
|
||||
.replace(/\{" "\}/g, ' ')
|
||||
.replace(/<[^>]*>/g, '\n')
|
||||
.split('\n')
|
||||
.map((z) => z.replace(/\s+/g, ' ').trim())
|
||||
.filter(Boolean)
|
||||
}
|
||||
const agbText = (knoten) => norm(knoten.join(' '))
|
||||
/** Die erste Stelle, an der zwei Texte auseinanderlaufen, mit etwas Umgebung (für die Fehlermeldung). */
|
||||
const ersterUnterschied = (a, b) => {
|
||||
let i = 0
|
||||
while (i < a.length && i < b.length && a[i] === b[i]) i += 1
|
||||
return `Kopie: „…${a.slice(Math.max(0, i - 40), i + 60)}…“, Website: „…${b.slice(Math.max(0, i - 40), i + 60)}…“`
|
||||
}
|
||||
|
||||
test('agb_fassung: AGB_FASSUNG ist der 30.09.2026, der Tag der letzten Textänderung der Website-AGB (Commit 72f278b); die Kopie trägt dieselbe Fassung', () => {
|
||||
assert.equal(AGB_FASSUNG, '2026-09-30')
|
||||
assert.ok(lies(AGB_MD).startsWith(`# Allgemeine Geschäftsbedingungen\n\n**Fassung ${AGB_FASSUNG}**`))
|
||||
})
|
||||
|
||||
test('agb_md_aufbau: vierzehn Abschnitte in Reihenfolge; die Verweise der Auftragsbestätigung auf Nr. 7, 8 und 9 treffen', () => {
|
||||
const md = lies(AGB_MD)
|
||||
const abschnitte = md.split('\n').filter((z) => /^## \d+\. /.test(z)).map((z) => z.replace(/^## /, ''))
|
||||
assert.deepEqual(abschnitte.map((z) => Number.parseInt(z, 10)), Array.from({ length: 14 }, (_, i) => i + 1))
|
||||
const titel = Object.fromEntries(abschnitte.map((z) => [Number.parseInt(z, 10), z.replace(/^\d+\. /, '')]))
|
||||
assert.equal(titel[7], 'Mitwirkungspflichten des Kunden')
|
||||
assert.equal(titel[8], 'Abnahme')
|
||||
assert.equal(titel[9], 'Vergütung und Zahlung')
|
||||
assert.ok(T.mitwirkung.includes('Nr. 7 und Nr. 8'))
|
||||
assert.ok(T.aufwand({ satz: '100,00 €' }).includes('(AGB Nr. 9)'))
|
||||
const nr9 = md.slice(md.indexOf('## 9. '), md.indexOf('## 10. '))
|
||||
assert.ok(nr9.includes('in Schritten von 15 Minuten') && nr9.includes('die Hälfte der Arbeitszeit'), 'Nr. 9 enthält die Regel, auf die die Auftragsbestätigung verweist')
|
||||
// der Titelblock der Website steht als Überschrift und Parteien, die Verweise auf die Abos bleiben Text
|
||||
assert.ok(md.includes('## Kaufvertrag – WEBklar (Modularer Projektvertrag)'))
|
||||
assert.ok(md.includes('**zwischen**') && md.includes('**und**'))
|
||||
})
|
||||
|
||||
test('agb_text_pin: der normierte Text der Kopie hat den festgehaltenen SHA-256 (keine versehentliche Änderung eines Rechtstextes)', () => {
|
||||
const hash = sha256(agbText(agbKnotenAusMarkdown(lies(AGB_MD))))
|
||||
assert.equal(hash, AGB_TEXT_SHA256, `Der Text der AGB-Kopie hat sich geändert (Hash jetzt ${hash}). Änderungen nur mit neuer Fassung: AGB_FASSUNG hochziehen, Seite und PDF neu erzeugen.`)
|
||||
})
|
||||
|
||||
test('agb_text_gleich_website: die Kopie ist wortgleich mit der Seite AGB.tsx der Website (Titelblock und Nr. 1 bis 14)', (t) => {
|
||||
if (!existsSync(AGB_WEBSITE)) return t.skip(`Website-AGB nicht erreichbar: ${AGB_WEBSITE}`)
|
||||
const website = agbText(agbKnotenAusWebsite(readFileSync(AGB_WEBSITE, 'utf8')))
|
||||
const kopie = agbText(agbKnotenAusMarkdown(lies(AGB_MD)))
|
||||
assert.ok(kopie === website, `Die AGB-Kopie weicht von den AGB der Website ab (${ersterUnterschied(kopie, website)}). Ist die Website neu, braucht die Kopie eine neue Fassung (AGB_FASSUNG, agb.md, Seite, PDF, Hash).`)
|
||||
assert.equal(sha256(website), AGB_TEXT_SHA256)
|
||||
})
|
||||
Reference in new issue
Block a user