From 0fbeadd0c19450bf5733ed8132e117252436ffee Mon Sep 17 00:00:00 2001 From: Claude Date: Tue, 20 Oct 2026 10:02:30 +0000 Subject: [PATCH] =?UTF-8?q?Rechtliches:=20Skript=20zum=20Hochladen=20der?= =?UTF-8?q?=20Datenschutzerkl=C3=A4rung=20als=20neue=20Version?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der Ablauf des Vorbilds (hochladen) ist jetzt eine exportierte, parametrisierte Funktion; datenschutz-hochladen.mjs ruft sie mit Karte datenschutz, PDF und HTML auf und bricht ab, wenn der Stand in der HTML nicht der Freigabe entspricht. Co-Authored-By: Claude Sonnet 5.5 (cherry picked from commit 06676c459d0ac4f534aed7d616e2c0d40ac97b97) --- scripts/datenschutz-hochladen.mjs | 103 +++++++++++++++++++++++++++ scripts/rechtliches-hochladen.mjs | 57 ++++++++++----- test/datenschutzHochladen.test.js | 111 ++++++++++++++++++++++++++++++ 3 files changed, 253 insertions(+), 18 deletions(-) create mode 100644 scripts/datenschutz-hochladen.mjs create mode 100644 test/datenschutzHochladen.test.js diff --git a/scripts/datenschutz-hochladen.mjs b/scripts/datenschutz-hochladen.mjs new file mode 100644 index 0000000..6ec4514 --- /dev/null +++ b/scripts/datenschutz-hochladen.mjs @@ -0,0 +1,103 @@ +#!/usr/bin/env node +// Lädt die Datenschutzerklärung (PDF und HTML) nach der Freigabe als neue Version in WOMS „Rechtliches“ hoch (Karte datenschutz, Bucket +// legal-files, Collection legalVersions): neue Fassung "current", die bisherige gültige wird "superseded" mit validTo. Nur mit +// --freigabe=JJJJ-MM-TT; dieser Tag muss der „Stand TT.MM.JJJJ“ in public/datenschutz.html entsprechen. Schon hochgeladene Stände (gleiche source, +// also gleicher Commit) werden übersprungen. Den Ablauf teilt sich das Skript mit rechtliches-hochladen.mjs (Funktion hochladen). +// +// node scripts/datenschutz-hochladen.mjs --probe nur lesen: Schlüssel, Bucket, Karte datenschutz und ihre Versionen +// node scripts/datenschutz-hochladen.mjs --freigabe=JJJJ-MM-TT lädt die Datenschutzerklärung hoch (validFrom = Freigabedatum); ohne Karte, +// ohne beide Dateien, mit nicht committetem Stand oder mit abweichendem +// „Stand“ Abbruch vor dem Schreiben +// +// Schlüssel: kurzlebiger Appwrite-Schlüssel mit files.read/files.write/documents.read/documents.write in /root/.appwrite-rechtliches.key (chmod 600). +// Der Import dieser Datei liest weder den Schlüssel noch nutzt er das Netz; beides geschieht nur in main (deps.schluessel, deps.fetch). +// Dieses Skript wird nur geschrieben und mit Nachbau-deps getestet, nie von einem Agenten ausgeführt (auch nicht mit --probe). +import { readFileSync } from 'node:fs' +import path from 'node:path' +import { fileURLToPath } from 'node:url' +import { BUCKET, KEY_DATEI, alleDokumente, client, ddmmjjjj, hochladen, istKalendertag, versionen } from './rechtliches-hochladen.mjs' + +const DOKUMENT_KEY = 'datenschutz' +/** [PDF, HTML]: in dieser Reihenfolge hochgeladen. */ +const DATEIEN = Object.freeze(['docs/WEBklar-Datenschutzerklaerung.pdf', 'public/datenschutz.html']) +const QUELLE = 'public/datenschutz.html' +/** Für Kunden sichtbar (WOMS „Rechtliches“): darum weder „Workers“ noch „Hustler“. */ +const AENDERUNG = 'Abschnitt 11 Interessenten (Ansprache per E-Mail)' + +const USAGE = 'Aufruf: node scripts/datenschutz-hochladen.mjs --probe | --freigabe=JJJJ-MM-TT' + +/** + * Kommandozeile prüfen: --probe allein oder genau eine --freigabe=JJJJ-MM-TT (ein echter Kalendertag). + * @returns {{ modus: 'probe'|'hochladen', freigabe: string|null }} + */ +export function parseArgs(argv) { + const fremde = argv.filter((a) => a !== '--probe' && !a.startsWith('--freigabe=')) + if (fremde.length > 0) throw new Error(`${USAGE}\nUnbekanntes Argument: ${fremde[0].slice(0, 40)}`) + const probe = argv.filter((a) => a === '--probe') + const freigaben = argv.filter((a) => a.startsWith('--freigabe=')) + if (probe.length > 0) { + if (probe.length > 1 || freigaben.length > 0) throw new Error(`${USAGE}\n--probe steht allein.`) + return { modus: 'probe', freigabe: null } + } + if (freigaben.length !== 1) throw new Error(`${USAGE}\nFreigabe fehlt: genau eine --freigabe=JJJJ-MM-TT ist beim Hochladen Pflicht.`) + const freigabe = freigaben[0].slice('--freigabe='.length) + if (!istKalendertag(freigabe)) throw new Error(`Freigabe ungültig (erwartet JJJJ-MM-TT, ein echter Kalendertag): ${freigabe.slice(0, 20)}`) + return { modus: 'hochladen', freigabe } +} + +/** Der Tag hinter „Stand“ in der Kopfzeile der HTML (TT.MM.JJJJ), sonst null. */ +const standIn = (html) => /

