WIP! A BB-style forum, on the ATmosphere! We're still working... we'll be back soon when we have something to show off!
node typescript hono htmx atproto
at root/atb-56-theme-caching-layer 213 lines 6.5 kB view raw
1import { describe, it, expect, beforeEach, vi, afterEach } from "vitest"; 2import { CursorManager } from "../cursor-manager.js"; 3import { createMockLogger } from "./mock-logger.js"; 4import type { Database } from "@atbb/db"; 5 6describe("CursorManager", () => { 7 let mockDb: Database; 8 let cursorManager: CursorManager; 9 let mockLogger: ReturnType<typeof createMockLogger>; 10 11 beforeEach(() => { 12 mockLogger = createMockLogger(); 13 14 // Create mock database with common patterns 15 const mockInsert = vi.fn().mockReturnValue({ 16 values: vi.fn().mockReturnValue({ 17 onConflictDoUpdate: vi.fn().mockResolvedValue(undefined), 18 }), 19 }); 20 21 const mockSelect = vi.fn().mockReturnValue({ 22 from: vi.fn().mockReturnValue({ 23 where: vi.fn().mockReturnValue({ 24 limit: vi.fn().mockResolvedValue([]), 25 }), 26 }), 27 }); 28 29 mockDb = { 30 insert: mockInsert, 31 select: mockSelect, 32 } as unknown as Database; 33 34 cursorManager = new CursorManager(mockDb, mockLogger); 35 }); 36 37 afterEach(() => { 38 vi.clearAllMocks(); 39 }); 40 41 describe("load", () => { 42 it("should return null when no cursor exists", async () => { 43 // Mock empty result 44 vi.spyOn(mockDb, "select").mockReturnValue({ 45 from: vi.fn().mockReturnValue({ 46 where: vi.fn().mockReturnValue({ 47 limit: vi.fn().mockResolvedValue([]), 48 }), 49 }), 50 } as any); 51 52 const cursor = await cursorManager.load(); 53 expect(cursor).toBeNull(); 54 }); 55 56 it("should return saved cursor when it exists", async () => { 57 const savedCursor = BigInt(1234567890000000); 58 59 // Mock cursor retrieval 60 vi.spyOn(mockDb, "select").mockReturnValue({ 61 from: vi.fn().mockReturnValue({ 62 where: vi.fn().mockReturnValue({ 63 limit: vi.fn().mockResolvedValue([{ cursor: savedCursor }]), 64 }), 65 }), 66 } as any); 67 68 const cursor = await cursorManager.load(); 69 expect(cursor).toBe(savedCursor); 70 }); 71 72 it("should return null and log error on database failure", async () => { 73 // Mock database error 74 vi.spyOn(mockDb, "select").mockReturnValue({ 75 from: vi.fn().mockReturnValue({ 76 where: vi.fn().mockReturnValue({ 77 limit: vi.fn().mockRejectedValue(new Error("Database error")), 78 }), 79 }), 80 } as any); 81 82 const cursor = await cursorManager.load(); 83 expect(cursor).toBeNull(); 84 expect(mockLogger.error).toHaveBeenCalledWith( 85 "Failed to load cursor from database", 86 expect.objectContaining({ error: "Database error" }) 87 ); 88 }); 89 90 it("should allow custom service name", async () => { 91 const savedCursor = BigInt(9876543210000000); 92 93 // Mock cursor retrieval 94 const whereFn = vi.fn().mockReturnValue({ 95 limit: vi.fn().mockResolvedValue([{ cursor: savedCursor }]), 96 }); 97 98 vi.spyOn(mockDb, "select").mockReturnValue({ 99 from: vi.fn().mockReturnValue({ 100 where: whereFn, 101 }), 102 } as any); 103 104 const cursor = await cursorManager.load("custom-service"); 105 expect(cursor).toBe(savedCursor); 106 }); 107 }); 108 109 describe("update", () => { 110 it("should update cursor in database", async () => { 111 const mockInsert = vi.fn().mockReturnValue({ 112 values: vi.fn().mockReturnValue({ 113 onConflictDoUpdate: vi.fn().mockResolvedValue(undefined), 114 }), 115 }); 116 117 vi.spyOn(mockDb, "insert").mockImplementation(mockInsert); 118 119 await cursorManager.update(1234567890000000); 120 121 expect(mockInsert).toHaveBeenCalled(); 122 }); 123 124 it("should not throw on database failure", async () => { 125 // Mock database error 126 vi.spyOn(mockDb, "insert").mockReturnValue({ 127 values: vi.fn().mockReturnValue({ 128 onConflictDoUpdate: vi.fn().mockRejectedValue(new Error("Database error")), 129 }), 130 } as any); 131 132 // Should not throw 133 await expect(cursorManager.update(1234567890000000)).resolves.toBeUndefined(); 134 135 expect(mockLogger.error).toHaveBeenCalledWith( 136 "Failed to update cursor", 137 expect.objectContaining({ error: "Database error" }) 138 ); 139 }); 140 141 it("should allow custom service name", async () => { 142 const valuesFn = vi.fn().mockReturnValue({ 143 onConflictDoUpdate: vi.fn().mockResolvedValue(undefined), 144 }); 145 146 vi.spyOn(mockDb, "insert").mockReturnValue({ 147 values: valuesFn, 148 } as any); 149 150 await cursorManager.update(1234567890000000, "custom-service"); 151 152 // Verify values was called with custom service 153 expect(valuesFn).toHaveBeenCalledWith({ 154 service: "custom-service", 155 cursor: BigInt(1234567890000000), 156 updatedAt: expect.any(Date), 157 }); 158 }); 159 }); 160 161 describe("rewind", () => { 162 it("should rewind cursor by specified microseconds", () => { 163 const cursor = BigInt(1234567890000000); 164 const rewindAmount = 10_000_000; // 10 seconds 165 166 const rewound = cursorManager.rewind(cursor, rewindAmount); 167 168 expect(rewound).toBe(cursor - BigInt(rewindAmount)); 169 }); 170 171 it("should handle zero rewind", () => { 172 const cursor = BigInt(1234567890000000); 173 174 const rewound = cursorManager.rewind(cursor, 0); 175 176 expect(rewound).toBe(cursor); 177 }); 178 179 it("should handle large rewind amounts", () => { 180 const cursor = BigInt(1234567890000000); 181 const rewindAmount = 1_000_000_000; // 1000 seconds 182 183 const rewound = cursorManager.rewind(cursor, rewindAmount); 184 185 expect(rewound).toBe(cursor - BigInt(rewindAmount)); 186 }); 187 }); 188 189 describe("getCursorAgeHours", () => { 190 it("returns null when cursor is null", () => { 191 const age = cursorManager.getCursorAgeHours(null); 192 expect(age).toBeNull(); 193 }); 194 195 it("calculates age in hours from microsecond cursor", () => { 196 // Cursor from 24 hours ago 197 const twentyFourHoursAgoUs = BigInt( 198 (Date.now() - 24 * 60 * 60 * 1000) * 1000 199 ); 200 const age = cursorManager.getCursorAgeHours(twentyFourHoursAgoUs); 201 // Allow 1-hour tolerance for test execution time 202 expect(age).toBeGreaterThanOrEqual(23); 203 expect(age).toBeLessThanOrEqual(25); 204 }); 205 206 it("returns near-zero for recent cursor", () => { 207 const recentCursorUs = BigInt(Date.now() * 1000); 208 const age = cursorManager.getCursorAgeHours(recentCursorUs); 209 expect(age).toBeGreaterThanOrEqual(0); 210 expect(age).toBeLessThan(1); 211 }); 212 }); 213});