Files
orca/src/relay/git-status-upstream-negative-cache.test.ts
T

364 lines
14 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import {
clearNoEffectiveUpstreamStatusCache,
clearNoEffectiveUpstreamStatusCacheEntry,
getNoEffectiveUpstreamStatusCacheCountForTests,
getNoEffectiveUpstreamStatusGenerationCountForTests,
readOrProbeNoEffectiveUpstreamStatus
} from './git-status-upstream-negative-cache'
function isConfigListSnapshotCommand(args: string[]): boolean {
return args[0] === 'config' && args[1] === '--list' && args[2] === '-z'
}
function emptyGitConfigSnapshot(): { stdout: string } {
return { stdout: 'core.repositoryformatversion\n0\0' }
}
describe('relay upstream negative cache', () => {
beforeEach(() => {
clearNoEffectiveUpstreamStatusCache()
})
afterEach(() => {
clearNoEffectiveUpstreamStatusCache()
})
it('bypasses cached no-effective-upstream status when requested', async () => {
let originBranchExists = false
const runGit = vi.fn(async (args: string[]): Promise<{ stdout: string }> => {
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature\n' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error('fatal: no upstream configured for branch feature')
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')) {
if (originBranchExists) {
return { stdout: 'abc123\n' }
}
throw new Error('missing remote branch')
}
if (args[0] === 'rev-list' && args.includes('HEAD...origin/feature')) {
return { stdout: '0\t1\n' }
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
const identity = { worktreePath: '/repo', branchName: 'feature' }
const first = await readOrProbeNoEffectiveUpstreamStatus(identity, runGit)
originBranchExists = true
const automatic = await readOrProbeNoEffectiveUpstreamStatus(identity, runGit)
const strict = await readOrProbeNoEffectiveUpstreamStatus(identity, runGit, {
bypassCache: true
})
expect(first).toEqual({ hasUpstream: false, ahead: 0, behind: 0 })
expect(automatic).toEqual(first)
expect(strict).toEqual({
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 0,
behind: 1
})
})
it('keeps an older automatic negative probe from overwriting a strict positive result', async () => {
let originBranchExists = false
let deferredOriginReject: ((error: Error) => void) | null = null
const runGit = vi.fn(async (args: string[]) => {
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature\n' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error('fatal: no upstream configured for branch feature')
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')) {
if (originBranchExists) {
return { stdout: 'abc123\n' }
}
return await new Promise<{ stdout: string }>((_, reject) => {
deferredOriginReject = reject
})
}
if (args[0] === 'rev-list' && args.includes('HEAD...origin/feature')) {
return { stdout: '0\t1\n' }
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
const identity = { worktreePath: '/repo', branchName: 'feature' }
const automatic = readOrProbeNoEffectiveUpstreamStatus(identity, runGit)
await vi.waitFor(() => expect(deferredOriginReject).toBeTruthy())
originBranchExists = true
const strict = await readOrProbeNoEffectiveUpstreamStatus(identity, runGit, {
bypassCache: true
})
if (!deferredOriginReject) {
throw new Error('expected deferred origin reject')
}
;(deferredOriginReject as (error: Error) => void)(new Error('missing remote branch'))
const staleAutomatic = await automatic
const nextAutomatic = await readOrProbeNoEffectiveUpstreamStatus(identity, runGit)
expect(strict).toEqual({
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 0,
behind: 1
})
expect(staleAutomatic).toEqual({ hasUpstream: false, ahead: 0, behind: 0 })
expect(nextAutomatic).toEqual(strict)
})
it('does not trim generation for an unresolved automatic probe', async () => {
let originBranchExists = false
let deferredOriginReject: ((error: Error) => void) | null = null
const runGit = vi.fn(async (args: string[]): Promise<{ stdout: string }> => {
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature\n' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error('fatal: no upstream configured for branch feature')
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')) {
if (originBranchExists) {
return { stdout: 'abc123\n' }
}
return await new Promise<{ stdout: string }>((_, reject) => {
deferredOriginReject = reject
})
}
if (args[0] === 'rev-list' && args.includes('HEAD...origin/feature')) {
return { stdout: '0\t1\n' }
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
const identity = { worktreePath: '/repo', branchName: 'feature' }
const automatic = readOrProbeNoEffectiveUpstreamStatus(identity, runGit)
await vi.waitFor(() => expect(deferredOriginReject).toBeTruthy())
originBranchExists = true
const strict = await readOrProbeNoEffectiveUpstreamStatus(identity, runGit, {
bypassCache: true
})
for (let index = 0; index < 512; index += 1) {
const branchName = `other-${index}`
await readOrProbeNoEffectiveUpstreamStatus(
{ worktreePath: '/repo', branchName },
async (args) => {
if (args[0] === 'symbolic-ref') {
return { stdout: `${branchName}\n` }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error(`fatal: no upstream configured for branch ${branchName}`)
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes(`refs/remotes/origin/${branchName}`)) {
return { stdout: 'abc123\n' }
}
if (args[0] === 'rev-list' && args.includes(`HEAD...origin/${branchName}`)) {
return { stdout: '0\t1\n' }
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
},
{ bypassCache: true }
)
}
if (!deferredOriginReject) {
throw new Error('expected deferred origin reject')
}
;(deferredOriginReject as (error: Error) => void)(new Error('missing remote branch'))
await automatic
const nextAutomatic = await readOrProbeNoEffectiveUpstreamStatus(identity, runGit)
expect(strict).toEqual({
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 0,
behind: 1
})
expect(nextAutomatic).toEqual(strict)
expect(getNoEffectiveUpstreamStatusGenerationCountForTests()).toBeLessThanOrEqual(512)
})
it('does not trim generation for a cleared automatic probe before it settles', async () => {
let originBranchExists = false
let deferredOriginReject: ((error: Error) => void) | null = null
const runGit = vi.fn(async (args: string[]): Promise<{ stdout: string }> => {
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature\n' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error('fatal: no upstream configured for branch feature')
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')) {
if (originBranchExists) {
return { stdout: 'abc123\n' }
}
return await new Promise<{ stdout: string }>((_, reject) => {
deferredOriginReject = reject
})
}
if (args[0] === 'rev-list' && args.includes('HEAD...origin/feature')) {
return { stdout: '0\t1\n' }
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
const identity = { worktreePath: '/repo', branchName: 'feature' }
const automatic = readOrProbeNoEffectiveUpstreamStatus(identity, runGit)
await vi.waitFor(() => expect(deferredOriginReject).toBeTruthy())
originBranchExists = true
clearNoEffectiveUpstreamStatusCacheEntry(identity)
for (let index = 0; index < 512; index += 1) {
const branchName = `other-${index}`
await readOrProbeNoEffectiveUpstreamStatus(
{ worktreePath: '/repo', branchName },
async (args) => {
if (args[0] === 'symbolic-ref') {
return { stdout: `${branchName}\n` }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error(`fatal: no upstream configured for branch ${branchName}`)
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes(`refs/remotes/origin/${branchName}`)) {
return { stdout: 'abc123\n' }
}
if (args[0] === 'rev-list' && args.includes(`HEAD...origin/${branchName}`)) {
return { stdout: '0\t1\n' }
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
},
{ bypassCache: true }
)
}
if (!deferredOriginReject) {
throw new Error('expected deferred origin reject')
}
;(deferredOriginReject as (error: Error) => void)(new Error('missing remote branch'))
await automatic
const nextAutomatic = await readOrProbeNoEffectiveUpstreamStatus(identity, runGit)
expect(nextAutomatic).toEqual({
hasUpstream: true,
upstreamName: 'origin/feature',
ahead: 0,
behind: 1
})
expect(getNoEffectiveUpstreamStatusGenerationCountForTests()).toBeLessThanOrEqual(512)
})
it('bounds no-effective-upstream entries', async () => {
for (let index = 0; index < 513; index += 1) {
const branchName = `feature-${index}`
await readOrProbeNoEffectiveUpstreamStatus(
{ worktreePath: '/repo', branchName },
async (args) => {
if (args[0] === 'symbolic-ref') {
return { stdout: `${branchName}\n` }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error(`fatal: no upstream configured for branch ${branchName}`)
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes(`refs/remotes/origin/${branchName}`)) {
throw new Error('missing remote branch')
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
}
)
}
expect(getNoEffectiveUpstreamStatusCacheCountForTests()).toBe(512)
expect(getNoEffectiveUpstreamStatusGenerationCountForTests()).toBeLessThanOrEqual(512)
})
it('bounds write-generation entries from positive strict probes', async () => {
for (let index = 0; index < 513; index += 1) {
const branchName = `feature-${index}`
await readOrProbeNoEffectiveUpstreamStatus(
{ worktreePath: '/repo', branchName },
async (args) => {
if (args[0] === 'symbolic-ref') {
return { stdout: `${branchName}\n` }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error(`fatal: no upstream configured for branch ${branchName}`)
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes(`refs/remotes/origin/${branchName}`)) {
return { stdout: 'abc123\n' }
}
if (args[0] === 'rev-list' && args.includes(`HEAD...origin/${branchName}`)) {
return { stdout: '0\t1\n' }
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
},
{ bypassCache: true }
)
}
expect(getNoEffectiveUpstreamStatusCacheCountForTests()).toBe(0)
expect(getNoEffectiveUpstreamStatusGenerationCountForTests()).toBe(512)
})
it('coalesces no-upstream config reads into one snapshot subprocess', async () => {
const runGit = vi.fn(async (args: string[]): Promise<{ stdout: string }> => {
if (args[0] === 'symbolic-ref') {
return { stdout: 'feature\n' }
}
if (args[0] === 'rev-parse' && args.includes('HEAD@{u}')) {
throw new Error('fatal: no upstream configured for branch feature')
}
if (isConfigListSnapshotCommand(args)) {
return emptyGitConfigSnapshot()
}
if (args[0] === 'rev-parse' && args.includes('refs/remotes/origin/feature')) {
throw new Error('missing remote branch')
}
throw new Error(`No upstream fixture for git ${args.join(' ')}`)
})
const status = await readOrProbeNoEffectiveUpstreamStatus(
{ worktreePath: '/repo', branchName: 'feature' },
runGit
)
const configListCalls = runGit.mock.calls.filter((call) =>
isConfigListSnapshotCommand(call[0] as string[])
)
const configGetCalls = runGit.mock.calls.filter((call) => {
const args = call[0] as string[]
return args[0] === 'config' && args[1] === '--get'
})
expect(configListCalls).toHaveLength(1)
expect(configGetCalls).toHaveLength(0)
expect(status.hasUpstream).toBe(false)
})
})