/* Golden tests pinning lib/conjugation.js. This module encodes the seven Korean irregular classes and is the reason the app ships no runtime morphological analyser — surfaceForms() generates the index at build time. It ships unchanged; these tests exist so it stays that way. */ import { describe, it, expect } from "vitest"; import { haeche, past, polite, irregularClass, explain, surfaceForms, IRREGULAR_FORMS, deconjugate, deconjugateCandidates, } from "@lib/conjugation.js"; describe("haeche — the 아/어 rule", () => { const regular: [string, string][] = [ ["먹다", "먹어"], // consonant-final, dark vowel ["앉다", "앉아"], // consonant-final, bright vowel ["좋다", "좋아"], ["읽다", "읽어"], ["가다", "가"], // ㅏ-final stem contracts ["서다", "서"], // ㅓ-final stem contracts ["오다", "와"], // ㅗ + 아 → ㅘ ["주다", "줘"], // ㅜ + 어 → ㅝ ["마시다", "마셔"], // ㅣ + 어 → ㅕ ]; it.each(regular)("%s → %s", (dict, want) => { expect(haeche(dict)).toBe(want); }); it("returns null for anything that is not a dictionary form", () => { expect(haeche("학교")).toBeNull(); expect(haeche("")).toBeNull(); expect(haeche("다")).toBeNull(); }); }); describe("the seven irregular classes", () => { const cases: [string, string, string][] = [ // dict, 해체, class ["크다", "커", "ㅡ"], // ㅡ drops ["바쁘다", "바빠", "ㅡ"], // ㅡ drops, preceding vowel is bright ["슬프다", "슬퍼", "ㅡ"], ["덥다", "더워", "ㅂ"], // ㅂ → 우 ["어렵다", "어려워", "ㅂ"], ["듣다", "들어", "ㄷ"], // ㄷ → ㄹ ["걷다", "걸어", "ㄷ"], ["모르다", "몰라", "르"], // 르 doubles ["빠르다", "빨라", "르"], ["낫다", "나아", "ㅅ"], // ㅅ drops ["짓다", "지어", "ㅅ"], ["그렇다", "그래", "ㅎ"], // ㅎ drops, vowel shifts ["어떻다", "어때", "ㅎ"], ["하다", "해", "special"], ["되다", "돼", "special"], ["이다", "야", "special"], ]; it.each(cases)("%s → %s (%s 불규칙)", (dict, form, cls) => { expect(haeche(dict)).toBe(form); expect(irregularClass(dict)).toBe(cls); }); it("covers all seven classes plus the specials", () => { const seen = new Set(cases.map(([, , c]) => c)); expect(seen).toEqual(new Set(["ㅡ", "ㅂ", "ㄷ", "르", "ㅅ", "ㅎ", "special"])); }); it("every listed irregular form is reachable through haeche()", () => { for (const [dict, form] of Object.entries(IRREGULAR_FORMS)) { expect(haeche(dict), dict).toBe(form); } }); it("calls ordinary stems regular", () => { for (const d of ["먹다", "가다", "읽다", "좋다"]) expect(irregularClass(d), d).toBe("regular"); }); }); describe("past and polite", () => { const cases: [string, string][] = [ ["먹어", "먹었어"], // final present → append 었어 ["가", "갔어"], // open syllable takes ㅆ ["해", "했어"], ["와", "왔어"], ["몰라", "몰랐어"], ]; it.each(cases)("%s → %s", (present, want) => { expect(past(present)).toBe(want); }); it("polite just adds 요", () => { expect(polite("먹어")).toBe("먹어요"); expect(polite(null)).toBeNull(); }); it("past of nothing is nothing", () => { expect(past(null)).toBeNull(); }); }); describe("explain — which rule was missed", () => { it("names the irregular class when there is one", () => { expect(explain("듣다")).toBe("ㄷ 불규칙"); expect(explain("크다")).toBe("ㅡ 불규칙"); }); it("names the 하다 contraction", () => { expect(explain("공부하다")).toBe("하다 → 해"); }); it("otherwise explains the vowel choice", () => { expect(explain("먹다")).toContain("어"); expect(explain("앉다")).toContain("아"); }); }); describe("surfaceForms — the build-time index generator", () => { it("emits 반말, polite and past for a regular verb", () => { const out = surfaceForms("먹다", "to eat"); expect(out.map((s) => s.form)).toEqual(["먹어", "먹어요", "먹었어"]); // "to eat" is stripped to "eat"; the past form is marked as such. expect(out[0]!.gloss).toBe("eat"); expect(out[2]!.gloss).toBe("eat (past)"); // Every form carries its lemma so the index can point back. for (const s of out) expect(s.note).toContain("먹다"); }); it("strips a leading 'to be' as well as 'to'", () => { expect(surfaceForms("좋다", "to be good")[0]!.gloss).toBe("good"); }); it("handles irregulars through the same path", () => { expect(surfaceForms("듣다", "to listen").map((s) => s.form)).toEqual([ "들어", "들어요", "들었어", ]); }); it("emits nothing for a non-verb", () => { expect(surfaceForms("학교", "school")).toEqual([]); }); }); /* Reading an inflected form back to its dictionary entry. Without it every one of 660 realistic inflections of the curriculum's verbs failed to resolve, and the student was told a taught word was not in the list. */ describe("deconjugate", () => { const verbs = new Set(["가다", "앉다", "먹다", "마시다", "좋다"]); const isVerb = (d: string) => verbs.has(d); it.each([ ["갑니다", "가다"], ["먹습니다", "먹다"], ["앉으면", "앉다"], ["가고", "가다"], ["가면", "가다"], ["가네", "가다"], ["간다", "가다"], ["먹었어요", "먹다"], ["좋아서", "좋다"], ])("%s → %s", (form, dict) => { expect(deconjugate(form, isVerb)).toBe(dict); }); it("proposes every plausible dictionary form, unguarded", () => { expect(deconjugateCandidates("갑니다")).toEqual(["가다", "갑니다"]); expect(deconjugateCandidates("앉으면")).toEqual(["앉다", "앉으다"]); }); it("accepts nothing the lexicon does not hold as a verb — 가지 is an eggplant", () => { expect(deconjugateCandidates("가지")).toContain("가다"); expect(deconjugate("가지", () => false)).toBeNull(); }); });