OverviewFilesCommitsIssuesDocsReleasesPeers

pear-git-v3 / test / repo-activity-flush-on-close.test.js

87 lines · javascript ref:
// 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')
})