/* What a full wipe has to leave behind: nothing the learner did. The reported failure was that "Reset everything" did not clear learned words. It did delete them — and then also deleted the `seed.known` guard, so the next boot re-seeded ~47 cards from the known-words list and the deck looked untouched. The seed is gone now; these tests pin the wipe so a future seed cannot reintroduce the same shape. */ import { describe, it, expect, beforeEach, afterEach } from "vitest"; import { SqliteWasmDb } from "@app/db/sqlite-wasm-core.js"; import { migrate } from "@app/db/migrate.js"; import { editCard, editChatTurn, editMeta, editReset, editStudyLog, insertBand, seedCard, type LemmaRow, } from "@app/db/writes.js"; import { newCard, markKnown } from "@lib/srs.js"; import type { Db } from "@app/db/types.js"; let db: Db; beforeEach(async () => { db = await SqliteWasmDb.open({ memory: true }); await migrate(db); }); afterEach(async () => { await db.close(); }); const lemma = (id: number, headword: string): LemmaRow => ({ id, headword, pos: "noun", freq_rank: id, level: null, gloss_en: `gloss ${id}`, gloss_ko: "", unit_band: 0, source: "curated", }); const count = async (tbl: string): Promise => (await db.get<{ n: number }>(`SELECT COUNT(*) AS n FROM ${tbl}`))!.n; async function aLearnerWithHistory(): Promise { await insertBand(db, [lemma(1, "물"), lemma(2, "책"), lemma(3, "가다")], []); await editCard(db, 1, markKnown(10)); await editCard(db, 2, newCard()); await editStudyLog(db, 10, { reviews: 4, correct: 3 }); await editChatTurn(db, "user", "Start unit 1.1."); await editChatTurn(db, "assistant", "좋아요."); await editMeta(db, "prefs.focus", "particles"); await editMeta(db, "grammar.learned", '["p1"]'); await db.run("INSERT INTO progress (unit_id, confidence, updated_at) VALUES ('1.1', 40, 1)"); } describe("editReset('everything')", () => { it("leaves no cards, no study log, no transcript, no progress", async () => { await aLearnerWithHistory(); await editReset(db, "everything"); for (const tbl of ["card", "study_log", "chat", "progress", "peek"]) { expect(await count(tbl), `${tbl} should be empty after a full wipe`).toBe(0); } }); it("clears the learner's preferences, grammar flags and notes", async () => { await aLearnerWithHistory(); await editReset(db, "everything"); const left = await db.all<{ k: string }>( "SELECT k FROM meta WHERE k LIKE 'prefs.%' OR k LIKE 'grammar.%' OR k LIKE 'trainer.%'", ); expect(left).toEqual([]); }); it("keeps the dictionary, which is reference data and not the learner's", async () => { await aLearnerWithHistory(); await editReset(db, "everything"); expect(await count("lemma")).toBe(3); }); it("tombstones what it deleted, so a sync cannot restore it", async () => { await aLearnerWithHistory(); await editReset(db, "everything"); const graves = await db.all<{ tbl: string }>("SELECT DISTINCT tbl FROM tombstone"); const tables = graves.map((g) => g.tbl).sort(); expect(tables).toContain("card"); expect(tables).toContain("chat"); expect(tables).toContain("progress"); expect(tables).toContain("study_log"); }); it("does not survive a reboot: nothing reseeds the deck", async () => { await aLearnerWithHistory(); await editReset(db, "everything"); // Re-running the migrations is what a restart does. await migrate(db); expect(await count("card")).toBe(0); }); }); describe("editReset('roadmap')", () => { it("clears the roadmap and transcript but keeps the deck and history", async () => { await aLearnerWithHistory(); await editReset(db, "roadmap"); expect(await count("progress")).toBe(0); expect(await count("chat")).toBe(0); expect(await count("card")).toBe(2); expect(await count("study_log")).toBe(1); }); }); describe("migration 5 — the known-words seed is gone", () => { it("removes an existing install's seeded cards and keeps graded ones", async () => { await insertBand(db, [lemma(1, "물"), lemma(2, "책")], []); await seedCard(db, 1, markKnown(10)); // what the seed used to write await editCard(db, 2, newCard()); // something the learner actually did await db.run("INSERT INTO meta (k, v, updated_at) VALUES ('seed.known', '47', 0)"); // Wind the recorded version back to 4 so migrate() really runs 5. await db.run("INSERT OR REPLACE INTO meta (k, v, updated_at) VALUES ('schema_version','4',0)"); const { from, to } = await migrate(db); expect(from).toBe(4); expect(to).toBe(5); const rows = await db.all<{ lemma_id: number }>("SELECT lemma_id FROM card"); expect(rows.map((r) => r.lemma_id)).toEqual([2]); expect(await db.get("SELECT v FROM meta WHERE k = 'seed.known'")).toBeUndefined(); }); });