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 main 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});