import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { __clearStagedDocsForTest, detectStagedExt, getStagedDoc, hasStagedDoc, parseStagedSource, putStagedDoc, stagedSourceUrl, StagedCapError, } from "@/lib/staged-docs"; // PLAN-STAGED.md D2 — RAM staging (pure module ไม่ต้องตั้ง DATABASE_URL) // ใช้ fake timers คุม Date.now ของ TTL sliding + env stub ปรับ cap ต่อเคส // (store เก็บบน globalThis จึงต้อง __clearStagedDocsForTest ทุกเคส) const ID_A = "0d7e6a52-9a71-4d40-bd1c-b2e1a44ab133"; const ID_B = "11111111-2222-4333-8444-555555555555"; const ID_C = "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee"; beforeEach(() => { __clearStagedDocsForTest(); }); afterEach(() => { vi.unstubAllEnvs(); vi.useRealTimers(); }); describe("sentinel: stagedSourceUrl/parseStagedSource", () => { it("สร้าง + แกะกลับได้ครบทุก ext", () => { for (const ext of ["pdf", "jpg", "png", "tif"] as const) { const url = stagedSourceUrl(ID_A, ext); expect(url).toBe(`staged:${ID_A}.${ext}`); expect(parseStagedSource(url)).toEqual({ fileId: ID_A, ext }); } }); it("ปฏิเสธ uuid/ext เพี้ยนและค่าที่ไม่ใช่ sentinel", () => { expect(parseStagedSource(`staged:not-a-uuid.pdf`)).toBeNull(); expect(parseStagedSource(`staged:${ID_A}.exe`)).toBeNull(); expect(parseStagedSource(`staged:${ID_A}`)).toBeNull(); expect(parseStagedSource(`staged:.pdf`)).toBeNull(); expect(parseStagedSource(`/samples/${ID_A}.pdf`)).toBeNull(); expect(parseStagedSource("https://x.go.th/a.pdf")).toBeNull(); expect(parseStagedSource("")).toBeNull(); }); }); describe("detectStagedExt — magic bytes", () => { it("แยก pdf/jpg/png/tif (ทั้งสอง endian) ถูก", () => { expect(detectStagedExt(Buffer.from("%PDF-1.7\n..."))).toBe("pdf"); expect(detectStagedExt(Buffer.from([0xff, 0xd8, 0xff, 0xe0, 0x00]))).toBe("jpg"); expect(detectStagedExt(Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d]))).toBe("png"); expect(detectStagedExt(Buffer.from([0x49, 0x49, 0x2a, 0x00, 0x00]))).toBe("tif"); expect(detectStagedExt(Buffer.from([0x4d, 0x4d, 0x00, 0x2a, 0x00]))).toBe("tif"); }); it("ไม่รู้จัก = null (รวมไฟล์ว่าง/GIF/ตัด magic สั้นเกิน)", () => { expect(detectStagedExt(Buffer.alloc(0))).toBeNull(); expect(detectStagedExt(Buffer.from("GIF89a"))).toBeNull(); expect(detectStagedExt(Buffer.from([0x89, 0x50]))).toBeNull(); expect(detectStagedExt(Buffer.from("%PD"))).toBeNull(); }); }); describe("put/get ตัวตน", () => { it("put แล้ว get ได้ bytes เดิม (อ่านซ้ำก็ได้)", () => { const bytes = Buffer.from("%PDF-1.4 เอกสารทดสอบ"); putStagedDoc(ID_A, bytes, "pdf"); expect(getStagedDoc(`staged:${ID_A}.pdf`)).toEqual(bytes); expect(getStagedDoc(`staged:${ID_A}.pdf`)).toEqual(bytes); expect(hasStagedDoc(`staged:${ID_A}.pdf`)).toBe(true); }); it("get ด้วย ext ที่ไม่ตรง entry = miss แต่ไม่ทำลาย entry เดิม", () => { const bytes = Buffer.from("%PDF-1.4"); putStagedDoc(ID_A, bytes, "pdf"); expect(getStagedDoc(`staged:${ID_A}.jpg`)).toBeNull(); expect(getStagedDoc(`staged:${ID_A}.pdf`)).toEqual(bytes); }); it("replace ช่องเดิมต่อ fileId — ไม่งอก entry ใหม่ต่อครั้ง", () => { vi.stubEnv("STAGED_MAX_TOTAL_BYTES", "1000"); putStagedDoc(ID_B, Buffer.alloc(500, 1), "pdf"); putStagedDoc(ID_B, Buffer.alloc(300, 2), "pdf"); // ถ้างอกเป็น 2 entry รวม 800 + ID_C 400 = 1200 > 1000 จะโดน evict — แทนที่แล้ว // รวม 300 + 400 = 700 ทุกตัวรอด putStagedDoc(ID_C, Buffer.alloc(400, 3), "pdf"); expect(getStagedDoc(`staged:${ID_B}.pdf`)).toEqual(Buffer.alloc(300, 2)); expect(getStagedDoc(`staged:${ID_C}.pdf`)).toEqual(Buffer.alloc(400, 3)); }); }); describe("TTL sliding", () => { it("get ที่สำเร็จ = touch — ใช้งานต่อเนื่องไม่หมดอายุ / ปล่อยเลยหมดแล้วถูกเก็บกวาด", () => { vi.useFakeTimers(); vi.stubEnv("STAGED_DOC_TTL_SECONDS", "10"); const bytes = Buffer.from("%PDF-1.4"); putStagedDoc(ID_A, bytes, "pdf"); vi.advanceTimersByTime(9_000); expect(getStagedDoc(`staged:${ID_A}.pdf`)).toEqual(bytes); // touch ที่ 9 วิ vi.advanceTimersByTime(9_000); // 18 วินับจาก put แต่ 9 วินับจาก touch ล่าสุด expect(getStagedDoc(`staged:${ID_A}.pdf`)).toEqual(bytes); vi.advanceTimersByTime(11_000); // ไม่มีใคร touch 11 วิ = พ้น TTL expect(getStagedDoc(`staged:${ID_A}.pdf`)).toBeNull(); expect(hasStagedDoc(`staged:${ID_A}.pdf`)).toBe(false); // หมดอายุแล้ว get เก็บกวาดทิ้ง — put ใหม่ได้ตามปกติ putStagedDoc(ID_A, bytes, "pdf"); expect(hasStagedDoc(`staged:${ID_A}.pdf`)).toBe(true); }); it("hasStagedDoc ไม่ touch TTL (lastAccessAt ไม่ขยับ)", () => { vi.useFakeTimers(); vi.stubEnv("STAGED_DOC_TTL_SECONDS", "10"); const bytes = Buffer.from("%PDF-1.4"); putStagedDoc(ID_A, bytes, "pdf"); vi.advanceTimersByTime(8_000); expect(hasStagedDoc(`staged:${ID_A}.pdf`)).toBe(true); // อีก 5 วิ: รวม 13 วิจาก put — ถ้า has() แอบ touch ตัวนี้จะยังไม่หมดอายุ (5 < 10) vi.advanceTimersByTime(5_000); expect(hasStagedDoc(`staged:${ID_A}.pdf`)).toBe(false); expect(getStagedDoc(`staged:${ID_A}.pdf`)).toBeNull(); }); }); describe("cap + LRU", () => { it("เกิน total cap = evict รายการ lastAccessAt เก่าสุด (ผู้เพิ่งใช้รอด)", () => { vi.useFakeTimers(); vi.stubEnv("STAGED_MAX_TOTAL_BYTES", "1000"); putStagedDoc(ID_A, Buffer.alloc(400), "pdf"); // t=0 vi.advanceTimersByTime(1_000); putStagedDoc(ID_B, Buffer.alloc(400), "pdf"); // t=1s vi.advanceTimersByTime(1_000); expect(getStagedDoc(`staged:${ID_A}.pdf`)).toEqual(Buffer.alloc(400)); // A ถูก touch ล่าสุด putStagedDoc(ID_C, Buffer.alloc(400), "pdf"); // รวม 1200 > 1000 → evict B (เก่าสุด) expect(hasStagedDoc(`staged:${ID_B}.pdf`)).toBe(false); expect(hasStagedDoc(`staged:${ID_A}.pdf`)).toBe(true); expect(hasStagedDoc(`staged:${ID_C}.pdf`)).toBe(true); }); it("ไฟล์เดี่ยวใหญ่กว่า total cap = put ปฏิเสธ (StagedCapError)", () => { vi.stubEnv("STAGED_MAX_TOTAL_BYTES", "1000"); expect(() => putStagedDoc(ID_A, Buffer.alloc(1001), "pdf")).toThrow(StagedCapError); expect(hasStagedDoc(`staged:${ID_A}.pdf`)).toBe(false); }); });