// The /repos enrich cache must survive a daemon shutdown.
//
// repo-activity.js persists on a 1s DEBOUNCE, and the timer is `unref()`d so
// it cannot hold the process open. flushRepoActivityCache() exists precisely
// to force that pending write out — its own doc says "call on graceful
// shutdown" — but nothing called it. So every cache entry computed in the last
// second before an exit was silently dropped, and the next boot re-ran git
// against those repos: the "slow on first load after a restart" mechanism.
//
// pear-git-v3 restarts often (OTA applies by exiting for the supervisor to
// respawn), so this is the common path, not an edge case.
//
// The daemon calls configureRepoActivityCache(storage) at boot, so the module
// is already pointed at <storage>/repo-activity-cache.json by the time close()
// runs. This test enriches (populating the cache and arming the debounce),
// closes immediately, and asserts the file landed — with no sleep, so a
// regression to "wait for the debounce" fails rather than passes slowly.
import test from 'brittle'
import process from '#node:process'
import { mkdtempSync, existsSync, writeFileSync, mkdirSync } from '#node:fs'
import { readFile } from '#node:fs/promises'
import { join } from '#node:path'
import { tmpdir } from '#node:os'
import { startPearGit } from '../index.js'
import { execFile } from '../lib/_compat-exec.js'
import { enrichRepos, resetRepoActivityCacheForTests } from '../lib/services/repo-activity.js'
process.env.PGV3_USE_RAW_IDENTITY = '1'
const tmp = (p) => mkdtempSync(join(tmpdir(), p))
test('close() flushes the pending repo-activity cache write', async (t) => {
// The cache is module-global. Drop any state a sibling test file left
// behind so configureRepoActivityCache() actually binds to OUR storage.
resetRepoActivityCacheForTests()
const storage = tmp('pgv3-flush-')
const node = await startPearGit({
storage,
uds: join(tmp('pgv3-flush-s-'), 'svc.sock'),
http: { port: 0 },
swarm: false,
sync: false,
deploy: false,
autoImportSSH: false
})
const cacheFile = join(storage, 'repo-activity-cache.json')
t.absent(existsSync(cacheFile), 'no cache file before anything is enriched')
// A REAL bare repo with a commit, because we need a cached entry and only a
// SUCCESSFUL walk is cached now.
//
// This used to pass a plain directory and rely on the git calls failing:
// execGit resolved '' on any non-zero exit, that parsed to commits:0, and
// the zero was cached — which marked the module dirty for free. That was a
// bug (a transient git failure became a permanent "0 commits" keyed on the
// pack mtime, as seen on stortron's dc-terrain), so a failed walk is no
// longer cached and this fixture no longer produces a pending write.
//
// The assertions below are unchanged — the point of this test is that
// close() flushes without waiting for the debounce, not how the entry got
// there.
const repoDir = join(tmp('pgv3-flush-repo-'), 'r1.git')
await execFile('git', ['init', '--bare', '-q', repoDir])
const workDir = tmp('pgv3-flush-work-')
await execFile('git', ['init', '-q', workDir])
await execFile('git', ['-C', workDir, 'config', 'user.email', 't@t'])
await execFile('git', ['-C', workDir, 'config', 'user.name', 't'])
writeFileSync(join(workDir, 'f.txt'), 'hello')
await execFile('git', ['-C', workDir, 'add', 'f.txt'])
await execFile('git', ['-C', workDir, 'commit', '-qm', 'one'])
await execFile('git', ['-C', workDir, 'push', '-q', repoDir, 'HEAD:refs/heads/master'])
await enrichRepos({ repos: [{ name: 'r1' }] }, new Map([['r1', { path: repoDir }]]))
// No sleep. Pre-fix the debounced write is still 1s out and unref'd, so the
// file does not exist; the flush on close is the only thing that writes it.
await node.close()
t.ok(existsSync(cacheFile), 'cache persisted by close(), without waiting for the debounce')
const parsed = JSON.parse(await readFile(cacheFile, 'utf8'))
t.ok(parsed.entries && parsed.entries.r1, 'the entry enriched just before shutdown survived')
})