\s*Stand (\d{2}\.\d{2}\.\d{4})\s*<\/p>/.exec(html)?.[1] ?? null + +/** Einstieg: deps = { fetch, schluessel, log, git, dateien } wie bei rechtliches-hochladen.mjs. @returns {Promise} Exit-Code (0 oder 1) */ +export async function main(argv, deps = {}) { + const log = deps.log ?? console.log + let args + try { + args = parseArgs(argv) + } catch (err) { + log(err.message) + return 1 + } + try { + const fetchFn = deps.fetch ?? globalThis.fetch + const api = client(fetchFn, (deps.schluessel ?? (() => readFileSync(KEY_DATEI, 'utf8').trim()))()) + if (args.modus === 'probe') return await probe(api, log) + const { freigabe } = args + return await hochladen(api, log, { + documentKey: DOKUMENT_KEY, + dateien: DATEIEN, + quelle: QUELLE, + changeNote: `Fassung vom ${ddmmjjjj(freigabe)}: ${AENDERUNG}`, + freigabe, + kartenHinweis: '(die Karte gehört zu WOMS „Rechtliches“; dieses Skript legt keine Karten an)', + pruefeInhalt: (lies) => { + const stand = standIn(lies(QUELLE).toString('utf8')) + if (!stand) return `Abbruch: „Stand TT.MM.JJJJ“ fehlt in ${QUELLE}` + if (stand !== ddmmjjjj(freigabe)) return `Abbruch: Stand ${stand} in ${QUELLE} passt nicht zur Freigabe ${ddmmjjjj(freigabe)}` + return null + }, + }, deps) + } catch (err) { + log(`Fehler: ${err.message}`) + return 1 + } +} + +/** --probe: nur lesen. Eine fehlende Karte ist ein Stopp. */ +async function probe(api, log) { + await api('GET', `/storage/buckets/${BUCKET}/files`) + const karten = await alleDokumente(api, 'legalDocuments') + log(`${DOKUMENT_KEY}: ${(await versionen(api, DOKUMENT_KEY)).map((v) => `${v.version}:${v.status}`).join(' ') || '—'}`) + if (!karten.some((k) => k.key === DOKUMENT_KEY)) { + log(`STOPP: Karte fehlt: ${DOKUMENT_KEY}`) + return 1 + } + log(`Schlüssel ok (files.read + documents.read), Karte ${DOKUMENT_KEY} vorhanden.`) + return 0 +} + +if (process.argv[1] && fileURLToPath(import.meta.url) === path.resolve(process.argv[1])) { + main(process.argv.slice(2)).then((code) => { + process.exitCode = code + }) +} diff --git a/scripts/rechtliches-hochladen.mjs b/scripts/rechtliches-hochladen.mjs index 523ef75..e9e1d43 100644 --- a/scripts/rechtliches-hochladen.mjs +++ b/scripts/rechtliches-hochladen.mjs @@ -23,8 +23,8 @@ import { FUNKTIONEN_UNTERLAGEN_V6 } from '../server/services/products.js' const ENDPOINT = 'https://ticket.webklar.com/v1' const PROJECT = '6a1058610003c5a13a05' const DB = 'woms-database' -const BUCKET = 'legal-files' -const KEY_DATEI = '/root/.appwrite-rechtliches.key' +export const BUCKET = 'legal-files' +export const KEY_DATEI = '/root/.appwrite-rechtliches.key' const ROOT = fileURLToPath(new URL('..', import.meta.url)) const LIVE = 'https://project.webklar.com/' @@ -81,7 +81,7 @@ export function dokumentFuer(documentKey) { const MODI = { '--probe': 'probe', '--karten-anlegen': 'karten', '--pruefen': 'pruefen' } -const istKalendertag = (iso) => { +export const istKalendertag = (iso) => { if (!/^\d{4}-\d{2}-\d{2}$/.test(iso)) return false const d = new Date(`${iso}T12:00:00Z`) return !Number.isNaN(d.getTime()) && d.toISOString().slice(0, 10) === iso @@ -165,7 +165,7 @@ const q = (o) => encodeURIComponent(JSON.stringify(o)) const neueId = () => Array.from(crypto.getRandomValues(new Uint8Array(10)), (b) => b.toString(16).padStart(2, '0')).join('').slice(0, 20) /** Kleiner Appwrite-Client über deps.fetch; Fehler werden zu Error mit Methode, Pfad und Status. */ -function client(fetchFn, schluessel) { +export function client(fetchFn, schluessel) { const kopf = { 'X-Appwrite-Project': PROJECT, 'X-Appwrite-Key': schluessel } return async function api(method, pfad, body) { const formular = typeof FormData !== 'undefined' && body instanceof FormData @@ -180,7 +180,7 @@ function client(fetchFn, schluessel) { } } -async function alleDokumente(api, sammlung) { +export async function alleDokumente(api, sammlung) { const aus = [] for (let offset = 0; ; offset += 100) { const r = await api('GET', `/databases/${DB}/collections/${sammlung}/documents?queries[]=${q({ method: 'limit', values: [100] })}&queries[]=${q({ method: 'offset', values: [offset] })}`) @@ -189,12 +189,12 @@ async function alleDokumente(api, sammlung) { } } -async function versionen(api, documentKey) { +export async function versionen(api, documentKey) { const r = await api('GET', `/databases/${DB}/collections/legalVersions/documents?queries[]=${q({ method: 'equal', attribute: 'documentKey', values: [documentKey] })}&queries[]=${q({ method: 'limit', values: [100] })}`) return r.documents } -const ddmmjjjj = (iso) => iso.split('-').reverse().join('.') +export const ddmmjjjj = (iso) => iso.split('-').reverse().join('.') // ---------------------------------------------------------------------------------------------------------- Aufruf @@ -218,7 +218,14 @@ export async function main(argv, deps = {}) { if (args.modus === 'probe') return await probe(api, log) if (args.modus === 'karten') return await kartenAnlegen(api, log) if (args.modus === 'pruefen') return await pruefen(api, log) - return await hochladen(api, log, args, deps) + const d = dokumentFuer(args.documentKey) + return await hochladen(api, log, { + documentKey: args.documentKey, + dateien: d.dateien, + quelle: d.quelle, + changeNote: `Fassung vom ${ddmmjjjj(args.freigabe)}: Kundenansicht neu (Fassung 6)`, + freigabe: args.freigabe, + }, deps) } catch (err) { log(`Fehler: ${err.message}`) return 1 @@ -263,28 +270,42 @@ async function pruefen(api, log) { return fehlt.length > 0 ? 1 : 0 } -/** --freigabe=…: genau ein Dokument hochladen. Als Erstes die Kartenprüfung, dann Git- und Dateiprüfung, erst dann schreiben. */ -async function hochladen(api, log, { documentKey, freigabe }, deps) { - const d = dokumentFuer(documentKey) +/** + * Genau ein Dokument als neue Version hochladen (auch vom Datenschutz-Skript genutzt). Als Erstes die Kartenprüfung, dann Datei- und Git-Prüfung + * und die Inhaltsprüfung des Aufrufers, erst dann schreiben: erst alle Dateien in den Bucket, dann das Versionsdokument, zuletzt die bisherige + * gültige Version ablösen. deps = { git, dateien } wie bei main. + * @param {object} api Client aus client() + * @param {Function} log + * @param {object} dok documentKey; dateien = [PDF, HTML] (Pfade im Projekt, in dieser Reihenfolge hochgeladen); quelle = Pfad in source + * (kundenbereich@:); changeNote und freigabe (JJJJ-MM-TT = validFrom) fertig vom Aufrufer; kartenHinweis hängt an der Meldung + * „Karte fehlt“; pruefeInhalt(lies) darf nach der Git-Prüfung mit lies(pfad) → Buffer den Inhalt prüfen und gibt eine Abbruchmeldung oder null zurück + * @returns {Promise} Exit-Code (0 oder 1) + */ +export async function hochladen(api, log, { documentKey, dateien, quelle, changeNote, freigabe, kartenHinweis = '(zuerst --karten-anlegen)', pruefeInhalt }, deps) { const karten = await alleDokumente(api, 'legalDocuments') if (!karten.some((k) => k.key === documentKey)) { - log(`Karte fehlt: ${documentKey} (zuerst --karten-anlegen)`) + log(`Karte fehlt: ${documentKey} ${kartenHinweis}`) return 1 } const git = deps.git ?? ((args) => execFileSync('git', args, { cwd: ROOT, encoding: 'utf8' })) const dateisystem = deps.dateien ?? { existsSync: (rel) => existsSync(path.join(ROOT, rel)), readFileSync: (rel) => readFileSync(path.join(ROOT, rel)) } - for (const rel of d.dateien) { + for (const rel of dateien) { if (!dateisystem.existsSync(rel)) { log(`Datei fehlt: ${rel}`) return 1 } } - if (git(['status', '--porcelain', '--', ...d.dateien]).trim() !== '') { - log(`Abbruch: nicht committeter Stand in ${d.dateien.join(' und ')} (git status --porcelain ist nicht leer)`) + if (git(['status', '--porcelain', '--', ...dateien]).trim() !== '') { + log(`Abbruch: nicht committeter Stand in ${dateien.join(' und ')} (git status --porcelain ist nicht leer)`) + return 1 + } + const inhaltsfehler = pruefeInhalt?.((rel) => dateisystem.readFileSync(rel)) + if (inhaltsfehler) { + log(inhaltsfehler) return 1 } const commit = git(['rev-parse', '--short', 'HEAD']).trim() - const source = `kundenbereich@${commit}:${d.quelle}` + const source = `kundenbereich@${commit}:${quelle}` const alle = await versionen(api, documentKey) const plan = planVersion({ alle, source, validFrom: freigabe }) if (plan.schon) { @@ -292,7 +313,7 @@ async function hochladen(api, log, { documentKey, freigabe }, deps) { return 0 } const fileIds = [] - for (const rel of d.dateien) { + for (const rel of dateien) { const form = new FormData() form.append('fileId', neueId()) form.append('file', new Blob([dateisystem.readFileSync(rel)], { type: rel.endsWith('.pdf') ? 'application/pdf' : 'text/html' }), path.basename(rel)) @@ -306,7 +327,7 @@ async function hochladen(api, log, { documentKey, freigabe }, deps) { validFrom: freigabe, validTo: null, status: 'current', - changeNote: `Fassung vom ${ddmmjjjj(freigabe)}: Kundenansicht neu (Fassung 6)`, + changeNote, fileIds, source, }, diff --git a/test/datenschutzHochladen.test.js b/test/datenschutzHochladen.test.js new file mode 100644 index 0000000..22e804f --- /dev/null +++ b/test/datenschutzHochladen.test.js @@ -0,0 +1,111 @@ +import { test } from 'node:test' +import assert from 'node:assert/strict' + +// Hochladen der Datenschutzerklärung nach WOMS "Rechtliches" (scripts/datenschutz-hochladen.mjs): die Tests rufen main nur mit Nachbau-deps +// auf, nie mit dem echten Schlüssel oder dem Netz. Das globale fetch wirft und zählt, damit ein Weg am Nachbau vorbei auffällt. +let netzzugriffe = 0 +globalThis.fetch = () => { + netzzugriffe += 1 + throw new Error('Netzzugriff im Test') +} +const m = await import('../scripts/datenschutz-hochladen.mjs') // der Import liest weder Schlüssel noch Netz + +const frueher = ['v1', 'v2', 'v3', 'v4', 'v5'].map((v, i) => ({ $id: `s${i}`, version: v, validFrom: `2026-09-0${i + 1}`, status: 'superseded', source: `x${i}` })) +const AKTUELL = { $id: 'alt', version: 'v6', validFrom: '2026-10-05', status: 'current', source: 'kundenbereich@aaaaaaa:public/datenschutz.html' } +// stand = das Datum, das im Nachbau der HTML-Datei hinter "Stand" steht (die PDF-Datei ist beliebiger Inhalt) +function backend({ versionen = [...frueher, AKTUELL], karten = [{ $id: 'datenschutz', key: 'datenschutz' }], gitStatus = '', stand = '20.10.2026', vorhanden = () => true } = {}) { + const aufrufe = [], log = [] + const antwort = (obj) => ({ ok: true, status: 200, text: async () => JSON.stringify(obj) }) + const fetch = async (url, opt = {}) => { + const methode = opt.method ?? 'GET', pfad = new URL(url).pathname.replace(/^\/v1/, '') + aufrufe.push({ methode, pfad, body: opt.body }) + if (methode === 'GET' && pfad.endsWith('/collections/legalDocuments/documents')) return antwort({ total: karten.length, documents: karten }) + if (methode === 'GET' && pfad.endsWith('/collections/legalVersions/documents')) return antwort({ total: versionen.length, documents: versionen }) + if (methode === 'GET' && pfad.startsWith('/storage/buckets/legal-files/files')) return antwort({ total: 0, files: [] }) + if (methode === 'POST' && pfad === '/storage/buckets/legal-files/files') return antwort({ $id: `f${aufrufe.length}` }) + if (methode === 'POST' && pfad.endsWith('/collections/legalVersions/documents')) return antwort({ $id: 'v-neu' }) + if (methode === 'PATCH' && pfad.includes('/collections/legalVersions/documents/')) return antwort({}) + throw new Error(`unerwartet: ${methode} ${pfad}`) + } + const html = () => Buffer.from(stand === null ? '

ohne Datum

' : `

Stand ${stand}

`) + return { aufrufe, log, deps: { fetch, schluessel: () => 'test-schluessel', log: (z) => log.push(z), git: (args) => (args[0] === 'status' ? gitStatus : 'abc1234\n'), dateien: { existsSync: vorhanden, readFileSync: (rel) => (rel.endsWith('.html') ? html() : Buffer.from('x')) } } } +} +const schreibend = (b) => b.aufrufe.filter((a) => a.methode !== 'GET') + +test('parseArgs: --probe allein oder --freigabe=Tag, sonst Aufruf-Fehler', () => { + assert.deepEqual(m.parseArgs(['--probe']), { modus: 'probe', freigabe: null }) + assert.deepEqual(m.parseArgs(['--freigabe=2026-10-20']), { modus: 'hochladen', freigabe: '2026-10-20' }) + for (const bad of [[], ['--freigabe=20.10.2026'], ['--freigabe=2026-13-40'], ['--probe', '--freigabe=2026-10-20'], ['datenschutz']]) assert.throws(() => m.parseArgs(bad), /Aufruf|Freigabe/) + for (const bad of [['--freigabe=2026-10-20', '--freigabe=2026-10-21'], ['--probe', '--probe'], ['--freigabe=2026-10-20', '--karten-anlegen'], ['datenschutz', '--freigabe=2026-10-20']]) assert.throws(() => m.parseArgs(bad), /Aufruf|Freigabe/, JSON.stringify(bad)) +}) +test('Hochladen: Dateien zuerst (PDF, dann HTML), dann Version v7, dann die bisherige ablösen', async () => { + const b = backend() + assert.equal(await m.main(['--freigabe=2026-10-20'], b.deps), 0) + assert.deepEqual(schreibend(b).map((a) => `${a.methode} ${a.pfad.split('/').slice(-1)[0]}`), ['POST files', 'POST files', 'POST documents', 'PATCH alt']) + const neu = JSON.parse(schreibend(b)[2].body).data + assert.deepEqual([neu.documentKey, neu.version, neu.validFrom, neu.status, neu.source, neu.fileIds.length], ['datenschutz', 'v7', '2026-10-20', 'current', 'kundenbereich@abc1234:public/datenschutz.html', 2]) + assert.match(neu.changeNote, /^Fassung vom 20\.10\.2026: Abschnitt 11 Interessenten/) + assert.deepEqual(JSON.parse(schreibend(b)[3].body).data, { status: 'superseded', validTo: '2026-10-19' }) +}) +test('Hochladen: die beiden Dateien sind die PDF und die HTML der Datenschutzerklärung, mit Dateiname und Typ', async () => { + const b = backend() + assert.equal(await m.main(['--freigabe=2026-10-20'], b.deps), 0) + const dateien = schreibend(b).slice(0, 2).map((a) => a.body.get('file')) + assert.deepEqual(dateien.map((f) => [f.name, f.type]), [['WEBklar-Datenschutzerklaerung.pdf', 'application/pdf'], ['datenschutz.html', 'text/html']]) + assert.equal(await dateien[1].text(), '

Stand 20.10.2026

') +}) +test('Changenote nennt weder Workers noch Hustler (sie kann für Kunden sichtbar werden)', async () => { + const b = backend() + assert.equal(await m.main(['--freigabe=2026-10-20'], b.deps), 0) + assert.doesNotMatch(JSON.parse(schreibend(b)[2].body).data.changeNote, /workers|hustler/i) +}) +test('schon hochgeladen, fehlende Karte, nicht eingecheckter Stand und --probe schreiben nichts', async () => { + const schon = backend({ versionen: [{ ...AKTUELL, source: 'kundenbereich@abc1234:public/datenschutz.html' }] }) + assert.equal(await m.main(['--freigabe=2026-10-20'], schon.deps), 0) + const ohneKarte = backend({ karten: [] }) + assert.equal(await m.main(['--freigabe=2026-10-20'], ohneKarte.deps), 1) + const schmutzig = backend({ gitStatus: ' M public/datenschutz.html\n' }) + assert.equal(await m.main(['--freigabe=2026-10-20'], schmutzig.deps), 1) + const probe = backend() + assert.equal(await m.main(['--probe'], probe.deps), 0) + for (const b of [schon, ohneKarte, schmutzig, probe]) assert.equal(schreibend(b).length, 0) +}) +test('Fehlende Karte und fehlende Datei brechen mit Meldung ab, bevor etwas geschrieben wird', async () => { + const ohneKarte = backend({ karten: [] }) + assert.equal(await m.main(['--freigabe=2026-10-20'], ohneKarte.deps), 1) + assert.ok(ohneKarte.log.some((z) => z.includes('Karte fehlt: datenschutz'))) + assert.ok(!ohneKarte.log.some((z) => z.includes('--karten-anlegen')), 'dieses Skript legt keine Karten an') + const ohnePdf = backend({ vorhanden: (rel) => !rel.endsWith('.pdf') }) + assert.equal(await m.main(['--freigabe=2026-10-20'], ohnePdf.deps), 1) + assert.ok(ohnePdf.log.some((z) => z.includes('Datei fehlt: docs/WEBklar-Datenschutzerklaerung.pdf'))) + assert.equal(schreibend(ohneKarte).length + schreibend(ohnePdf).length, 0) +}) +test('Stand in der HTML muss der Freigabe entsprechen, sonst Abbruch vor jedem Schreiben', async () => { + const falsch = backend({ stand: '17.10.2026' }) + assert.equal(await m.main(['--freigabe=2026-10-20'], falsch.deps), 1) + assert.ok(falsch.log.some((z) => z.includes('Stand 17.10.2026') && z.includes('20.10.2026')), falsch.log.join(' | ')) + const ohneStand = backend({ stand: null }) + assert.equal(await m.main(['--freigabe=2026-10-20'], ohneStand.deps), 1) + assert.ok(ohneStand.log.some((z) => z.includes('Stand')), ohneStand.log.join(' | ')) + for (const b of [falsch, ohneStand]) assert.equal(schreibend(b).length, 0) + const richtig = backend({ stand: '21.10.2026' }) + assert.equal(await m.main(['--freigabe=2026-10-21'], richtig.deps), 0, 'mit passender Freigabe geht derselbe Stand durch') +}) +test('--probe liest nur: Bucket, Karte und Versionen, meldet eine fehlende Karte als Stopp', async () => { + const b = backend() + assert.equal(await m.main(['--probe'], b.deps), 0) + assert.ok(b.log.includes('datenschutz: v1:superseded v2:superseded v3:superseded v4:superseded v5:superseded v6:current'), b.log.join(' | ')) + assert.deepEqual(b.aufrufe.map((a) => a.pfad.replace(/^\/databases\/woms-database/, '')), ['/storage/buckets/legal-files/files', '/collections/legalDocuments/documents', '/collections/legalVersions/documents']) + const ohneKarte = backend({ karten: [] }) + assert.equal(await m.main(['--probe'], ohneKarte.deps), 1) + assert.ok(ohneKarte.log.includes('STOPP: Karte fehlt: datenschutz')) +}) +test('ungültige Aufrufe enden mit Exit 1 und Hinweis, ohne ein einziges Netz-Zeichen', async () => { + const b = backend() + assert.equal(await m.main([], b.deps), 1) + assert.match(b.log[0], /Aufruf/) + assert.equal(b.aufrufe.length, 0) +}) +test('kein Test hat das echte fetch erreicht', () => { + assert.equal(netzzugriffe, 0) +})