LCOV - code coverage report
Current view: top level - dashql-app/src/storage - test_fs_mock.ts Coverage Total Hit
Test: DashQL 915828b Lines: 92.8 % 152 141
Test Date: 2026-11-06 19:30:32

            Line data    Source code
       1              : /// Shared in-memory filesystem backing the Electron filesystem mock used by the storage tests.
       2              : ///
       3              : /// `native_storage_backend.test.ts` and `composite_storage_backend.test.ts` both exercise the *real*
       4              : /// `NativeStorageBackend` against a mocked Electron bridge. Keeping the mock implementation here
       5              : /// ensures both tests model the filesystem consistently. Each test file receives an isolated module
       6              : /// instance, and resets its store in `beforeEach`.
       7              : export interface FsStore {
       8              :     files: Map<string, string>;
       9              :     /// Binary files written via `writeFile` / read via `readFile` (e.g. the `.arrow` cache entries),
      10              :     /// kept separate from the text `files` map so each mock method touches only its own kind.
      11              :     binFiles: Map<string, Uint8Array>;
      12              :     dirs: Set<string>;
      13              :     /// Per-path last-modified time in epoch ms, stamped on every write. Backs the `stat().mtime` the
      14              :     /// cache eviction policy sorts on. Uses a monotonic counter (not wall-clock) so write order is
      15              :     /// deterministic across test runs.
      16              :     mtimes: Map<string, number>;
      17              :     /// Monotonic clock feeding `mtimes`; bumped on each write.
      18              :     clock: number;
      19              : }
      20              : 
      21              : /// Store shared by the test and its dynamic `vi.mock` factory within one isolated test context.
      22            1 : export const fsStore: FsStore = {
      23            1 :     files: new Map<string, string>(),
      24            1 :     binFiles: new Map<string, Uint8Array>(),
      25            1 :     dirs: new Set<string>(),
      26            1 :     mtimes: new Map<string, number>(),
      27            1 :     clock: 0,
      28            1 : };
      29              : 
      30              : /// Reset the shared store. Call from `beforeEach`.
      31            1 : export function resetFsStore(): void {
      32            1 :     fsStore.files.clear();
      33            1 :     fsStore.binFiles.clear();
      34            1 :     fsStore.dirs.clear();
      35            1 :     fsStore.mtimes.clear();
      36            1 :     fsStore.clock = 0;
      37            1 : }
      38              : 
      39              : /// Build the Electron filesystem mock object over the shared store.
      40            1 : export function makeFsMock() {
      41            1 :     const { files, binFiles, dirs, mtimes } = fsStore;
      42            1 :     const nextMtime = () => ++fsStore.clock;
      43            1 :     const parentOf = (p: string) => {
      44            1 :         const i = p.lastIndexOf('/');
      45            1 :         return i < 0 ? '' : p.substring(0, i);
      46            1 :     };
      47            1 :     const nameOf = (p: string) => {
      48            1 :         const i = p.lastIndexOf('/');
      49            1 :         return i < 0 ? p : p.substring(i + 1);
      50            1 :     };
      51            1 :     const isAncestorDir = (p: string) => {
      52            1 :         for (const f of files.keys()) if (f.startsWith(p + '/')) return true;
      53            1 :         for (const f of binFiles.keys()) if (f.startsWith(p + '/')) return true;
      54            1 :         for (const d of dirs) if (d.startsWith(p + '/')) return true;
      55            1 :         return false;
      56            1 :     };
      57            1 :     const stampOf = (p: string) => files.has(p) || binFiles.has(p)
      58            1 :         ? { size: binFiles.get(p)?.byteLength ?? files.get(p)!.length, mtimeMs: mtimes.get(p) ?? 0 }
      59            1 :         : null;
      60            1 :     return {
      61            1 :         exists: async (p: string) => files.has(p) || binFiles.has(p) || dirs.has(p) || isAncestorDir(p),
      62            1 :         fileStamp: async (p: string) => stampOf(p),
      63            1 :         listFiles: async (p: string) => [...files.keys(), ...binFiles.keys()]
      64            1 :             .filter(f => parentOf(f) === p)
      65            1 :             .map(f => ({ name: nameOf(f), ...stampOf(f)! })),
      66            1 :         mkdir: async (p: string) => {
      67              :             // Register the dir and all ancestors, preserving any leading slash (absolute paths)
      68              :             // the same way writeTextFile's parent walk does.
      69            1 :             dirs.add(p);
      70            1 :             let parent = parentOf(p);
      71            1 :             while (parent) {
      72            1 :                 dirs.add(parent);
      73            1 :                 parent = parentOf(parent);
      74            1 :             }
      75            1 :         },
      76            1 :         readDir: async (p: string) => {
      77            1 :             const children = new Map<string, { isFile: boolean; isDirectory: boolean }>();
      78            1 :             for (const f of files.keys()) {
      79            1 :                 if (parentOf(f) === p) children.set(nameOf(f), { isFile: true, isDirectory: false });
      80              :             }
      81            1 :             for (const f of binFiles.keys()) {
      82            1 :                 if (parentOf(f) === p) children.set(nameOf(f), { isFile: true, isDirectory: false });
      83              :             }
      84            1 :             for (const d of dirs) {
      85            1 :                 if (parentOf(d) === p) children.set(nameOf(d), { isFile: false, isDirectory: true });
      86              :             }
      87            1 :             return [...children.entries()].map(([name, kind]) => ({
      88            1 :                 name,
      89            1 :                 isFile: kind.isFile,
      90            1 :                 isDirectory: kind.isDirectory,
      91            1 :                 isSymlink: false,
      92            1 :             }));
      93            1 :         },
      94            1 :         readTextFile: async (p: string) => {
      95            1 :             if (!files.has(p)) {
      96            0 :                 throw new Error(`File not found: ${p}`);
      97              :             }
      98            1 :             return files.get(p)!;
      99            1 :         },
     100            1 :         writeTextFile: async (p: string, data: string) => {
     101            1 :             files.set(p, data);
     102            1 :             mtimes.set(p, nextMtime());
     103            1 :             let parent = parentOf(p);
     104            1 :             while (parent) {
     105            1 :                 dirs.add(parent);
     106            1 :                 parent = parentOf(parent);
     107            1 :             }
     108            1 :             return stampOf(p)!;
     109            1 :         },
     110            1 :         readFile: async (p: string) => {
     111            1 :             if (!binFiles.has(p)) {
     112            0 :                 throw new Error(`File not found: ${p}`);
     113              :             }
     114            1 :             return binFiles.get(p)!;
     115            1 :         },
     116            1 :         writeFile: async (p: string, data: Uint8Array) => {
     117              :             // Copy so a later mutation of the caller's buffer can't retroactively change stored bytes.
     118            1 :             binFiles.set(p, new Uint8Array(data));
     119            1 :             mtimes.set(p, nextMtime());
     120            1 :             let parent = parentOf(p);
     121            1 :             while (parent) {
     122            1 :                 dirs.add(parent);
     123            1 :                 parent = parentOf(parent);
     124            1 :             }
     125            1 :         },
     126            1 :         stat: async (p: string) => {
     127            1 :             const size = binFiles.has(p) ? binFiles.get(p)!.byteLength
     128            1 :                 : files.has(p) ? files.get(p)!.length
     129            0 :                     : 0;
     130            1 :             if (!binFiles.has(p) && !files.has(p)) {
     131            0 :                 throw new Error(`File not found: ${p}`);
     132              :             }
     133            1 :             return {
     134            1 :                 size,
     135            1 :                 mtime: new Date(mtimes.get(p) ?? 0),
     136            1 :                 isFile: true,
     137            1 :                 isDirectory: false,
     138            1 :                 isSymlink: false,
     139            1 :             };
     140            1 :         },
     141            1 :         remove: async (p: string, opts?: { recursive?: boolean }) => {
     142            1 :             files.delete(p);
     143            1 :             binFiles.delete(p);
     144            1 :             mtimes.delete(p);
     145            1 :             dirs.delete(p);
     146            1 :             if (opts?.recursive) {
     147            0 :                 for (const f of [...files.keys()]) if (f.startsWith(p + '/')) { files.delete(f); mtimes.delete(f); }
     148            0 :                 for (const f of [...binFiles.keys()]) if (f.startsWith(p + '/')) { binFiles.delete(f); mtimes.delete(f); }
     149            0 :                 for (const d of [...dirs]) if (d.startsWith(p + '/')) dirs.delete(d);
     150            0 :             }
     151            1 :         },
     152            1 :         rename: async (from: string, to: string) => {
     153              :             // Move a file or a whole directory subtree, re-keying every path prefixed by `from`.
     154              :             // Mirrors the host filesystem's atomic rename.
     155            1 :             const reKey = (k: string) => (k === from ? to : k.startsWith(from + '/') ? to + k.substring(from.length) : null);
     156            1 :             for (const [k, v] of [...files.entries()]) {
     157            1 :                 const nk = reKey(k);
     158            1 :                 if (nk !== null) {
     159            1 :                     files.delete(k);
     160            1 :                     files.set(nk, v);
     161            1 :                     const mt = mtimes.get(k);
     162            1 :                     if (mt !== undefined) { mtimes.delete(k); mtimes.set(nk, mt); }
     163            1 :                 }
     164            1 :             }
     165            1 :             for (const [k, v] of [...binFiles.entries()]) {
     166            1 :                 const nk = reKey(k);
     167            1 :                 if (nk !== null) {
     168            1 :                     binFiles.delete(k);
     169            1 :                     binFiles.set(nk, v);
     170            1 :                     const mt = mtimes.get(k);
     171            1 :                     if (mt !== undefined) { mtimes.delete(k); mtimes.set(nk, mt); }
     172            1 :                 }
     173            1 :             }
     174            1 :             for (const d of [...dirs]) {
     175            1 :                 const nd = reKey(d);
     176            1 :                 if (nd !== null) {
     177            0 :                     dirs.delete(d);
     178            0 :                     dirs.add(nd);
     179            0 :                 }
     180            1 :             }
     181            1 :             let parent = parentOf(to);
     182            1 :             while (parent) {
     183            1 :                 dirs.add(parent);
     184            1 :                 parent = parentOf(parent);
     185            1 :             }
     186            1 :         },
     187            1 :     };
     188            1 : }
     189              : 
     190              : /// Build the path mock object. Tests use "/" separators for simplicity.
     191              : export function makePathMock() {
     192              :     return {
     193              :         join: async (...parts: string[]) => parts.filter(p => p.length > 0).join('/'),
     194              :     };
     195              : }
        

Generated by: LCOV version 2.0-1