From dcac3ae425342ec28e69b8fb175a9354f35c6029 Mon Sep 17 00:00:00 2001 From: Horizontal42 <75383519+Horizontal42@users.noreply.github.com> Date: Sun, 9 Aug 2026 15:57:13 +0300 Subject: [PATCH] add registry tests --- src/engine/registry.test.ts | 162 ++++++++++++++++++++++++++++++++++++ 1 file changed, 162 insertions(+) create mode 100644 src/engine/registry.test.ts diff --git a/src/engine/registry.test.ts b/src/engine/registry.test.ts new file mode 100644 index 0000000..324e297 --- /dev/null +++ b/src/engine/registry.test.ts @@ -0,0 +1,162 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { buildRegistry, Registry } from "./registry"; +import { lexLine } from "./lexer"; +import { Decimal } from "./types"; + +describe("registry", () => { + let reg: Registry; + + beforeEach(() => { + reg = buildRegistry(); + }); + + describe("buildRegistry", () => { + it("initializes a valid Registry instance with core vocabulary", () => { + // Check core words + const plusPayload = reg.match(lexLine("plus"), ["plus"], 0); + expect(plusPayload).not.toBeNull(); + expect(plusPayload?.payload.t).toBe("op"); + if (plusPayload?.payload.t === "op") expect(plusPayload.payload.op).toBe("plus"); + + const piPayload = reg.match(lexLine("pi"), ["pi"], 0); + expect(piPayload).not.toBeNull(); + expect(piPayload?.payload.t).toBe("const"); + if (piPayload?.payload.t === "const") expect(piPayload.payload.name).toBe("pi"); + }); + + it("registers built-in functions", () => { + expect(reg.isFunc("sin")).toBe(true); + expect(reg.isFunc("sqrt")).toBe(true); + expect(reg.isFunc("random")).toBe(true); + expect(reg.isFunc("unknownFunc")).toBe(false); + + const sinPayload = reg.match(lexLine("sin"), ["sin"], 0); + expect(sinPayload).not.toBeNull(); + expect(sinPayload?.payload.t).toBe("func"); + }); + + it("registers common units", () => { + // "meter" + const meterPayload = reg.match(lexLine("meter"), ["meter"], 0); + expect(meterPayload).not.toBeNull(); + expect(meterPayload?.payload.t).toBe("unit"); + + // "kg" (strict/lenient) + const kgPayload = reg.match(lexLine("kg"), ["kg"], 0); + expect(kgPayload).not.toBeNull(); + expect(kgPayload?.payload.t).toBe("unit"); + + // "USD" (strict/lenient currency) + const usdPayload = reg.match(lexLine("USD"), ["usd"], 0); + expect(usdPayload).not.toBeNull(); + expect(usdPayload?.payload.t).toBe("currency"); + }); + }); + + describe("match", () => { + it("matches single-word phrases", () => { + const match = reg.match(lexLine("usd"), ["usd"], 0); + expect(match).not.toBeNull(); + expect(match?.length).toBe(1); + }); + + it("matches multi-word phrases", () => { + const match = reg.match(lexLine("square meter"), ["square", "meter"], 0); + expect(match).not.toBeNull(); + expect(match?.length).toBe(2); + expect(match?.payload.t).toBe("unit"); + }); + + it("respects case-sensitivity", () => { + // "m" (milli) vs "M" (mega) + const mMatch = reg.match(lexLine("mm"), ["mm"], 0); + const mPayload = mMatch?.payload; + expect(mPayload?.t).toBe("unit"); + + const MMatch = reg.match(lexLine("Mm"), ["mm"], 0); + const MPayload = MMatch?.payload; + expect(MPayload?.t).toBe("unit"); + + if (mPayload?.t === "unit" && MPayload?.t === "unit") { + expect(mPayload.unit.id).not.toBe(MPayload.unit.id); + expect(mPayload.unit.id).toBe("milli:meter"); + expect(MPayload.unit.id).toBe("mega:meter"); + } + }); + + it("respects mustTouch flag", () => { + // "log2" vs "log 2" + const lex2 = lexLine("log2"); + const log2Match = reg.match(lex2, lex2.map(l => l.raw), 0); + expect(log2Match).not.toBeNull(); + expect(log2Match?.payload.t).toBe("func"); + if (log2Match?.payload.t === "func") expect(log2Match.payload.name).toBe("log2"); + + const lexSpace2 = lexLine("log 2"); + const logSpace2Match = reg.match(lexSpace2, lexSpace2.map(l => l.raw), 0); + // "log 2" should match "log", length 1, not "log2" + expect(logSpace2Match).not.toBeNull(); + expect(logSpace2Match?.length).toBe(1); + if (logSpace2Match?.payload.t === "func") expect(logSpace2Match.payload.name).toBe("log"); + }); + + it("returns null for no match", () => { + const match = reg.match(lexLine("unknownword"), ["unknownword"], 0); + expect(match).toBeNull(); + }); + }); + + describe("currency and rates", () => { + it("sets and updates conversion rates", () => { + reg.setRates({ "EUR": 0.9, "GBP": "0.8" }); + expect(reg.rates.get("EUR")?.toNumber()).toBe(0.9); + expect(reg.rates.get("GBP")?.toNumber()).toBe(0.8); + // USD is implicitly set to 1 + expect(reg.rates.get("USD")?.toNumber()).toBe(1); + }); + + it("ignores negative rates", () => { + reg.setRates({ "EUR": 0.9, "INVALID": -1 }); + expect(reg.rates.get("EUR")?.toNumber()).toBe(0.9); + expect(reg.rates.has("INVALID")).toBe(false); + }); + + it("creates currency unit from rate", () => { + const unit = reg.makeCurrencyUnitFromRate("EUR", new Decimal(0.9)); + expect(unit.id).toBe("EUR"); + expect(unit.dimension).toBe("currency"); + // 1 / 0.9 = 1.111... + expect(unit.ratio.toNumber()).toBeCloseTo(1.1111, 4); + }); + + it("makes currency unit if rate exists", () => { + reg.setRates({ "EUR": 0.9 }); + const unit = reg.makeCurrencyUnit("EUR"); + expect(unit).not.toBeNull(); + expect(unit?.id).toBe("EUR"); + + const noUnit = reg.makeCurrencyUnit("JPY"); + expect(noUnit).toBeNull(); + }); + }); + + describe("completions", () => { + it("populates completions with expected categories", () => { + expect(reg.completions.length).toBeGreaterThan(0); + + const types = new Set(reg.completions.map(c => c.type)); + expect(types.has("unit")).toBe(true); + expect(types.has("currency")).toBe(true); + expect(types.has("function")).toBe(true); + expect(types.has("keyword")).toBe(true); + }); + + it("contains specific known completions", () => { + const labels = reg.completions.map(c => c.label); + expect(labels).toContain("USD"); + expect(labels).toContain("meter"); + expect(labels).toContain("sin"); + expect(labels).toContain("sum"); + }); + }); +});