Files
orca/src/main/runtime/github-check-details-paired.integration.test.ts
T
Jinjing 0d4eb242d7 Recover failed-job log retrieval onto main (#29180)
* feat(checks): add optional job log outcome

* fix(checks): preserve host capture lifetimes and safe outcomes

* fix(checks): retrieve and retry bounded failed-job logs

* test(checks): cover queued snapshots and captured error safety

* test(checks): verify sanitized host excerpts and paired failures

* test(checks): identify measurements and preserve paired cleanup

* Use the generated check-log fixture in host retrieval tests

* Keep real paired-server shutdown outside the injected stall deadline
2026-10-15 10:36:21 -07:00

300 lines
12 KiB
TypeScript

import './rpc/unused-default-rpc-methods.test-fixture'
import type { ChildProcess, SpawnOptions } from 'node:child_process'
import { existsSync, mkdtempSync, rmSync } from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import type * as ProcessHostModule from '@orca/process-host'
import {
completeFakeSpawn,
createFakeSpawnedChild
} from '../../shared/__fixtures__/fake-spawned-child'
import { parsePairingCode } from '../../shared/pairing'
import { pairedRuntimeConnectionFromOffer } from '../../shared/paired-runtime-connection'
import { RemoteRuntimeRequestConnection } from '../../shared/remote-runtime-request-connection'
import type { Repo } from '../../shared/repo-types'
import type { OrcaRuntimeService } from './orca-runtime'
import { OrcaRuntimeRpcServer } from './runtime-rpc'
import { RuntimeGitHubReviewQueryCommands } from './runtime-github-review-query-commands'
import { GITHUB_PULL_REQUEST_METHODS } from './rpc/methods/github-pull-request-methods'
import { clearGhAccountTokenCacheForTests } from '../github/gh-account-token'
import { clearGhCapabilityStateForTests } from '../github/gh-capability-state'
import { prCheckLogTailCache } from '../github/client/check/check-job-log-tails'
import { withPairedCheckDetailsCleanup } from './github-check-details-paired-cleanup.test-fixture'
const { spawnMock, resolverMock, detailsDelegate, captures, terminationMock } = vi.hoisted(() => {
const signals: AbortSignal[] = []
return {
spawnMock: vi.fn(),
resolverMock: vi.fn(),
detailsDelegate: vi.fn(),
captures: { host: 0, client: 0, signals },
terminationMock: vi.fn()
}
})
vi.mock('node:child_process', async (original) => ({ ...(await original()), spawn: spawnMock }))
vi.mock('../github/github-api-repository', () => ({ resolveGitHubRepoExecution: resolverMock }))
vi.mock('../github/client', async () => {
const actual = await import('../github/client/check/get-pr-check-details')
return {
getPRCheckDetails: detailsDelegate.mockImplementation(actual.getPRCheckDetails),
getPRChecks: vi.fn(),
getPRComments: vi.fn(),
getIssue: vi.fn()
}
})
vi.mock('../github/work-item-details', () => ({ getPRFileContents: vi.fn() }))
vi.mock('@orca/process-host/process-tree-termination', () => ({
signalProcessTree: terminationMock,
forceTerminateProcessTree: terminationMock
}))
vi.mock('@orca/process-host', async (original) => {
const actual = await original<typeof ProcessHostModule>()
return {
...actual,
runProcess: (spec: Parameters<typeof actual.runProcess>[0]) => {
if (spec.cwd === hostFolder && spec.signal) {
captures.signals.push(spec.signal)
}
return actual.runProcess(spec)
}
}
})
let hostFolder = ''
const children: ChildProcess[] = []
let logChild: ChildProcess | undefined
describe('paired check details over real encrypted loopback transport', () => {
beforeEach(() => {
children.length = 0
logChild = undefined
captures.host = captures.client = 0
captures.signals.length = 0
detailsDelegate.mockClear()
terminationMock.mockReset().mockResolvedValue(false)
clearGhAccountTokenCacheForTests()
clearGhCapabilityStateForTests()
prCheckLogTailCache.clear()
vi.spyOn(console, 'warn').mockImplementation(() => {})
})
afterEach(() => vi.restoreAllMocks())
it.each(['failure', 'disconnect', 'drain-failure', 'stop-failure', 'stop-stall'])(
'executes only on the handling host and cleans up after %s',
{ timeout: 15_000 },
async (mode) => {
const disconnect = mode === 'disconnect' || mode === 'drain-failure'
const directory = mkdtempSync(join(tmpdir(), 'orca-paired-check-details-'))
hostFolder = join(directory, 'handling-host-folder')
const repo: Repo = {
id: 'folder',
path: hostFolder,
kind: 'folder',
displayName: 'folder',
badgeColor: 'blue',
addedAt: 1,
ghAccount: { host: 'github.enterprise.test', user: 'Alice' }
}
const resolveRepo = vi.fn(async (selector: string) => {
if (selector !== 'id:folder') {
throw new Error('Unknown registered folder')
}
return repo
})
const query = new RuntimeGitHubReviewQueryCommands({
resolveRepo,
getLocalGitArgs: (registered) => [{ ghAccount: registered.ghAccount }]
})
const delegate = vi.spyOn(query, 'getRepoPRCheckDetails')
let hostSettled = false
const runtime = {
configureNotificationDismissalStore: () => {},
getRuntimeId: () => 'paired-check-host',
getStartedAt: () => 1,
cleanupSubscriptionsForConnection: () => {},
cancelMobileDictationForConnection: () => {},
onClientDisconnected: () => {},
getRepoPRCheckDetails: (...args: Parameters<typeof query.getRepoPRCheckDetails>) =>
query.getRepoPRCheckDetails(...args).finally(() => {
hostSettled = true
})
}
resolverMock.mockImplementation(async (path, prRepo, connection, options) => {
expect(path).toBe(hostFolder)
expect(connection).toBeNull()
expect(options.ghAccount).toEqual(repo.ghAccount)
return {
ownerRepo: prRepo,
ghOptions: { ...options, cwd: hostFolder, host: 'github.enterprise.test', env: {} }
}
})
spawnMock
.mockReset()
.mockImplementation((_program: string, argv: string[], options: SpawnOptions) => {
if (options.cwd === hostFolder) {
captures.host += 1
} else {
captures.client += 1
}
const child = createFakeSpawnedChild(800 + children.length)
children.push(child)
if (argv.some((arg) => arg.endsWith('/logs'))) {
logChild = child
return child
}
queueMicrotask(() =>
completeFakeSpawn(
child,
argv.includes('--help')
? { stdout: '--user' }
: argv[0] === 'auth'
? { stdout: 'fixture-bound-token' }
: !disconnect
? { code: 1, stderr: 'fixture-ordinary-failure' }
: {
stdout: JSON.stringify({
jobs: [
{
id: 77,
name: 'Test',
status: 'completed',
conclusion: 'failure',
completed_at: 'done',
steps: []
}
]
})
}
)
)
return child
})
const server = new OrcaRuntimeRpcServer({
// oxlint-disable-next-line typescript/consistent-type-assertions -- SAFETY: Explicit methods and lifecycle callbacks are the entire runtime surface used by this server fixture.
runtime: runtime as unknown as OrcaRuntimeService,
userDataPath: directory,
enableWebSocket: true,
wsPort: 0,
methods: GITHUB_PULL_REQUEST_METHODS
})
let connection: RemoteRuntimeRequestConnection | undefined
const cleanupFailure = new Error('Forced paired cleanup failure')
let childClosureAttempted = false
let serverStopped = false
let finishStall: () => void = () => {}
const stalled = new Promise<void>((resolve) => {
finishStall = resolve
})
const pendingCleanup = withPairedCheckDetailsCleanup(
{
closeClient: () => connection?.close(),
drain: () =>
vi.waitFor(
() => {
if (mode === 'drain-failure') {
throw cleanupFailure
}
expect(server.readClientActivity().openConnections).toBe(0)
},
{ timeout: mode === 'drain-failure' ? 25 : 1_000 }
),
closeChildren: async () => {
childClosureAttempted = true
for (const child of children) {
child.emit('close', 0, null)
}
if (mode === 'stop-stall') {
await server.stop()
serverStopped = true
}
},
stopServer: async () => {
if (mode !== 'stop-stall') {
await server.stop()
serverStopped = true
}
if (mode === 'stop-failure') {
throw cleanupFailure
}
if (mode === 'stop-stall') {
await stalled
}
},
removeDirectory: () => rmSync(directory, { recursive: true, force: true })
},
async () => {
await server.start()
const offer = server.createPairingOffer({ name: 'check-details', scope: 'runtime' })
if (!offer.available) {
throw new Error('Pairing unavailable')
}
const pairing = parsePairingCode(offer.pairingUrl)
if (!pairing) {
throw new Error('Invalid fixture pairing')
}
connection = new RemoteRuntimeRequestConnection(pairedRuntimeConnectionFromOffer(pairing))
const caller = new AbortController()
const pending = connection.request(
'github.prCheckDetails',
{
repo: 'id:folder',
checkRunId: !disconnect ? 9 : undefined,
workflowRunId: disconnect ? 8 : undefined,
prRepo: { owner: 'base', repo: 'widgets', host: 'github.enterprise.test' }
},
5_000,
caller.signal
)
if (!disconnect) {
await expect(pending).resolves.toMatchObject({
ok: false,
error: { message: 'Could not load check details.' }
})
} else {
const rejection = expect(pending).rejects.toThrow()
await vi.waitFor(() => expect(logChild).toBeDefined())
connection.close()
await rejection
await vi.waitFor(() => expect(delegate.mock.calls[0][2]?.aborted).toBe(true))
expect(captures.signals.some((signal) => signal.aborted)).toBe(true)
expect(logChild).toBeDefined()
logChild?.emit('close', null, 'SIGKILL')
}
await vi.waitFor(() => expect(hostSettled).toBe(true))
expect(delegate).toHaveBeenCalledOnce()
expect(detailsDelegate).toHaveBeenCalledOnce()
expect(detailsDelegate.mock.calls[0][4]).toBe(delegate.mock.calls[0][2])
expect(resolveRepo).toHaveBeenCalledOnce()
expect(captures.host).toBe(disconnect ? 4 : 3)
expect(captures.client).toBe(0)
expect(captures.signals).toHaveLength(disconnect ? 2 : 1)
const apiCalls = spawnMock.mock.calls.filter((call) => call[1][0] === 'api')
expect(apiCalls).toHaveLength(disconnect ? 2 : 1)
for (const call of apiCalls) {
expect(call[1]).toContain('github.enterprise.test')
expect(call[2].cwd).toBe(hostFolder)
expect(call[2].env.GH_ENTERPRISE_TOKEN).toBe('fixture-bound-token')
}
expect(prCheckLogTailCache.size).toBe(0)
},
mode === 'stop-stall' ? 100 : 2_000
)
try {
if (mode === 'drain-failure' || mode === 'stop-failure') {
await expect(pendingCleanup).rejects.toBe(cleanupFailure)
} else if (mode === 'stop-stall') {
await expect(pendingCleanup).rejects.toThrow('Paired check server stop timed out')
} else {
await pendingCleanup
}
} finally {
finishStall()
}
expect(childClosureAttempted).toBe(true)
expect(serverStopped).toBe(true)
expect(server.readClientActivity().openConnections).toBe(0)
expect(existsSync(directory)).toBe(false)
}
)
})