Shuffle/supabase/catalog/build_startbestand_test.ts
cschlaefke 9fa91eb684
All checks were successful
CI / flutter (pull_request) Successful in 1m19s
CI / web-build (pull_request) Successful in 57s
CI / supabase (pull_request) Successful in 11s
CI / supabase-tests (pull_request) Successful in 38s
CI / e2e (pull_request) Successful in 2m59s
fix(db): single filter set, filter tests with query, perf guards, evidence docs (KAT-01, 3a review)
Filters are evaluated once into an id set used by both search branches.
Adds tests for age/genre/mechanic filters with a search term, rank 2 vs 3
and null p_limit. approxNormalize uses NFKD. Perf script checks confirm_local
value and unix socket and re-analyzes after rollback. Adds
docs/specs/3-katalog-suche-umsetzung.md (measurements, log check incl. GET
finding), release checklist section 2a and the CSV-only note for alt titles.

Refs #3

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-11-04 10:39:02 +01:00

413 lines
22 KiB
TypeScript
Raw Blame History

This file contains invisible Unicode characters

This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Tests für build_startbestand.ts. Aufruf (Repo-Root):
// deno test --allow-read=supabase supabase/catalog/
// Fixtures sind offensichtlich fiktiv ("Testspiel", "Testverlag"). Es werden keine echten Spieldaten verwendet.
import assert from "node:assert/strict";
import {
build,
BuildError,
generateStartbestandSql,
NAMESPACE,
approxNormalize,
parseAltTitles,
parseCsv,
parseKeys,
parseStartbestand,
parseVocabulary,
renderKeys,
uuidV5,
} from "./build_startbestand.ts";
const VOCAB = `kind,vocab_key,name_de,name_en
genre,test-genre-a,Testgenre A,Test genre A
genre,test-genre-b,Testgenre B,Test genre B
mechanic,test-mechanic-a,Testmechanik A,Test mechanic A
`;
const HEADER =
"key,title,publishers,year,min_players,max_players,min_minutes,max_minutes,min_age,genres,mechanics,description,description_locale,hidden,facts_source,alt_titles,base_game_key";
// Zeile mit Standardwerten; einzelne Felder lassen sich überschreiben.
function row(over: Record<string, string> = {}): string {
const base: Record<string, string> = {
key: "testspiel-a", title: "Testspiel A", publishers: "Testverlag", year: "2020", min_players: "2",
max_players: "4", min_minutes: "30", max_minutes: "60", min_age: "8", genres: "Testgenre A",
mechanics: "Testmechanik A", description: "Fiktiver Text.", description_locale: "de", hidden: "",
facts_source: "Test", alt_titles: "", base_game_key: "",
};
const r = { ...base, ...over };
return HEADER.split(",").map((c) => {
const v = r[c];
return /[",\n]/.test(v) ? `"${v.replaceAll('"', '""')}"` : v;
}).join(",");
}
const csv = (...rows: string[]) => [HEADER, ...rows].join("\n") + "\n";
async function vocab() {
return await parseVocabulary(VOCAB);
}
async function rejects(text: string, includes: RegExp, keys = new Map<string, string>()) {
const v = await vocab();
await assert.rejects(
() => parseStartbestand(text, v, keys),
(e: unknown) => {
assert.ok(e instanceof BuildError, "BuildError erwartet");
assert.match(e.problems.join("\n"), includes);
return true;
},
);
}
// ---- UUID v5 ------------------------------------------------------------------------------
Deno.test("uuidV5: Referenzwert aus RFC 4122 / Python uuid5(NAMESPACE_DNS, ...)", async () => {
assert.equal(
await uuidV5("www.example.com", "6ba7b810-9dad-11d1-80b4-00c04fd430c8"),
"2ed6657d-e927-568b-95e1-2665a8aea6a2",
);
});
Deno.test("uuidV5: gleicher key gibt gleiche UUID wie uuid_generate_v5 in PostgreSQL (siehe pgTAP 06)", async () => {
assert.equal(NAMESPACE, "6f1d9a52-3c4e-4b8a-9f27-5d0e8c71a3b4");
assert.equal(await uuidV5("game:testspiel-a"), "5189ad0e-ab5b-5500-a3e0-a0b3671cdf14");
assert.equal(await uuidV5("game:testspiel-a"), await uuidV5("game:testspiel-a"));
assert.notEqual(await uuidV5("game:testspiel-a"), await uuidV5("game:testspiel-b"));
});
// ---- CSV ----------------------------------------------------------------------------------
Deno.test("parseCsv: Anführungszeichen, doppelte Anführungszeichen, Zeilenumbruch im Feld, CRLF, BOM", () => {
const parsed = parseCsv('a,b\r\n"x, y","sagt ""hallo"""\r\n"zwei\nZeilen",z\r\n', "t.csv");
assert.deepEqual(parsed.header, ["a", "b"]);
assert.equal(parsed.rows[0].cells.a, "x, y");
assert.equal(parsed.rows[0].cells.b, 'sagt "hallo"');
assert.equal(parsed.rows[1].cells.a, "zwei\nZeilen");
assert.equal(parsed.rows[1].line, 3);
});
Deno.test("parseCsv: falsche Spaltenzahl und offenes Anführungszeichen nennen die Zeile", () => {
assert.throws(() => parseCsv("a,b\n1,2,3\n", "t.csv"), /t\.csv:2: 3 Spalten statt 2/);
assert.throws(() => parseCsv('a,b\n"offen,2\n', "t.csv"), /t\.csv:2: Anführungszeichen nicht geschlossen/);
});
Deno.test("parseCsv: nur Kopfzeile ergibt keine Zeilen", () => {
assert.equal(parseCsv("a,b\n", "t.csv").rows.length, 0);
});
// ---- Vokabular ----------------------------------------------------------------------------
Deno.test("parseVocabulary: gültig, IDs nach UUID v5", async () => {
const v = await vocab();
assert.equal(v.length, 3);
assert.equal(v[0].id, await uuidV5("genre:test-genre-a"));
assert.equal(v[2].id, await uuidV5("mechanic:test-mechanic-a"));
});
Deno.test("parseVocabulary: doppelter vocab_key, doppelter Name, falsche kind, falscher Schlüssel", async () => {
const head = "kind,vocab_key,name_de,name_en\n";
await assert.rejects(() => parseVocabulary(head + "genre,a-b,A,A\ngenre,a-b,B,B\n"), /vocabulary\.csv:3: vocab_key "a-b" doppelt/);
await assert.rejects(() => parseVocabulary(head + "genre,a-b,A,A\ngenre,c-d,a,C\n"), /vocabulary\.csv:3: name_de "a" doppelt/);
await assert.rejects(() => parseVocabulary(head + "genre,a-b,A,A\ngenre,c-d,C,a\n"), /vocabulary\.csv:3: name_en "a" doppelt/);
await assert.rejects(() => parseVocabulary(head + "kategorie,a-b,A,A\n"), /kind muss/);
await assert.rejects(() => parseVocabulary(head + "genre,A_B,A,A\n"), /vocab_key "A_B" ungültig/);
await assert.rejects(() => parseVocabulary("kind,vocab_key,name_de\ngenre,a-b,A\n"), /Spalte "name_en" fehlt/);
});
Deno.test("parseVocabulary: gleiche Namen in verschiedenen Arten sind erlaubt", async () => {
const v = await parseVocabulary("kind,vocab_key,name_de,name_en\ngenre,a-b,A,A\nmechanic,a-b,A,A\n");
assert.equal(v.length, 2);
assert.notEqual(v[0].id, v[1].id);
});
// ---- Startbestand: gültig -----------------------------------------------------------------
Deno.test("parseStartbestand: gültige Zeile (Positivfall)", async () => {
const games = await parseStartbestand(csv(row()), await vocab(), new Map());
assert.equal(games.length, 1);
assert.equal(games[0].id, await uuidV5("game:testspiel-a"));
assert.deepEqual(games[0].publishers, ["Testverlag"]);
assert.equal(games[0].hidden, false);
});
Deno.test("parseStartbestand: nur Kopfzeile ist gültig (heutiger Stand ohne Spiele)", async () => {
assert.deepEqual(await parseStartbestand(HEADER + "\n", await vocab(), new Map()), []);
});
// ---- Startbestand: eine Negativprobe je Prüfregel -----------------------------------------
Deno.test("SB2: fehlende Pflicht-Fakten", async () => {
for (const col of ["year", "min_players", "max_players", "min_minutes", "max_minutes", "min_age"]) {
await rejects(csv(row({ [col]: "" })), new RegExp(`startbestand\\.csv:2: ${col} fehlt`));
}
await rejects(csv(row({ publishers: "" })), /mindestens ein Verlag/);
await rejects(csv(row({ publishers: " ; " })), /mindestens ein Verlag/);
await rejects(csv(row({ genres: "" })), /mindestens ein Genre/);
await rejects(csv(row({ mechanics: "" })), /mindestens eine Mechanik/);
await rejects(csv(row({ title: "" })), /title muss 1-200 Zeichen/);
});
Deno.test("DB-Grenzen: Jahr, max < min, keine Zahl, negativ", async () => {
await rejects(csv(row({ year: "1799" })), /year 1799 außerhalb/);
await rejects(csv(row({ year: "2101" })), /year 2101 außerhalb/);
await rejects(csv(row({ year: "20x0" })), /year "20x0" ist keine ganze Zahl/);
await rejects(csv(row({ min_players: "0" })), /min_players 0 außerhalb/);
await rejects(csv(row({ min_players: "5", max_players: "3" })), /max_players \(3\) kleiner als min_players \(5\)/);
await rejects(csv(row({ min_minutes: "90", max_minutes: "30" })), /max_minutes \(30\) kleiner als min_minutes \(90\)/);
await rejects(csv(row({ min_age: "-1" })), /min_age "-1" ist keine ganze Zahl/);
await rejects(csv(row({ title: "x".repeat(201) })), /title muss 1-200 Zeichen/);
});
Deno.test("SB4: Kurztext Pflicht, Sprache de, Länge", async () => {
await rejects(csv(row({ description: "" })), /Kurztext fehlt/);
await rejects(csv(row({ description_locale: "en" })), /description_locale muss "de" sein/);
await rejects(csv(row({ description_locale: "" })), /description_locale muss "de" sein/);
await rejects(csv(row({ description: "x".repeat(5001) })), /länger als 5000/);
});
Deno.test("SB5: unbekanntes Genre oder unbekannte Mechanik, Doppelte", async () => {
await rejects(csv(row({ genres: "Erfundenes" })), /unbekannt in genres: "Erfundenes"/);
await rejects(csv(row({ mechanics: "Testgenre A" })), /unbekannt in mechanics/); // Genre ist keine Mechanik
await rejects(csv(row({ genres: "Testgenre A;Testgenre A" })), /genres: "Testgenre A" doppelt/);
});
Deno.test("SB6: keine Spalte für Cover, Quelle oder BGG-Kennung", async () => {
for (const col of ["image_path", "bgg_id", "catalog_source_id", "source_ref", "cover"]) {
await rejects(`${HEADER},${col}\n${row()},x\n`, new RegExp(`unbekannte Spalte "${col}"`));
}
});
Deno.test("SB7: base_game_key muss auf einen vorhandenen key zeigen", async () => {
await rejects(csv(row({ base_game_key: "gibt-es-nicht" })), /base_game_key "gibt-es-nicht" hat keinen passenden key/);
await rejects(csv(row({ base_game_key: "testspiel-a" })), /verweist auf das Spiel selbst/);
const ok = await parseStartbestand(
csv(row(), row({ key: "testspiel-b", title: "Testspiel B", base_game_key: "testspiel-a" })),
await vocab(),
new Map(),
);
assert.equal(ok.length, 2);
});
Deno.test("key: Format und Eindeutigkeit", async () => {
await rejects(csv(row({ key: "Ungültig_Key" })), /key "Ungültig_Key" ungültig/);
await rejects(csv(row({ key: "a" })), /key "a" ungültig/);
await rejects(csv(row(), row({ title: "Noch eins" })), /startbestand\.csv:3: key "testspiel-a" doppelt/);
});
Deno.test("hidden: nur leer oder ja; Verlage nicht doppelt", async () => {
await rejects(csv(row({ hidden: "nein" })), /hidden muss leer oder "ja" sein/);
await rejects(csv(row({ publishers: "Testverlag;TESTVERLAG" })), /Verlag "TESTVERLAG" doppelt/);
});
Deno.test("Pflichtspalte fehlt in der Kopfzeile", async () => {
await assert.rejects(
async () => await parseStartbestand("key,title\nx,y\n", await vocab(), new Map()),
/Spalte "publishers" fehlt/,
);
});
Deno.test("mehrere Fehler werden gesammelt, mit Zeilennummern", async () => {
const v = await vocab();
await assert.rejects(
() => parseStartbestand(csv(row({ year: "" }), row({ key: "testspiel-b", genres: "X" })), v, new Map()),
(e: unknown) => {
assert.ok(e instanceof BuildError);
assert.match(e.problems.join("\n"), /startbestand\.csv:2: year fehlt/);
assert.match(e.problems.join("\n"), /startbestand\.csv:3: unbekannt in genres/);
return true;
},
);
});
// ---- Schlüsselliste -----------------------------------------------------------------------
Deno.test("Schlüssel sind unveränderlich: ein entfernter key bricht ab", async () => {
const previous = parseKeys(`key,uuid\ntestspiel-a,${await uuidV5("game:testspiel-a")}\ntestspiel-alt,${await uuidV5("game:testspiel-alt")}\n`);
await rejects(csv(row()), /Schlüssel entfernt oder umbenannt: "testspiel-alt"/, previous);
// Umbenennen ist dasselbe wie Entfernen
await rejects(csv(row({ key: "testspiel-neu" })), /Schlüssel entfernt oder umbenannt: "testspiel-a"/, previous);
});
Deno.test("Schlüssel: falsche UUID in der Liste (Namespace geändert?)", async () => {
const previous = parseKeys("key,uuid\ntestspiel-a,00000000-0000-5000-8000-000000000000\n");
await rejects(csv(row()), /uuid zu "testspiel-a" ist 00000000-0000-5000-8000-000000000000, erwartet/, previous);
});
Deno.test("Schlüsselliste wächst nur und ist sortiert", async () => {
const first = await build({ vocabularyCsv: VOCAB, startbestandCsv: csv(row({ key: "zzz-spiel" })), previousKeysCsv: "key,uuid\n" });
const second = await build({
vocabularyCsv: VOCAB,
startbestandCsv: csv(row({ key: "zzz-spiel" }), row({ key: "aaa-spiel", title: "Neu" })),
previousKeysCsv: first["startbestand-keys.csv"],
});
const before = first["startbestand-keys.csv"].trim().split("\n");
const after = second["startbestand-keys.csv"].trim().split("\n");
for (const line of before) assert.ok(after.includes(line), `Zeile fehlt: ${line}`);
assert.deepEqual(after.slice(1).map((l) => l.split(",")[0]), ["aaa-spiel", "zzz-spiel"]);
assert.equal(renderKeys(parseKeys(second["startbestand-keys.csv"])), second["startbestand-keys.csv"]);
});
// ---- SQL-Erzeugung ------------------------------------------------------------------------
async function sqlFor(...rows: string[]): Promise<string> {
return generateStartbestandSql(await parseStartbestand(csv(...rows), await vocab(), new Map()));
}
Deno.test("hidden = ja erzeugt hidden_at, leer fasst hidden_at nicht an", async () => {
const hiddenSql = await sqlFor(row({ hidden: "ja" }));
assert.match(hiddenSql, /update public\.games set hidden_at = now\(\)/);
assert.match(hiddenSql, /where hidden_at is null and id = any/);
const plain = await sqlFor(row());
assert.doesNotMatch(plain, /hidden_at/);
assert.doesNotMatch(plain, /update public\.games set/);
});
Deno.test("facts_source und base_game_key landen nicht in der SQL", async () => {
const sql = await sqlFor(row({ facts_source: "GEHEIMER-NACHWEIS", base_game_key: "" }));
assert.doesNotMatch(sql, /GEHEIMER-NACHWEIS|base_game_key|facts_source/);
});
// ---- alt_titles (KAT-01, Spec #3 4.3) --------------------------------------------------------
function altProblems(raw: string, main = "Testspiel A"): string[] {
const problems: string[] = [];
parseAltTitles(raw, main, (m) => problems.push(m));
return problems;
}
Deno.test("alt_titles: Format de:Titel;en:Title, Leerraum am Rand wird entfernt, ':' im Titel ist erlaubt", () => {
const problems: string[] = [];
const parsed = parseAltTitles(" de: Catan: Das Duell ; en:Test Duel ", "Testspiel A", (m) => problems.push(m));
assert.deepEqual(problems, []);
assert.deepEqual(parsed, [{ locale: "de", title: "Catan: Das Duell" }, { locale: "en", title: "Test Duel" }]);
assert.deepEqual(parseAltTitles("", "Testspiel A", () => assert.fail("kein Fehler erwartet")), []);
});
Deno.test("alt_titles: Sprache, fehlender Doppelpunkt, leerer und zu langer Titel werden abgelehnt", () => {
assert.match(altProblems("fr:Titre").join("\n"), /Sprache "fr".*"de" oder "en"/);
assert.match(altProblems("Nur ein Titel").join("\n"), /kein ":"/);
assert.match(altProblems("de: ").join("\n"), /leerer Titel/);
assert.match(altProblems("de:" + "x".repeat(201)).join("\n"), /länger als 200 Zeichen/);
assert.equal(altProblems("de:" + "x".repeat(200)).length, 0);
assert.match(altProblems("de:!!!").join("\n"), /nur aus Sonderzeichen/);
});
Deno.test("alt_titles: Duplikat in derselben Sprache und Titel gleich dem Haupttitel werden abgelehnt", () => {
assert.match(altProblems("de:Café;de:CAFE").join("\n"), /doppelt \(Sprache de\)/);
assert.equal(altProblems("de:Café;en:Café").length, 0, "gleicher Titel in anderer Sprache ist zulässig");
assert.match(altProblems("de:testspiel-a").join("\n"), /gleich dem Haupttitel/);
assert.match(altProblems("en:CATAN®", "Catan").join("\n"), /gleich dem Haupttitel/);
});
Deno.test("alt_titles: ';' im Titel ergibt einen Fehler mit Zeilennummer und Hinweis", async () => {
await rejects(
csv(row(), row({ key: "testspiel-b", title: "Testspiel B", alt_titles: "de:Catan;Das Duell" })),
/startbestand\.csv:3: alt_titles: Eintrag "Das Duell" hat kein ":".*";" ist in alternativen Titeln nicht erlaubt/,
);
await rejects(
csv(row({ alt_titles: "de:Eins;Zwei: Drei" })),
/startbestand\.csv:2: alt_titles: Sprache "Zwei" .*";" ist in alternativen Titeln nicht erlaubt/,
);
});
Deno.test("alt_titles: Fehler anderer Art nennen die Zeilennummer", async () => {
await rejects(csv(row(), row({ key: "testspiel-b", alt_titles: "fr:X" })), /startbestand\.csv:3: alt_titles: Sprache "fr"/);
});
Deno.test("approxNormalize: Marken- und Schutzzeichen, Akzente, ß, Æ, Sonderzeichen", () => {
assert.equal(approxNormalize("CATAN®"), "catan");
assert.equal(approxNormalize("Spiel™Ding"), "spielding"); // Schutzzeichen werden entfernt, nicht zum Trenner (wie search_normalize)
assert.equal(approxNormalize("Café Größe"), "cafe grosse");
assert.equal(approxNormalize("Æon’s End"), "aeon s end");
assert.equal(approxNormalize("7 Wonders: Duel"), "7 wonders duel");
assert.equal(approxNormalize("Ticket to Ride – Europe"), "ticket to ride europe");
assert.equal(approxNormalize("!!!"), "");
assert.equal(approxNormalize("Spiel™ ½"), "spiel 1 2"); // Bruch wie unaccent: NFKD
});
Deno.test("SQL alt_titles: Abgleich über normalisierten Titel, Update nur bei geänderter Schreibweise", async () => {
const sql = await sqlFor(row({ alt_titles: "de:Alter Titel;en:Old 'Title'" }));
assert.match(sql, /with want\(game_id, locale, title\) as \(values/);
assert.match(sql, /'de', 'Alter Titel'\)/);
assert.match(sql, /'en', 'Old ''Title'''\)/); // Apostroph verdoppelt
assert.match(sql, /delete from public\.game_alt_titles a/);
assert.match(sql, /public\.search_normalize\(w\.title\) = a\.title_search/);
assert.match(sql, /on conflict \(game_id, locale, title_search\) do update set title = excluded\.title/);
assert.match(sql, /where public\.game_alt_titles\.title is distinct from excluded\.title;/);
assert.doesNotMatch(sql, /game_alt_titles \(id/); // keine abgeleitete ID
});
Deno.test("SQL alt_titles: ohne alternative Titel nur das Löschen, nie ein leeres values", async () => {
const sql = await sqlFor(row());
assert.match(sql, /delete from public\.game_alt_titles a where a\.game_id = any\(array\['/);
assert.doesNotMatch(sql, /with want\(game_id, locale, title\)/);
assert.doesNotMatch(sql, /insert into public\.game_alt_titles/);
});
Deno.test("checks.sql: Informationszeile zu Startbestandsspielen mit alternativen Titeln", async () => {
const out = await build({ vocabularyCsv: VOCAB, startbestandCsv: csv(row()), previousKeysCsv: "key,uuid\n" });
assert.match(out["checks.sql"], /'Info-Alt', 'Startbestandsspiele mit alternativen Titeln'/);
assert.match(out["checks.sql"], /count\(distinct a\.game_id\) as n from public\.game_alt_titles a join sb on sb\.id = a\.game_id/);
});
Deno.test("SQL: Apostroph im Text wird verdoppelt, Upsert nutzt is distinct from, Zuordnungen werden abgeglichen", async () => {
const sql = await sqlFor(row({ title: "Wo's lang", description: "Es ist 'nett'." }));
assert.match(sql, /'Wo''s lang'/);
assert.match(sql, /'Es ist ''nett''\.'/);
assert.match(sql, /on conflict \(id\) do update set/);
assert.match(sql, /is distinct from \(excluded\.title/);
assert.match(sql, /on conflict \(\(lower\(name\)\)\) do nothing/); // Verlage
assert.match(sql, /delete from public\.game_genres/);
assert.match(sql, /delete from public\.game_mechanics/);
assert.match(sql, /delete from public\.game_publishers/);
assert.match(sql, /on conflict \(game_id, genre_id\) do nothing/);
assert.match(sql, /Genre fehlt in der Datenbank/); // Wächter gegen still verlorene Zuordnungen
assert.doesNotMatch(sql, /image_path|bgg_id|catalog_source_id|source_ref/); // SB6
});
Deno.test("SQL: leerer Startbestand ergibt eine gültige, leere Datei", async () => {
const sql = generateStartbestandSql([]);
assert.match(sql, /keine Spiele/);
assert.doesNotMatch(sql, /insert into/);
});
Deno.test("build: deterministisch, ohne Zeitstempel", async () => {
const input = { vocabularyCsv: VOCAB, startbestandCsv: csv(row(), row({ key: "testspiel-b", title: "B" })), previousKeysCsv: "key,uuid\n" };
assert.deepEqual(await build(input), await build(input));
});
Deno.test("checks.sql: bettet die Startbestands-IDs ein, Sollwert 180, leerer Bestand ist gültiges SQL", async () => {
const out = await build({ vocabularyCsv: VOCAB, startbestandCsv: csv(row()), previousKeysCsv: "key,uuid\n" });
assert.match(out["checks.sql"], new RegExp(await uuidV5("game:testspiel-a")));
assert.match(out["checks.sql"], />= 180/);
const empty = await build({ vocabularyCsv: VOCAB, startbestandCsv: HEADER + "\n", previousKeysCsv: "key,uuid\n" });
assert.match(empty["checks.sql"], /jsonb_array_elements_text\('\[\]'::jsonb\)/);
assert.doesNotMatch(empty["checks.sql"], /values \(\s*\)/);
});
Deno.test("vocabulary.sql: on conflict do nothing ohne Konfliktziel", async () => {
const out = await build({ vocabularyCsv: VOCAB, startbestandCsv: HEADER + "\n", previousKeysCsv: "key,uuid\n" });
assert.match(out["vocabulary.sql"], /insert into public\.genres \(id, name_de, name_en\) values/);
assert.match(out["vocabulary.sql"], /insert into public\.mechanics/);
assert.equal((out["vocabulary.sql"].match(/on conflict do nothing;/g) ?? []).length, 2);
});
// ---- Eingecheckte, erzeugte Dateien sind aktuell ------------------------------------------
Deno.test("die eingecheckten erzeugten Dateien entsprechen den CSV-Quellen", async () => {
const dir = new URL(".", import.meta.url);
const read = (name: string) => Deno.readTextFile(new URL(name, dir));
const real = await build({
vocabularyCsv: await read("vocabulary.csv"),
startbestandCsv: await read("startbestand.csv"),
previousKeysCsv: await read("startbestand-keys.csv"),
});
for (const name of ["vocabulary.sql", "startbestand.sql", "startbestand-keys.csv", "checks.sql"] as const) {
assert.equal(await read(name), real[name], `${name} ist nicht aktuell: build_startbestand.ts ausführen`);
}
const fixture = await build({
vocabularyCsv: await read("testdata/vocabulary.csv"),
startbestandCsv: await read("testdata/startbestand.csv"),
previousKeysCsv: "key,uuid\n",
});
const support = (name: string) => Deno.readTextFile(new URL(`../tests/support/${name}`, dir));
assert.equal(await support("vocabulary_fixture.inc"), fixture["vocabulary.sql"]);
assert.equal(await support("startbestand_fixture.inc"), fixture["startbestand.sql"]);
assert.equal(await support("checks_fixture.inc"), fixture.checksFixtureInclude);
});
Deno.test("Vokabular-Vorschlag: 12-15 Genres, 20-30 Mechaniken (Aufgabe B3a)", async () => {
const v = await parseVocabulary(await Deno.readTextFile(new URL("vocabulary.csv", import.meta.url)));
const genres = v.filter((e) => e.kind === "genre").length;
const mechanics = v.filter((e) => e.kind === "mechanic").length;
assert.ok(genres >= 12 && genres <= 15, `Genres: ${genres}`);
assert.ok(mechanics >= 20 && mechanics <= 30, `Mechaniken: ${mechanics}`);
});