mirror of
https://github.com/stablyai/orca.git
synced 2026-09-30 00:03:15 +00:00
* perf(git): bound git subprocess execution with an atomic admission scheduler Field traces (#16038, #11363) show Windows freeze storms driven by unbounded concurrent git children (12+ at once, 50-65s status convoys for 25+ minutes). Admit every main-process git child against atomic per-budget base+headroom counters (general / network / per-route), with reserved interactive capacity, ordering-only aging, close-bound permit release, a 120s fail-safe read timeout that feeds scheduler backoff, tier plumbing through every option carrier, and coalesced+jittered visibility pollers. Killswitch: ORCA_GIT_ADMISSION_DISABLED=1. Storm harness A/B: max concurrent children 65 -> 6, interactive p95 791ms -> 88ms; output-parity battery byte-identical with admission on vs off. * test(git): run the admission output-parity battery on every platform Parity needs real git, not the storm harness's PATH stub, so it must not share that file's POSIX gate - Windows is the platform where parity evidence matters. * fix(git): preserve interactive admission invariants * perf(git): keep admission queue drains linear * fix(git): close final admission gaps * perf(git): bound eligible route selection * fix(merge): remove unrelated stale snapshot changes * fix(git): preserve refresh lifecycle authority * test(git): align admission lifetime contracts * fix(git): harden admission across runtime paths * fix(git): restore freshness for bulk status reads * test(git): repoint delete-dialog source pins after admission plumbing The hydration effect now orders its targets through orderDeleteWorktreeStatusHydrationTargets and passes includeLineStats alongside the abort signal, so both literal anchors stopped matching. The invariants are unchanged and still pinned: dropping the signal, the main-worktree/folder filter, or getState-instead-of-subscribe each still reddens this test. * Fix git admission tier propagation and lock ordering Decode optional Git status tiers permissively and default runtime RPC status reads to the status lane while preserving renderer caller intent. Acquire the FETCH_HEAD mutex before atomic admission so same-repository fetch waiters hold no global or route permits. Preserve automatic pull-request refresh reasons, keep explicit hosted-review refreshes interactive, remove the dead candidate tier, and keep relay scheduling unchanged. Use tier-aware status lease keys because a shared lease cannot be safely promoted after its admission request is queued or granted. * test: align expectations with admission plumbing * refactor(child-process): move the process contract types to process-spec run-process.ts crossed its line cap after gaining the termination observer; the public types and defaults move out with re-exports so no caller changes. * chore: restore pnpm-lock.yaml to main (unintended local drift) --------- Co-authored-by: Merge Sim <sim@local>
404 lines
14 KiB
TypeScript
404 lines
14 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
|
import type { GitHubPRRefreshCandidate } from '../../shared/github/pull-request-refresh-types'
|
|
|
|
const { coordinatorMocks, moduleMocks } = await vi.hoisted(async () => {
|
|
const moduleMocks = await import('./pr-refresh-coordinator-test-mocks')
|
|
return { coordinatorMocks: moduleMocks.createPRRefreshCoordinatorMocks(), moduleMocks }
|
|
})
|
|
|
|
vi.mock('electron', () => moduleMocks.electronModuleMock(coordinatorMocks))
|
|
vi.mock('./client', () => moduleMocks.clientModuleMock(coordinatorMocks))
|
|
vi.mock('./github-api-repository', () =>
|
|
moduleMocks.githubApiRepositoryModuleMock(coordinatorMocks)
|
|
)
|
|
vi.mock('./rate-limit', () => moduleMocks.rateLimitModuleMock(coordinatorMocks))
|
|
vi.mock('../ipc/ui', () => moduleMocks.ipcUiModuleMock(coordinatorMocks))
|
|
|
|
import { makeCandidate, makePR } from './pr-refresh-coordinator-test-harness'
|
|
|
|
const { sendMock, getPRForBranchOutcomeMock } = coordinatorMocks
|
|
|
|
describe('pr-refresh-coordinator', () => {
|
|
beforeEach(() => {
|
|
moduleMocks.resetPRRefreshCoordinatorMocks(coordinatorMocks)
|
|
})
|
|
|
|
afterEach(() => {
|
|
vi.useRealTimers()
|
|
})
|
|
|
|
it('cancels queued work when a later enqueue marks the candidate invalid', async () => {
|
|
const { enqueuePRRefresh, reportVisiblePRRefreshCandidates } =
|
|
await import('./pr-refresh-coordinator')
|
|
getPRForBranchOutcomeMock.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ checksStatus: 'success' }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
|
|
const candidate = makeCandidate()
|
|
reportVisiblePRRefreshCandidates([candidate], 1, 1)
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
|
|
enqueuePRRefresh(
|
|
{ ...candidate, isArchived: true, cachedFetchedAt: Date.now() },
|
|
'active',
|
|
80,
|
|
1
|
|
)
|
|
await vi.advanceTimersByTimeAsync(10 * 60_000)
|
|
|
|
const skippedEvents = sendMock.mock.calls
|
|
.map(([, event]) => event)
|
|
.filter((event) => event.status === 'skipped')
|
|
|
|
expect(skippedEvents.at(-1)?.skippedReason).toBe('archived')
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('does not cancel other aliases when one coalesced PR alias becomes invalid', async () => {
|
|
const { enqueuePRRefresh, reportVisiblePRRefreshCandidates } =
|
|
await import('./pr-refresh-coordinator')
|
|
getPRForBranchOutcomeMock
|
|
.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ checksStatus: 'success' }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ checksStatus: 'success', state: 'merged' }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
|
|
const first = makeCandidate({
|
|
cacheKey: '/repo::feature/a',
|
|
branch: 'feature/a',
|
|
linkedPRNumber: 12,
|
|
worktreeId: 'wt-a'
|
|
})
|
|
const second = makeCandidate({
|
|
cacheKey: '/repo::feature/b',
|
|
branch: 'feature/b',
|
|
linkedPRNumber: 12,
|
|
worktreeId: 'wt-b'
|
|
})
|
|
reportVisiblePRRefreshCandidates([first, second], 1, 1)
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
|
|
enqueuePRRefresh({ ...first, isArchived: true, cachedFetchedAt: Date.now() }, 'active', 80, 1)
|
|
enqueuePRRefresh({ ...second, cachedFetchedAt: Date.now() }, 'active', 80, 1)
|
|
await vi.advanceTimersByTimeAsync(0)
|
|
|
|
const outcomeEvents = sendMock.mock.calls
|
|
.map(([, event]) => event)
|
|
.filter((event) => event.outcome)
|
|
|
|
expect(outcomeEvents.at(-1)?.aliases.map((alias) => alias.cacheKey)).toEqual([
|
|
'/repo::feature/b'
|
|
])
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('probes with the survivor head after the representative alias is invalidated', async () => {
|
|
const { enqueuePRRefresh } = await import('./pr-refresh-coordinator')
|
|
getPRForBranchOutcomeMock.mockResolvedValue({
|
|
kind: 'found',
|
|
pr: makePR({ state: 'merged' }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
|
|
const survivor = makeCandidate({
|
|
cacheKey: '/repo::feature/b',
|
|
branch: 'feature/b',
|
|
linkedPRNumber: 12,
|
|
worktreeId: 'wt-b',
|
|
currentHeadOid: 'head-b'
|
|
})
|
|
const representative = makeCandidate({
|
|
cacheKey: '/repo::feature/a',
|
|
branch: 'feature/a',
|
|
linkedPRNumber: 12,
|
|
worktreeId: 'wt-a',
|
|
currentHeadOid: 'head-a'
|
|
})
|
|
|
|
// Enqueue the survivor first, then the representative (active coalescing
|
|
// promotes the latter to representative), then invalidate the representative
|
|
// so the still-queued entry rebinds to the survivor before draining.
|
|
enqueuePRRefresh(survivor, 'active', 80, 1)
|
|
enqueuePRRefresh(representative, 'active', 80, 1)
|
|
enqueuePRRefresh({ ...representative, isArchived: true }, 'active', 80, 1)
|
|
await vi.runOnlyPendingTimersAsync()
|
|
|
|
const probedHeads = getPRForBranchOutcomeMock.mock.calls.map((call) => call[5]?.currentHeadOid)
|
|
expect(probedHeads).toContain('head-b')
|
|
expect(probedHeads).not.toContain('head-a')
|
|
})
|
|
|
|
it('probes with the survivor head after the representative worktree is pruned', async () => {
|
|
const { enqueuePRRefresh, pruneWorktreePRRefreshAliases } =
|
|
await import('./pr-refresh-coordinator')
|
|
getPRForBranchOutcomeMock.mockResolvedValue({
|
|
kind: 'found',
|
|
pr: makePR({ state: 'merged' }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
|
|
const survivor = makeCandidate({
|
|
cacheKey: '/repo::feature/b',
|
|
branch: 'feature/b',
|
|
linkedPRNumber: 12,
|
|
worktreeId: 'wt-b',
|
|
currentHeadOid: 'head-b'
|
|
})
|
|
const representative = makeCandidate({
|
|
cacheKey: '/repo::feature/a',
|
|
branch: 'feature/a',
|
|
linkedPRNumber: 12,
|
|
worktreeId: 'wt-a',
|
|
currentHeadOid: 'head-a'
|
|
})
|
|
|
|
enqueuePRRefresh(survivor, 'active', 80, 1)
|
|
enqueuePRRefresh(representative, 'active', 80, 1)
|
|
pruneWorktreePRRefreshAliases('wt-a')
|
|
await vi.runOnlyPendingTimersAsync()
|
|
|
|
const probedHeads = getPRForBranchOutcomeMock.mock.calls.map((call) => call[5]?.currentHeadOid)
|
|
expect(probedHeads).toContain('head-b')
|
|
expect(probedHeads).not.toContain('head-a')
|
|
})
|
|
|
|
it('refreshes the representative head when the same worktree re-reports a moved head', async () => {
|
|
const { reportVisiblePRRefreshCandidates } = await import('./pr-refresh-coordinator')
|
|
getPRForBranchOutcomeMock.mockResolvedValue({
|
|
kind: 'found',
|
|
pr: makePR({ state: 'merged' }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
|
|
// Same worktree/branch, moved head, coalescing visible→visible (no promote):
|
|
// the representative head must track the newest report before the drain.
|
|
reportVisiblePRRefreshCandidates(
|
|
[makeCandidate({ linkedPRNumber: 12, worktreeId: 'wt-a', currentHeadOid: 'head-a' })],
|
|
1,
|
|
1
|
|
)
|
|
reportVisiblePRRefreshCandidates(
|
|
[makeCandidate({ linkedPRNumber: 12, worktreeId: 'wt-a', currentHeadOid: 'head-b' })],
|
|
2,
|
|
1
|
|
)
|
|
await vi.runOnlyPendingTimersAsync()
|
|
|
|
const probedHeads = getPRForBranchOutcomeMock.mock.calls.map((call) => call[5]?.currentHeadOid)
|
|
expect(probedHeads).toContain('head-b')
|
|
expect(probedHeads).not.toContain('head-a')
|
|
})
|
|
|
|
it('does not coalesce local and SSH refreshes for the same branch', async () => {
|
|
const { enqueuePRRefresh } = await import('./pr-refresh-coordinator')
|
|
getPRForBranchOutcomeMock
|
|
.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ number: 12 }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ number: 44 }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
|
|
enqueuePRRefresh(makeCandidate({ cacheKey: 'local::repo-1::feature/test' }), 'active', 80, 1)
|
|
enqueuePRRefresh(
|
|
makeCandidate({
|
|
cacheKey: 'ssh:ssh-1::repo-1::feature/test',
|
|
connectionId: 'ssh-1'
|
|
}),
|
|
'active',
|
|
80,
|
|
1
|
|
)
|
|
await vi.runOnlyPendingTimersAsync()
|
|
await vi.runOnlyPendingTimersAsync()
|
|
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(2)
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenNthCalledWith(
|
|
1,
|
|
'/repo',
|
|
'feature/test',
|
|
null,
|
|
null,
|
|
null,
|
|
{ localGitExecOptions: { admissionTier: 'background' } }
|
|
)
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenNthCalledWith(
|
|
2,
|
|
'/repo',
|
|
'feature/test',
|
|
null,
|
|
'ssh-1',
|
|
null,
|
|
{ localGitExecOptions: { admissionTier: 'background' } }
|
|
)
|
|
})
|
|
|
|
it('does not coalesce host and WSL refreshes for the same local branch', async () => {
|
|
const { enqueuePRRefresh } = await import('./pr-refresh-coordinator')
|
|
getPRForBranchOutcomeMock
|
|
.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ number: 12 }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ number: 44 }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
|
|
enqueuePRRefresh(makeCandidate({ cacheKey: 'host::repo-1::feature/test' }), 'active', 80, 1)
|
|
enqueuePRRefresh(
|
|
makeCandidate({
|
|
cacheKey: 'wsl::repo-1::feature/test',
|
|
localGitOptions: { wslDistro: 'Ubuntu' }
|
|
}),
|
|
'active',
|
|
80,
|
|
1
|
|
)
|
|
await vi.runOnlyPendingTimersAsync()
|
|
await vi.runOnlyPendingTimersAsync()
|
|
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(2)
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenNthCalledWith(
|
|
1,
|
|
'/repo',
|
|
'feature/test',
|
|
null,
|
|
null,
|
|
null,
|
|
{ localGitExecOptions: { admissionTier: 'background' } }
|
|
)
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenNthCalledWith(
|
|
2,
|
|
'/repo',
|
|
'feature/test',
|
|
null,
|
|
null,
|
|
null,
|
|
{ localGitExecOptions: { wslDistro: 'Ubuntu', admissionTier: 'background' } }
|
|
)
|
|
})
|
|
|
|
it('preserves coalesced aliases across visible follow-up refreshes', async () => {
|
|
const { reportVisiblePRRefreshCandidates } = await import('./pr-refresh-coordinator')
|
|
getPRForBranchOutcomeMock
|
|
.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ checksStatus: 'pending', mergeable: 'MERGEABLE' }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
.mockResolvedValueOnce({
|
|
kind: 'found',
|
|
pr: makePR({ checksStatus: 'success' }),
|
|
fetchedAt: Date.now()
|
|
})
|
|
|
|
reportVisiblePRRefreshCandidates(
|
|
[
|
|
makeCandidate({
|
|
cacheKey: '/repo::feature/a',
|
|
branch: 'feature/a',
|
|
linkedPRNumber: 12,
|
|
worktreeId: 'wt-a'
|
|
}),
|
|
makeCandidate({
|
|
cacheKey: '/repo::feature/b',
|
|
branch: 'feature/b',
|
|
linkedPRNumber: 12,
|
|
worktreeId: 'wt-b'
|
|
})
|
|
],
|
|
1,
|
|
1
|
|
)
|
|
await vi.runOnlyPendingTimersAsync()
|
|
await vi.advanceTimersByTimeAsync(90_000)
|
|
|
|
const outcomeEvents = sendMock.mock.calls
|
|
.map(([, event]) => event)
|
|
.filter((event) => event.outcome)
|
|
|
|
expect(outcomeEvents).toHaveLength(2)
|
|
expect(outcomeEvents[1].aliases.map((alias) => alias.cacheKey).sort()).toEqual([
|
|
'/repo::feature/a',
|
|
'/repo::feature/b'
|
|
])
|
|
expect(getPRForBranchOutcomeMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
describe('pruneWorktreePRRefreshAliases', () => {
|
|
// Several local worktrees tracking the same linked PR coalesce into one
|
|
// queue entry (same refreshKey) whose alias map keeps one entry each.
|
|
const LINKED_PR_KEY = 'local::runtime:host::/repo::pr::42'
|
|
|
|
function makeLinkedCandidate(worktreeId: string): GitHubPRRefreshCandidate {
|
|
return makeCandidate({
|
|
worktreeId,
|
|
cacheKey: `/repo::${worktreeId}`,
|
|
branch: `feature/${worktreeId}`,
|
|
linkedPRNumber: 42
|
|
})
|
|
}
|
|
|
|
it('drops a removed worktree alias and deletes the entry when it was the last', async () => {
|
|
const { enqueuePRRefresh, pruneWorktreePRRefreshAliases, _getPRRefreshAliasCountForTests } =
|
|
await import('./pr-refresh-coordinator')
|
|
|
|
enqueuePRRefresh(makeLinkedCandidate('wt-1'), 'visible', 40, 1)
|
|
enqueuePRRefresh(makeLinkedCandidate('wt-2'), 'visible', 40, 1)
|
|
enqueuePRRefresh(makeLinkedCandidate('wt-3'), 'visible', 40, 1)
|
|
expect(_getPRRefreshAliasCountForTests(LINKED_PR_KEY)).toBe(3)
|
|
|
|
pruneWorktreePRRefreshAliases('wt-2')
|
|
expect(_getPRRefreshAliasCountForTests(LINKED_PR_KEY)).toBe(2)
|
|
|
|
pruneWorktreePRRefreshAliases('wt-1')
|
|
pruneWorktreePRRefreshAliases('wt-3')
|
|
// Last alias gone -> the whole queue entry is dropped.
|
|
expect(_getPRRefreshAliasCountForTests(LINKED_PR_KEY)).toBe(0)
|
|
})
|
|
|
|
it('keeps the entry alive and rebinds the candidate when other aliases remain', async () => {
|
|
const {
|
|
enqueuePRRefresh,
|
|
pruneWorktreePRRefreshAliases,
|
|
_getPRRefreshAliasCountForTests,
|
|
_getPRRefreshQueueSizeForTests
|
|
} = await import('./pr-refresh-coordinator')
|
|
|
|
// wt-1 becomes the entry's representative candidate (enqueued first).
|
|
enqueuePRRefresh(makeLinkedCandidate('wt-1'), 'visible', 40, 1)
|
|
enqueuePRRefresh(makeLinkedCandidate('wt-2'), 'visible', 40, 1)
|
|
expect(_getPRRefreshQueueSizeForTests()).toBe(1)
|
|
|
|
// Removing the representative worktree must not orphan the entry.
|
|
pruneWorktreePRRefreshAliases('wt-1')
|
|
expect(_getPRRefreshAliasCountForTests(LINKED_PR_KEY)).toBe(1)
|
|
expect(_getPRRefreshQueueSizeForTests()).toBe(1)
|
|
})
|
|
|
|
it('is a no-op for a worktree with no queued aliases', async () => {
|
|
const { enqueuePRRefresh, pruneWorktreePRRefreshAliases, _getPRRefreshAliasCountForTests } =
|
|
await import('./pr-refresh-coordinator')
|
|
|
|
enqueuePRRefresh(makeLinkedCandidate('wt-1'), 'visible', 40, 1)
|
|
pruneWorktreePRRefreshAliases('wt-unknown')
|
|
expect(_getPRRefreshAliasCountForTests(LINKED_PR_KEY)).toBe(1)
|
|
})
|
|
})
|
|
})
|