/ src / browser / network-cache.test.ts
network-cache.test.ts
 1  import { afterEach, beforeEach, describe, expect, it } from 'vitest';
 2  import * as fs from 'node:fs';
 3  import * as os from 'node:os';
 4  import * as path from 'node:path';
 5  import {
 6      DEFAULT_TTL_MS,
 7      findEntry,
 8      getCachePath,
 9      loadNetworkCache,
10      saveNetworkCache,
11      type CachedNetworkEntry,
12      type NetworkCacheFile,
13  } from './network-cache.js';
14  
15  function makeEntry(key: string, body: unknown = { ok: true }): CachedNetworkEntry {
16      return { key, url: `https://x.com/${key}`, method: 'GET', status: 200, size: 2, ct: 'application/json', body };
17  }
18  
19  describe('network-cache', () => {
20      let baseDir: string;
21  
22      beforeEach(() => {
23          baseDir = fs.mkdtempSync(path.join(os.tmpdir(), 'opencli-netcache-'));
24      });
25      afterEach(() => {
26          fs.rmSync(baseDir, { recursive: true, force: true });
27      });
28  
29      it('sanitizes workspace names into safe filenames', () => {
30          const p = getCachePath('browser:default', baseDir);
31          expect(path.basename(p)).toBe('browser_default.json');
32      });
33  
34      it('round-trips entries through save + load', () => {
35          saveNetworkCache('ws', [makeEntry('UserTweets'), makeEntry('UserByScreenName')], baseDir);
36          const res = loadNetworkCache('ws', { baseDir });
37          expect(res.status).toBe('ok');
38          expect(res.file?.entries).toHaveLength(2);
39          expect(res.file?.entries[0].key).toBe('UserTweets');
40      });
41  
42      it('reports missing when cache file does not exist', () => {
43          expect(loadNetworkCache('nope', { baseDir }).status).toBe('missing');
44      });
45  
46      it('reports expired when the cache is older than ttl', () => {
47          saveNetworkCache('ws', [makeEntry('A')], baseDir);
48          const future = Date.now() + DEFAULT_TTL_MS + 60_000;
49          const res = loadNetworkCache('ws', { baseDir, now: future });
50          expect(res.status).toBe('expired');
51          expect(res.file?.entries).toHaveLength(1);
52      });
53  
54      it('reports corrupt for malformed json', () => {
55          const file = getCachePath('ws', baseDir);
56          fs.mkdirSync(path.dirname(file), { recursive: true });
57          fs.writeFileSync(file, '{not json');
58          expect(loadNetworkCache('ws', { baseDir }).status).toBe('corrupt');
59      });
60  
61      it('reports corrupt for wrong schema version', () => {
62          const file = getCachePath('ws', baseDir);
63          fs.mkdirSync(path.dirname(file), { recursive: true });
64          fs.writeFileSync(file, JSON.stringify({ version: 0, entries: [] }));
65          expect(loadNetworkCache('ws', { baseDir }).status).toBe('corrupt');
66      });
67  
68      it('findEntry returns matching entry or null', () => {
69          const file: NetworkCacheFile = {
70              version: 1, workspace: 'ws', savedAt: new Date().toISOString(),
71              entries: [makeEntry('A'), makeEntry('B')],
72          };
73          expect(findEntry(file, 'B')?.key).toBe('B');
74          expect(findEntry(file, 'missing')).toBeNull();
75      });
76  });