| … | … | @@ -0,0 +1,66 @@ |
| 1 | // The /repos enrich cache must survive a daemon shutdown. |
| 2 | // |
| 3 | // repo-activity.js persists on a 1s DEBOUNCE, and the timer is `unref()`d so |
| 4 | // it cannot hold the process open. flushRepoActivityCache() exists precisely |
| 5 | // to force that pending write out — its own doc says "call on graceful |
| 6 | // shutdown" — but nothing called it. So every cache entry computed in the last |
| 7 | // second before an exit was silently dropped, and the next boot re-ran git |
| 8 | // against those repos: the "slow on first load after a restart" mechanism. |
| 9 | // |
| 10 | // pear-git-v3 restarts often (OTA applies by exiting for the supervisor to |
| 11 | // respawn), so this is the common path, not an edge case. |
| 12 | // |
| 13 | // The daemon calls configureRepoActivityCache(storage) at boot, so the module |
| 14 | // is already pointed at <storage>/repo-activity-cache.json by the time close() |
| 15 | // runs. This test enriches (populating the cache and arming the debounce), |
| 16 | // closes immediately, and asserts the file landed — with no sleep, so a |
| 17 | // regression to "wait for the debounce" fails rather than passes slowly. |
| 18 | |
| 19 | import test from 'brittle' |
| 20 | import process from '#node:process' |
| 21 | import { mkdtempSync, existsSync } from '#node:fs' |
| 22 | import { readFile } from '#node:fs/promises' |
| 23 | import { join } from '#node:path' |
| 24 | import { tmpdir } from '#node:os' |
| 25 | |
| 26 | import { startPearGit } from '../index.js' |
| 27 | import { enrichRepos, resetRepoActivityCacheForTests } from '../lib/services/repo-activity.js' |
| 28 | |
| 29 | process.env.PGV3_USE_RAW_IDENTITY = '1' |
| 30 | const tmp = (p) => mkdtempSync(join(tmpdir(), p)) |
| 31 | |
| 32 | test('close() flushes the pending repo-activity cache write', async (t) => { |
| 33 | // The cache is module-global. Drop any state a sibling test file left |
| 34 | // behind so configureRepoActivityCache() actually binds to OUR storage. |
| 35 | resetRepoActivityCacheForTests() |
| 36 | |
| 37 | const storage = tmp('pgv3-flush-') |
| 38 | const node = await startPearGit({ |
| 39 | storage, |
| 40 | uds: join(tmp('pgv3-flush-s-'), 'svc.sock'), |
| 41 | http: { port: 0 }, |
| 42 | swarm: false, |
| 43 | sync: false, |
| 44 | deploy: false, |
| 45 | autoImportSSH: false |
| 46 | }) |
| 47 | |
| 48 | const cacheFile = join(storage, 'repo-activity-cache.json') |
| 49 | t.absent(existsSync(cacheFile), 'no cache file before anything is enriched') |
| 50 | |
| 51 | // A plain directory is enough: getRepoDetails stats objects/pack (absent → |
| 52 | // mtime 0), the git calls fail and resolve empty, and the zero-commit result |
| 53 | // is still cached + marks the module dirty. That is the state we need — a |
| 54 | // pending write — without paying for a real repo. |
| 55 | const repoDir = tmp('pgv3-flush-repo-') |
| 56 | await enrichRepos({ repos: [{ name: 'r1' }] }, new Map([['r1', { path: repoDir }]])) |
| 57 | |
| 58 | // No sleep. Pre-fix the debounced write is still 1s out and unref'd, so the |
| 59 | // file does not exist; the flush on close is the only thing that writes it. |
| 60 | await node.close() |
| 61 | |
| 62 | t.ok(existsSync(cacheFile), 'cache persisted by close(), without waiting for the debounce') |
| 63 | |
| 64 | const parsed = JSON.parse(await readFile(cacheFile, 'utf8')) |
| 65 | t.ok(parsed.entries && parsed.entries.r1, 'the entry enriched just before shutdown survived') |
| 66 | }) |