import { describe, it, expect } from "vitest"; import { clamp, approxEqual, roundTo, solveRootBisection, sampleCurve, } from "../../src/engine/shared/math"; import { GLOSSARY, getGlossaryEntry } from "../../src/engine/shared/glossary"; import { createCobbDouglas, createShiftedCobbDouglas, } from "../../src/engine/micro/preferences"; import { createLinearBudget, createRationedBudget, createPriceCeilingBudget, createInKindBudget, } from "../../src/engine/micro/constraints"; import { solveConsumerOptimum } from "../../src/engine/micro/solver"; import { findPriceCeilingBreakevenX, analyzeInKindVsCash, } from "../../src/engine/micro/welfare"; describe("Shared Math Utils", () => { it("clamp giới hạn giá trị chính xác", () => { expect(clamp(5, 0, 10)).toBe(5); expect(clamp(-2, 0, 10)).toBe(0); expect(clamp(15, 0, 10)).toBe(10); }); it("approxEqual kiểm tra dung sai số thực", () => { expect(approxEqual(1.00001, 1.0, 1e-4)).toBe(true); expect(approxEqual(1.01, 1.0, 1e-4)).toBe(false); }); it("roundTo làm tròn thập phân", () => { expect(roundTo(3.14159, 2)).toBe(3.14); expect(roundTo(3.14159, 4)).toBe(3.1416); }); it("solveRootBisection tìm nghiệm chính xác", () => { // f(x) = x^2 - 2 = 0 trên [1, 2] => x = sqrt(2) const root = solveRootBisection((x) => x * x - 2, 1, 2, 1e-8); expect(root).toBeCloseTo(Math.SQRT2, 6); }); it("sampleCurve lấy mẫu đường cong", () => { const points = sampleCurve((x) => 100 / x, 2, 10, 8); expect(points.length).toBe(9); expect(points[0].x).toBe(2); expect(points[0].y).toBe(50); }); }); describe("Shared Glossary & Tiêu chuẩn hiển thị", () => { it("chứa đầy đủ các thuật ngữ Vi mô và Vĩ mô trọng yếu", () => { const microKeys = ["I", "Px", "Py", "X", "Y", "U", "MRS", "CS", "PS", "DWL", "MC", "MR"]; const macroKeys = ["Y_macro", "C", "I_macro", "G", "NX", "r", "i", "P", "pi", "u", "e", "k"]; microKeys.forEach((key) => { const entry = getGlossaryEntry(key); expect(entry).toBeDefined(); expect(entry.domain).toBe("micro"); }); macroKeys.forEach((key) => { const entry = getGlossaryEntry(key); expect(entry).toBeDefined(); expect(entry.domain).toBe("macro"); }); }); it("Quy tắc bắt buộc: KHÔNG có ký tự gạch nối/ngang trong văn bản tiếng Việt hiển thị", () => { // Regex kiểm tra gạch nối, gạch ngang làm dấu câu/phân cách // Cho phép công thức toán, nhưng trong name_vi và description không được có —, –, hoặc - làm dấu câu const disallowedDashPattern = /[—–]|(\s-\s)|(^-\s)/; Object.values(GLOSSARY).forEach((entry) => { expect(entry.name_vi).not.toMatch(disallowedDashPattern); expect(entry.description).not.toMatch(disallowedDashPattern); // Đảm bảo không chứa dấu gạch ngang đơn độc expect(entry.name_vi).not.toContain("—"); expect(entry.name_vi).not.toContain("–"); expect(entry.description).not.toContain("—"); expect(entry.description).not.toContain("–"); }); }); }); describe("Module 1: Định lượng hàng hóa (Rationing)", () => { const pref = createCobbDouglas({ alpha: 0.5, productForm: true }); const linearBudget = createLinearBudget({ I: 200, Px: 10, Py: 10 }); it("Cân bằng tự do ban đầu A(10, 10), U_A = 100, MRS_A = 1.00", () => { const optA = solveConsumerOptimum(pref, linearBudget); expect(optA.x).toBe(10); expect(optA.y).toBe(10); expect(optA.utility).toBe(100); expect(optA.mrs).toBeCloseTo(1.0, 4); expect(optA.solutionType).toBe("interior"); expect(optA.isBinding).toBe(false); }); it("Khi quotaX = 6 (hạn ngạch siết chặt): B(6, 14), U_B = 84, MRS_B ≈ 2.3333, corner_kink", () => { const budget = createRationedBudget({ I: 200, Px: 10, Py: 10, quotaX: 6 }); const optB = solveConsumerOptimum(pref, budget); expect(optB.x).toBe(6); expect(optB.y).toBe(14); expect(optB.utility).toBe(84); expect(optB.mrs).toBeCloseTo(14 / 6, 4); expect(optB.solutionType).toBe("corner_kink"); expect(optB.isBinding).toBe(true); }); it("Khi quotaX = 12 (hạn ngạch không ràng buộc): B(10, 10), U_B = 100, interior", () => { const budget = createRationedBudget({ I: 200, Px: 10, Py: 10, quotaX: 12 }); const optB = solveConsumerOptimum(pref, budget); expect(optB.x).toBe(10); expect(optB.y).toBe(10); expect(optB.utility).toBe(100); expect(optB.mrs).toBeCloseTo(1.0, 4); expect(optB.solutionType).toBe("interior"); expect(optB.isBinding).toBe(false); }); }); describe("Module 2: Giá trần và thiếu hụt (Price Ceiling & Shortage)", () => { const pref = createCobbDouglas({ alpha: 0.5, productForm: true }); it("Khi Px_ceiling = 5, supplyCapX = 6: B(6, 17), U_B = 102 > 100 (dư giả hơn)", () => { const budget = createPriceCeilingBudget({ I: 200, Px_ceiling: 5, Py: 10, supplyCapX: 6, }); const optB = solveConsumerOptimum(pref, budget); expect(optB.x).toBe(6); expect(optB.y).toBe(17); expect(optB.utility).toBe(102); expect(optB.solutionType).toBe("corner_kink"); expect(optB.isBinding).toBe(true); }); it("Khi Px_ceiling = 5, supplyCapX = 4: B(4, 18), U_B = 72 < 100 (khó khăn hơn)", () => { const budget = createPriceCeilingBudget({ I: 200, Px_ceiling: 5, Py: 10, supplyCapX: 4, }); const optB = solveConsumerOptimum(pref, budget); expect(optB.x).toBe(4); expect(optB.y).toBe(18); expect(optB.utility).toBe(72); expect(optB.solutionType).toBe("corner_kink"); expect(optB.isBinding).toBe(true); }); it("findPriceCeilingBreakevenX trả về chính xác 20 - sqrt(200) ≈ 5.857864", () => { const breakevenX = findPriceCeilingBreakevenX({ pref, I: 200, Px_ceiling: 5, Py: 10, baseUtility: 100, }); const expectedX = 20 - Math.sqrt(200); expect(breakevenX).toBeCloseTo(expectedX, 5); expect(breakevenX).toBeCloseTo(5.857864, 5); // Tại điểm hòa vốn, thỏa dụng phải xấp xỉ đúng 100 const breakevenY = (200 - 5 * breakevenX) / 10; const uBreakeven = pref.utility(breakevenX, breakevenY); expect(uBreakeven).toBeCloseTo(100, 4); }); }); describe("Module 3A: Giải pháp góc trên trục (Hình 3.15 Shifted Cobb-Douglas)", () => { const I = 200; const Px = 10; const Py = 10; const budget = createLinearBudget({ I, Px, Py }); it("Tại alpha = 0.55: nghiệm nội điểm E(12, 8), MRS = 1.00, interior", () => { const pref = createShiftedCobbDouglas({ alpha: 0.55, shift: 10 }); const opt = solveConsumerOptimum(pref, budget); expect(opt.x).toBeCloseTo(12, 4); expect(opt.y).toBeCloseTo(8, 4); expect(opt.mrs).toBeCloseTo(1.0, 4); expect(opt.solutionType).toBe("interior"); }); it("Tại alpha = 0.75: chạm đúng ranh giới góc (20, 0), MRS = 1.00", () => { const pref = createShiftedCobbDouglas({ alpha: 0.75, shift: 10 }); const opt = solveConsumerOptimum(pref, budget); expect(opt.x).toBeCloseTo(20, 4); expect(opt.y).toBeCloseTo(0, 4); expect(opt.mrs).toBeCloseTo(1.0, 4); }); it("Tại alpha = 0.85: nghiệm góc trên trục hoành B(20, 0), MRS ≈ 1.8889 > 1, corner_axis", () => { const pref = createShiftedCobbDouglas({ alpha: 0.85, shift: 10 }); const opt = solveConsumerOptimum(pref, budget); expect(opt.x).toBe(20); expect(opt.y).toBe(0); const expectedMRS = (0.85 / 0.15) * (10 / 30); expect(opt.mrs).toBeCloseTo(expectedMRS, 4); expect(opt.mrs).toBeCloseTo(1.8889, 4); expect(opt.mrs).toBeGreaterThan(1.0); expect(opt.solutionType).toBe("corner_axis"); }); it("Tại alpha = 0.20: nghiệm góc trên trục tung B(0, 20), MRS = 0.75 < 1, corner_axis", () => { const pref = createShiftedCobbDouglas({ alpha: 0.2, shift: 10 }); const opt = solveConsumerOptimum(pref, budget); expect(opt.x).toBe(0); expect(opt.y).toBe(20); const expectedMRS = (0.2 / 0.8) * (30 / 10); expect(opt.mrs).toBeCloseTo(expectedMRS, 4); expect(opt.mrs).toBeCloseTo(0.75, 4); expect(opt.mrs).toBeLessThan(1.0); expect(opt.solutionType).toBe("corner_axis"); }); }); describe("Module 3B: Phân tích Trợ cấp Hiện vật vs Tiền mặt (analyzeInKindVsCash)", () => { it("Kiểm tra solveConsumerOptimum với InKindBudget trực tiếp", () => { const pref = createCobbDouglas({ alpha: 0.3, productForm: false }); const budget = createInKindBudget({ I0: 100, transferX: 80, Px: 1, Py: 1 }); const opt = solveConsumerOptimum(pref, budget); expect(opt.x).toBe(80); expect(opt.y).toBe(100); expect(opt.solutionType).toBe("corner_kink"); expect(opt.isBinding).toBe(true); }); describe("Preset Jane (Quỹ ủy thác giáo dục: alpha = 0.30, I0 = 100)", () => { it("Khi T = 80 > T* ≈ 42.86: B kẹt tại (80, 100), méo mó, C(54, 126), CE ≈ 72.27, DWL ≈ 7.73", () => { const res = analyzeInKindVsCash({ alpha: 0.3, I0: 100, transfer: 80, Px: 1, Py: 1 }); expect(res.bindingThresholdT).toBeCloseTo(42.8571, 3); expect(res.isDistorted).toBe(true); // Điểm A expect(res.pointA.x).toBe(30); expect(res.pointA.y).toBe(70); // Điểm B (Hiện vật) expect(res.pointB.x).toBe(80); expect(res.pointB.y).toBe(100); expect(res.pointB.utility).toBeCloseTo(93.5248, 3); expect(res.pointB.mrs).toBeCloseTo(0.5357, 3); // Điểm C (Tiền mặt) expect(res.pointC.x).toBe(54); expect(res.pointC.y).toBe(126); expect(res.pointC.utility).toBeCloseTo(97.72, 2); expect(res.pointC.utility).toBeGreaterThan(res.pointB.utility); // Phúc lợi expect(res.cashEquivalent).toBeCloseTo(72.27, 2); expect(res.deadweightLoss).toBeCloseTo(7.73, 2); }); it("Khi T = 40 < T*: không méo mó, B trùng C tại (42, 98), U_B = U_C, DWL = 0", () => { const res = analyzeInKindVsCash({ alpha: 0.3, I0: 100, transfer: 40, Px: 1, Py: 1 }); expect(res.isDistorted).toBe(false); expect(res.pointB.x).toBeCloseTo(42, 4); expect(res.pointB.y).toBeCloseTo(98, 4); expect(res.pointC.x).toBeCloseTo(42, 4); expect(res.pointC.y).toBeCloseTo(98, 4); expect(res.pointB.utility).toBeCloseTo(res.pointC.utility, 4); expect(res.deadweightLoss).toBe(0); expect(res.cashEquivalent).toBe(40); }); }); describe("Preset Food Stamps (Tem phiếu thực phẩm: alpha = 0.25, I0 = 100)", () => { it("Khi T = 70 > T* ≈ 33.33: B(70, 100), C(42.5, 127.5), CE ≈ 60.51, DWL ≈ 9.49", () => { const res = analyzeInKindVsCash({ alpha: 0.25, I0: 100, transfer: 70, Px: 1, Py: 1 }); expect(res.bindingThresholdT).toBeCloseTo(33.3333, 3); expect(res.isDistorted).toBe(true); // Điểm A expect(res.pointA.x).toBe(25); expect(res.pointA.y).toBe(75); // Điểm B expect(res.pointB.x).toBe(70); expect(res.pointB.y).toBe(100); expect(res.pointB.utility).toBeCloseTo(91.4691, 3); expect(res.pointB.mrs).toBeCloseTo(0.4762, 3); // Điểm C expect(res.pointC.x).toBe(42.5); expect(res.pointC.y).toBe(127.5); expect(res.pointC.utility).toBeCloseTo(96.88, 2); expect(res.pointC.utility).toBeGreaterThan(res.pointB.utility); // Phúc lợi expect(res.cashEquivalent).toBeCloseTo(60.51, 2); expect(res.deadweightLoss).toBeCloseTo(9.49, 2); }); it("Khi T = 30 < T*: B trùng C tại (32.5, 97.5), DWL = 0", () => { const res = analyzeInKindVsCash({ alpha: 0.25, I0: 100, transfer: 30, Px: 1, Py: 1 }); expect(res.isDistorted).toBe(false); expect(res.pointB.x).toBeCloseTo(32.5, 4); expect(res.pointB.y).toBeCloseTo(97.5, 4); expect(res.pointC.x).toBeCloseTo(32.5, 4); expect(res.pointC.y).toBeCloseTo(97.5, 4); expect(res.pointB.utility).toBeCloseTo(res.pointC.utility, 4); expect(res.deadweightLoss).toBe(0); expect(res.cashEquivalent).toBe(30); }); }); });