Files
orca/src/main/git/status-submodule-path-cache.test.ts
T
BingZandJinjing be258e23ec Add Grok orchestration group routing (#8058)
* docs: design Grok orchestration group

* docs: plan Grok orchestration group implementation

* fix: add Grok orchestration group

* test(orchestration): accept Windows skill newlines

* Fix @grok orchestration group matching and remove stale planning docs

- Reuse the shared buildAgentNameRe matcher in groups.ts instead of a
  divergent local regex, so orchestration groups honor the same
  Windows launcher-suffix rule (grok.exe/.cmd/.bat/.ps1) as the rest
  of Orca's agent-title detection.
- Add test coverage for real Grok OSC title shapes (spinner-collapsed,
  session titles) and Windows launcher-suffix titles.
- Delete the now-completed design and implementation-plan docs for
  the Grok orchestration group work.

---------

Co-authored-by: Jinjing <6427696+AmethystLiang@users.noreply.github.com>
2026-07-11 21:43:20 -07:00

230 lines
8.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { gitExecFileAsyncMock, gitExecFileAsyncBufferMock, gitStreamStdoutMock } = vi.hoisted(
() => ({
gitExecFileAsyncMock: vi.fn(),
gitExecFileAsyncBufferMock: vi.fn(),
gitStreamStdoutMock: vi.fn()
})
)
vi.mock('./runner', () => ({
gitExecFileAsync: gitExecFileAsyncMock,
gitExecFileAsyncBuffer: gitExecFileAsyncBufferMock,
gitStreamStdout: gitStreamStdoutMock,
gitOptionalLocksDisabledEnv: (env: NodeJS.ProcessEnv = process.env) => ({
...env,
GIT_OPTIONAL_LOCKS: '0'
})
}))
import {
MAX_SUBMODULE_PATHS_CACHE_ENTRIES,
abortMerge,
abortRebase,
clearSubmodulePathsCacheForTests,
getSubmodulePathsCacheCountForTests,
listSubmodulePaths
} from './status'
import { checkoutBranch } from './checkout'
import { gitPull, gitPullRebaseFromBase } from './remote'
import { addWorktree, removeWorktree } from './worktree'
describe('submodule path cache', () => {
beforeEach(() => {
clearSubmodulePathsCacheForTests()
gitExecFileAsyncMock.mockReset()
gitExecFileAsyncBufferMock.mockReset()
gitStreamStdoutMock.mockReset()
gitExecFileAsyncMock.mockImplementation((_args: string[], options?: { cwd?: string }) =>
Promise.resolve({
stdout: `submodule.lib.path ${String(options?.cwd ?? 'repo').replace(/^.*[/\\\\]/, '')}-lib\n`
})
)
})
afterEach(() => {
vi.useRealTimers()
clearSubmodulePathsCacheForTests()
})
it('prunes expired entries even when later reads use different worktrees', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
await listSubmodulePaths('/repo-a')
await listSubmodulePaths('/repo-b')
expect(getSubmodulePathsCacheCountForTests()).toBe(2)
vi.setSystemTime(5_001)
await expect(listSubmodulePaths('/repo-c')).resolves.toEqual(['repo-c-lib'])
expect(getSubmodulePathsCacheCountForTests()).toBe(1)
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(3)
})
it('stays bounded through prolonged worktree churn and keeps recently reused worktrees', async () => {
vi.useFakeTimers()
vi.setSystemTime(0)
for (let wave = 0; wave < 4; wave += 1) {
for (let i = 0; i < MAX_SUBMODULE_PATHS_CACHE_ENTRIES; i += 1) {
await listSubmodulePaths(`/wave-${wave}-repo-${i}`)
}
expect(getSubmodulePathsCacheCountForTests()).toBe(MAX_SUBMODULE_PATHS_CACHE_ENTRIES)
vi.advanceTimersByTime(5_001)
}
await expect(listSubmodulePaths('/retained-repo')).resolves.toEqual(['retained-repo-lib'])
for (let i = 0; i < MAX_SUBMODULE_PATHS_CACHE_ENTRIES - 1; i += 1) {
await listSubmodulePaths(`/final-repo-${i}`)
}
await expect(listSubmodulePaths('/retained-repo')).resolves.toEqual(['retained-repo-lib'])
await listSubmodulePaths(`/final-repo-${MAX_SUBMODULE_PATHS_CACHE_ENTRIES - 1}`)
await listSubmodulePaths('/overflow-repo')
expect(getSubmodulePathsCacheCountForTests()).toBe(MAX_SUBMODULE_PATHS_CACHE_ENTRIES)
const callsBeforeRetainedRead = gitExecFileAsyncMock.mock.calls.length
await expect(listSubmodulePaths('/retained-repo')).resolves.toEqual(['retained-repo-lib'])
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(callsBeforeRetainedRead)
await expect(listSubmodulePaths('/final-repo-0')).resolves.toEqual(['final-repo-0-lib'])
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(callsBeforeRetainedRead + 1)
})
it('does not let a pre-invalidation read repopulate the cache', async () => {
let resolveOldRead: ((value: { stdout: string }) => void) | undefined
gitExecFileAsyncMock.mockImplementationOnce(
() =>
new Promise<{ stdout: string }>((resolve) => {
resolveOldRead = resolve
})
)
const oldRead = listSubmodulePaths('/repo')
expect(resolveOldRead).toBeTypeOf('function')
clearSubmodulePathsCacheForTests()
resolveOldRead?.({ stdout: 'submodule.lib.path old-lib\n' })
await expect(oldRead).resolves.toEqual(['old-lib'])
expect(getSubmodulePathsCacheCountForTests()).toBe(0)
await expect(listSubmodulePaths('/repo')).resolves.toEqual(['repo-lib'])
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it("does not reuse another branch's submodule paths after local or WSL checkout", async () => {
let modulePath = 'main-lib'
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args[0] === 'checkout') {
modulePath = 'feature-lib'
return Promise.resolve({ stdout: '' })
}
return Promise.resolve({ stdout: `submodule.lib.path ${modulePath}\n` })
})
const runtime = { wslDistro: 'Ubuntu' }
await expect(listSubmodulePaths('/repo', runtime)).resolves.toEqual(['main-lib'])
await checkoutBranch('/repo', 'feature', runtime)
await expect(listSubmodulePaths('/repo', runtime)).resolves.toEqual(['feature-lib'])
const configReads = gitExecFileAsyncMock.mock.calls.filter(
([args]) => args[0] === 'config' && args.includes('.gitmodules')
)
expect(configReads).toHaveLength(2)
expect(gitExecFileAsyncMock).toHaveBeenCalledWith(['checkout', 'feature', '--'], {
cwd: '/repo',
wslDistro: 'Ubuntu'
})
})
it.each([
['pull', () => gitPull('/repo')],
['rebase', () => gitPullRebaseFromBase('/repo', 'origin/main')]
])('invalidates submodule paths around local %s', async (_name, mutate) => {
let modulePath = 'old-lib'
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args[0] === 'remote') {
return Promise.resolve({ stdout: 'origin\n' })
}
if (args[0] === 'pull') {
modulePath = 'fresh-lib'
return Promise.resolve({ stdout: '' })
}
if (args[0] === 'config' && args.includes('.gitmodules')) {
return Promise.resolve({ stdout: `submodule.lib.path ${modulePath}\n` })
}
return Promise.resolve({ stdout: '' })
})
await expect(listSubmodulePaths('/repo')).resolves.toEqual(['old-lib'])
await mutate()
await expect(listSubmodulePaths('/repo')).resolves.toEqual(['fresh-lib'])
const configReads = gitExecFileAsyncMock.mock.calls.filter(
([args]) => args[0] === 'config' && args.includes('.gitmodules')
)
expect(configReads).toHaveLength(2)
})
it.each([
['merge', () => abortMerge('/repo', { wslDistro: 'Ubuntu' })],
['rebase', () => abortRebase('/repo', { wslDistro: 'Ubuntu' })]
])('drops a cached empty .gitmodules result when %s abort restores it', async (_name, abort) => {
let modulePath = ''
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args[1] === '--abort') {
modulePath = 'restored-lib'
return Promise.resolve({ stdout: '' })
}
return Promise.resolve({
stdout: modulePath ? `submodule.lib.path ${modulePath}\n` : ''
})
})
await expect(listSubmodulePaths('/repo', { wslDistro: 'Ubuntu' })).resolves.toEqual([])
await abort()
await expect(listSubmodulePaths('/repo', { wslDistro: 'Ubuntu' })).resolves.toEqual([
'restored-lib'
])
const configReads = gitExecFileAsyncMock.mock.calls.filter(
([args]) => args[0] === 'config' && args.includes('.gitmodules')
)
expect(configReads).toHaveLength(2)
})
it('drops a same-path negative cache when a local or WSL worktree is recreated', async () => {
let recreated = false
gitExecFileAsyncMock.mockImplementation((args: string[]) => {
if (args[0] === 'worktree' && args[1] === 'add') {
recreated = true
}
if (args[0] === 'config' && args.includes('.gitmodules')) {
return Promise.resolve({
stdout: recreated ? 'submodule.lib.path recreated-lib\n' : ''
})
}
return Promise.resolve({ stdout: '' })
})
const runtime = { wslDistro: 'Ubuntu' }
await expect(listSubmodulePaths('/repo-feature', runtime)).resolves.toEqual([])
await removeWorktree('/repo', '/repo-feature', true, {
...runtime,
knownRemovedWorktree: { branch: '', head: '', locked: false }
})
await addWorktree('/repo', '/repo-feature', 'feature', undefined, false, false, {
...runtime,
checkoutExistingBranch: true
})
await expect(listSubmodulePaths('/repo-feature', runtime)).resolves.toEqual(['recreated-lib'])
const configReads = gitExecFileAsyncMock.mock.calls.filter(
([args]) => args[0] === 'config' && args.includes('.gitmodules')
)
expect(configReads).toHaveLength(2)
})
})