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