|
1 | 1 | import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; |
| 2 | +import { existsSync } from 'node:fs'; |
2 | 3 | import { mkdtemp, readFile, rm, writeFile } from 'node:fs/promises'; |
3 | 4 | import { join } from 'node:path'; |
4 | 5 | import { tmpdir } from 'node:os'; |
@@ -234,6 +235,92 @@ describe('automatic cache revalidation', () => { |
234 | 235 | expect(cachedJson[0]?.event).toBe('build-2026'); |
235 | 236 | }); |
236 | 237 |
|
| 238 | + it('uses an existing scoped cache without checking other events', async () => { |
| 239 | + await writeCachedEvent('build-2026', {}, 'BRK202'); |
| 240 | + const originalMeta = await readMeta('build-2026'); |
| 241 | + const fetchMock = vi.fn(); |
| 242 | + vi.stubGlobal('fetch', fetchMock); |
| 243 | + |
| 244 | + const sessions = await ensureCache('build-2026'); |
| 245 | + |
| 246 | + expect(fetchMock).not.toHaveBeenCalled(); |
| 247 | + expect(sessions.map((s) => s.sessionCode)).toEqual(['BRK202']); |
| 248 | + expect(await readMeta('build-2026')).toEqual(originalMeta); |
| 249 | + expect(existsSync(join(cacheDir, 'build-2025-sessions.json'))).toBe(false); |
| 250 | + expect(existsSync(join(cacheDir, 'ignite-2025-sessions.json'))).toBe(false); |
| 251 | + }); |
| 252 | + |
| 253 | + it('fetches a different scoped event without refreshing an existing scoped cache', async () => { |
| 254 | + await writeCachedEvent('build-2026', {}, 'BRK202'); |
| 255 | + const originalBuild2026Meta = await readMeta('build-2026'); |
| 256 | + const fetchMock = vi.fn().mockResolvedValue(jsonResponse( |
| 257 | + [{ sessionCode: 'BRK101', title: 'Build 2025 session' }], |
| 258 | + { etag: '"2025"', 'last-modified': 'Thu, 07 May 2026 02:55:00 GMT' }, |
| 259 | + )); |
| 260 | + vi.stubGlobal('fetch', fetchMock); |
| 261 | + |
| 262 | + const sessions = await ensureCache('build-2025'); |
| 263 | + |
| 264 | + expect(fetchMock).toHaveBeenCalledTimes(1); |
| 265 | + expect(fetchMock.mock.calls[0]?.[0]).toBe('https://aka.ms/build2025-session-info'); |
| 266 | + expect(sessions.map((s) => s.sessionCode)).toEqual(['BRK101']); |
| 267 | + expect(await readMeta('build-2026')).toEqual(originalBuild2026Meta); |
| 268 | + expect(existsSync(join(cacheDir, 'ignite-2025-sessions.json'))).toBe(false); |
| 269 | + }); |
| 270 | + |
| 271 | + it('reuses existing caches and fetches missing caches when loading all events', async () => { |
| 272 | + await writeCachedEvent('build-2026', {}, 'BRK202'); |
| 273 | + const originalBuild2026Meta = await readMeta('build-2026'); |
| 274 | + const fetchMock = vi.fn() |
| 275 | + .mockResolvedValueOnce(jsonResponse( |
| 276 | + [{ sessionCode: 'BRK101', title: 'Build 2025 session' }], |
| 277 | + { etag: '"2025"', 'last-modified': 'Thu, 07 May 2026 02:55:00 GMT' }, |
| 278 | + )) |
| 279 | + .mockResolvedValueOnce(jsonResponse( |
| 280 | + [{ sessionCode: 'IGN301', title: 'Ignite 2025 session' }], |
| 281 | + { etag: '"ign2025"', 'last-modified': 'Thu, 07 May 2026 02:55:30 GMT' }, |
| 282 | + )); |
| 283 | + vi.stubGlobal('fetch', fetchMock); |
| 284 | + |
| 285 | + const sessions = await ensureCache(); |
| 286 | + |
| 287 | + expect(fetchMock).toHaveBeenCalledTimes(2); |
| 288 | + expect(fetchMock.mock.calls.map(([url]) => url)).toEqual([ |
| 289 | + 'https://aka.ms/build2025-session-info', |
| 290 | + 'https://aka.ms/ignite2025-session-info', |
| 291 | + ]); |
| 292 | + expect(sessions.map((s) => s.sessionCode).sort()).toEqual(['BRK101', 'BRK202', 'IGN301']); |
| 293 | + expect(await readMeta('build-2026')).toEqual(originalBuild2026Meta); |
| 294 | + }); |
| 295 | + |
| 296 | + it('fetches only the requested event when cache loading is scoped', async () => { |
| 297 | + const fetchMock = vi.fn().mockResolvedValue(jsonResponse( |
| 298 | + [{ sessionCode: 'BRK202', title: 'Build 2026 session' }], |
| 299 | + { etag: '"2026"', 'last-modified': 'Thu, 07 May 2026 02:56:00 GMT' }, |
| 300 | + )); |
| 301 | + vi.stubGlobal('fetch', fetchMock); |
| 302 | + |
| 303 | + const sessions = await ensureCache('build-2026'); |
| 304 | + |
| 305 | + expect(fetchMock).toHaveBeenCalledTimes(1); |
| 306 | + expect(fetchMock.mock.calls[0]?.[0]).toBe('https://aka.ms/build2026-session-info'); |
| 307 | + expect(sessions.map((s) => s.event)).toEqual(['build-2026']); |
| 308 | + expect(existsSync(join(cacheDir, 'build-2026-sessions.json'))).toBe(true); |
| 309 | + expect(existsSync(join(cacheDir, 'build-2025-sessions.json'))).toBe(false); |
| 310 | + expect(existsSync(join(cacheDir, 'ignite-2025-sessions.json'))).toBe(false); |
| 311 | + }); |
| 312 | + |
| 313 | + it('does not fall back to unrelated cached events when scoped cache loading fails', async () => { |
| 314 | + await writeCachedEvent('build-2025'); |
| 315 | + const fetchMock = vi.fn().mockRejectedValue(new TypeError('network down')); |
| 316 | + vi.stubGlobal('fetch', fetchMock); |
| 317 | + |
| 318 | + const sessions = await ensureCache('build-2026'); |
| 319 | + |
| 320 | + expect(fetchMock).toHaveBeenCalledTimes(1); |
| 321 | + expect(sessions).toEqual([]); |
| 322 | + }); |
| 323 | + |
237 | 324 | it('reports unchanged refreshes when the remote catalog returns 304', async () => { |
238 | 325 | await writeCachedEvent('build-2026'); |
239 | 326 | const fetchMock = vi.fn().mockResolvedValue(new Response(null, { status: 304 })); |
|
0 commit comments