// Two concurrent enrich passes over the same repo must compute it ONCE.
//
// startPearGit fires a fire-and-forget background pre-warm of the /repos
// enrich cache (lib/pear-git.js, "Pre-warm the /repos landing-page enrich
// cache"). Nothing coordinates it with the request path, so a visitor who
// arrives during boot starts a SECOND full enrich over the same repos while
// the first is still running. Neither has populated the cache yet, so both
// miss, and every repo is walked twice — up to 2x CONCURRENCY git processes
// at once, on exactly the cold boot where the box is already busy.
//
// `search-all-repos` runs the same full pass on an autocomplete keystroke, so
// a third and fourth overlapping pass are reachable from a single visitor.
//
// The fix is an in-flight map: the second caller joins the first's promise
// instead of forking its own git. getRepoActivityStats() exposes the two
// counters so this is observable rather than inferred from timing.
import test from 'brittle'
import { mkdtemp, rm, mkdir, writeFile } from '#node:fs/promises'
import { tmpdir } from '#node:os'
import { join } from '#node:path'
import process from '#node:process'
import { execFileSync } from '../lib/_compat-exec.js'
import {
enrichRepos,
getRepoActivityStats,
resetRepoActivityCacheForTests
} from '../lib/services/repo-activity.js'
function git (cwd, args) {
return execFileSync('git', args, {
cwd,
stdio: ['ignore', 'pipe', 'ignore'],
env: {
...process.env,
GIT_AUTHOR_NAME: 't',
GIT_AUTHOR_EMAIL: 't@t',
GIT_COMMITTER_NAME: 't',
GIT_COMMITTER_EMAIL: 't@t'
}
}).toString()
}
async function makeBareRepo (root, name, n) {
const barePath = join(root, name + '.git')
const workPath = join(root, name + '-work')
await mkdir(barePath, { recursive: true })
await mkdir(workPath, { recursive: true })
git(barePath, ['init', '--bare', '-q'])
git(workPath, ['init', '-q'])
for (let i = 0; i < n; i++) {
await writeFile(join(workPath, 'f'), 'v' + i)
git(workPath, ['add', 'f'])
git(workPath, ['commit', '-q', '-m', 'c' + i])
}
const branch = git(workPath, ['rev-parse', '--abbrev-ref', 'HEAD']).trim()
git(workPath, ['remote', 'add', 'origin', barePath])
git(workPath, ['push', '-q', 'origin', branch])
return barePath
}
const statusFor = (names) => ({ active: true, repos: names.map((name) => ({ name, serving: true })) })
const reposMapFor = (root, names) => {
const m = new Map()
for (const name of names) m.set(name, { path: join(root, name + '.git') })
return m
}
test('concurrent enrich passes share one computation per repo', async (t) => {
const root = await mkdtemp(join(tmpdir(), 'ra-inflight-'))
t.teardown(() => rm(root, { recursive: true, force: true }))
const names = ['alpha', 'beta', 'gamma']
for (const n of names) await makeBareRepo(root, n, 3)
// No persist path — this test is about in-flight coalescing, not disk.
resetRepoActivityCacheForTests()
t.is(getRepoActivityStats().computes, 0, 'counters start clean')
const status = statusFor(names)
const map = reposMapFor(root, names)
// Model the boot race: pre-warm and a visitor's render, started without an
// await between them so both are genuinely in flight.
const [warm, request] = await Promise.all([
enrichRepos(status, map),
enrichRepos(status, map)
])
const stats = getRepoActivityStats()
t.is(stats.computes, names.length, 'each repo computed exactly once across both passes')
t.ok(stats.deduped > 0, 'the second pass joined the first instead of forking its own git')
// Coalescing must not change what either caller sees.
t.is(warm.length, names.length, 'pre-warm pass returned every repo')
t.is(request.length, names.length, 'request pass returned every repo')
for (let i = 0; i < names.length; i++) {
t.is(warm[i].name, names[i], 'input order preserved')
t.is(warm[i].commits, 3, 'commit count correct in the computing pass')
t.is(request[i].commits, 3, 'the joined pass got the same result, not a stub')
t.alike(request[i].activity, warm[i].activity, 'both callers see identical activity buckets')
}
})
test('a later pass still hits the cache rather than recomputing', async (t) => {
const root = await mkdtemp(join(tmpdir(), 'ra-inflight2-'))
t.teardown(() => rm(root, { recursive: true, force: true }))
await makeBareRepo(root, 'solo', 2)
resetRepoActivityCacheForTests()
const status = statusFor(['solo'])
const map = reposMapFor(root, ['solo'])
await enrichRepos(status, map)
t.is(getRepoActivityStats().computes, 1, 'first pass computed')
// Sequential, after the in-flight entry has been cleared — this must be a
// plain cache hit, proving the dedup map does not leak or shadow the cache.
await enrichRepos(status, map)
t.is(getRepoActivityStats().computes, 1, 'second pass served from cache, no recompute')
})