Skip to content

Commit 802ef53

Browse files
committed
test(autofix-pr): 补齐 completionChecker / 边界 CI 检查覆盖率
针对 codecov patch coverage gap, 补足三块此前未走到的代码路径: prOutcomeCheck.ts (原 96.92%, 2 lines missing): - statusCheckRollup === undefined 路径 (与空数组分支不同, GitHub 在无 checks 配置的 PR 上直接省略字段) - COMPLETED 状态但 conclusion 为 null/空 的 in-flight 检查归为 pending launchAutofixPr.ts (原 58.33%, 15 lines missing): - registerCompletionChecker arrow body: metadata 缺失早返回 / 节流窗口内 返回 null / completed=false 返回 null / completed=true 返回 summary / initialHeadSha 透传到 checkPrAutofixOutcome - registerCompletionHook 的 if(meta) 短路两侧: 有 metadata 时清空节流条目, 无 metadata 时仍释放 active monitor lock 所有新测试沿用现有 mock.module 与 registerXxxMock.mock.calls 拉取注册 回调的模式, 无新增依赖。prOutcomeCheck 11/11 本地通过。
1 parent 7471132 commit 802ef53

2 files changed

Lines changed: 176 additions & 0 deletions

File tree

src/commands/autofix-pr/__tests__/launchAutofixPr.test.ts

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -529,6 +529,147 @@ describe('callAutofixPr · Phase 2 completionChecker integration', () => {
529529
})
530530
})
531531

532+
// Phase 2 (cont.): exercise the registered completionChecker arrow body
533+
// directly. The earlier suite verifies it was registered but never invokes
534+
// the arrow itself, leaving the throttle / metadata-guard / gh-CLI dispatch
535+
// branches uncovered.
536+
describe('callAutofixPr · Phase 2 completionChecker arrow body', () => {
537+
// Pull the most recent registered checker — beforeAll registers once at
538+
// module load; nothing else re-registers across this file's tests.
539+
function getChecker(): (
540+
metadata?: unknown,
541+
) => Promise<string | null> {
542+
const calls = registerCompletionCheckerMock.mock.calls.filter(
543+
c => c[0] === 'autofix-pr',
544+
)
545+
const fn = calls[calls.length - 1]?.[1]
546+
if (typeof fn !== 'function') {
547+
throw new Error('completionChecker not registered')
548+
}
549+
return fn
550+
}
551+
552+
test('returns null when metadata is undefined (early guard)', async () => {
553+
const checker = getChecker()
554+
expect(await checker(undefined)).toBeNull()
555+
})
556+
557+
test('returns null when checkPrAutofixOutcome reports not completed', async () => {
558+
const { checkPrAutofixOutcome } = await import('../prFetch.js')
559+
;(checkPrAutofixOutcome as ReturnType<typeof mock>).mockImplementationOnce(
560+
() => Promise.resolve({ completed: false }),
561+
)
562+
const checker = getChecker()
563+
// Distinct PR number to dodge the in-process throttle map carried over
564+
// from earlier tests.
565+
const result = await checker({
566+
owner: 'acme',
567+
repo: 'myrepo',
568+
prNumber: 1001,
569+
})
570+
expect(result).toBeNull()
571+
})
572+
573+
test('returns the summary string when checkPrAutofixOutcome reports completed', async () => {
574+
const { checkPrAutofixOutcome } = await import('../prFetch.js')
575+
;(checkPrAutofixOutcome as ReturnType<typeof mock>).mockImplementationOnce(
576+
() =>
577+
Promise.resolve({
578+
completed: true,
579+
summary: 'acme/myrepo#1002 merged. Autofix monitoring complete.',
580+
}),
581+
)
582+
const checker = getChecker()
583+
const result = await checker({
584+
owner: 'acme',
585+
repo: 'myrepo',
586+
prNumber: 1002,
587+
})
588+
expect(result).toBe(
589+
'acme/myrepo#1002 merged. Autofix monitoring complete.',
590+
)
591+
})
592+
593+
test('passes initialHeadSha through to checkPrAutofixOutcome', async () => {
594+
const { checkPrAutofixOutcome } = await import('../prFetch.js')
595+
const checkMock = checkPrAutofixOutcome as ReturnType<typeof mock>
596+
checkMock.mockClear()
597+
checkMock.mockImplementationOnce(() => Promise.resolve({ completed: false }))
598+
const checker = getChecker()
599+
await checker({
600+
owner: 'acme',
601+
repo: 'myrepo',
602+
prNumber: 1003,
603+
initialHeadSha: 'sha-baseline-xyz',
604+
})
605+
expect(checkMock).toHaveBeenCalledWith({
606+
owner: 'acme',
607+
repo: 'myrepo',
608+
prNumber: 1003,
609+
initialHeadSha: 'sha-baseline-xyz',
610+
})
611+
})
612+
613+
test('throttles back-to-back calls for the same PR within CHECK_INTERVAL_MS', async () => {
614+
const { checkPrAutofixOutcome } = await import('../prFetch.js')
615+
const checkMock = checkPrAutofixOutcome as ReturnType<typeof mock>
616+
checkMock.mockClear()
617+
checkMock.mockImplementation(() => Promise.resolve({ completed: false }))
618+
const checker = getChecker()
619+
const meta = { owner: 'acme', repo: 'myrepo', prNumber: 1004 }
620+
await checker(meta)
621+
// Second call within the 5s throttle window must short-circuit to null
622+
// without invoking the gh CLI layer again.
623+
const callCountAfterFirst = checkMock.mock.calls.length
624+
const result = await checker(meta)
625+
expect(result).toBeNull()
626+
expect(checkMock.mock.calls.length).toBe(callCountAfterFirst)
627+
})
628+
629+
test('completionHook with metadata clears the throttle entry (re-launch can re-check immediately)', async () => {
630+
const { checkPrAutofixOutcome } = await import('../prFetch.js')
631+
const checkMock = checkPrAutofixOutcome as ReturnType<typeof mock>
632+
checkMock.mockClear()
633+
checkMock.mockImplementation(() => Promise.resolve({ completed: false }))
634+
const checker = getChecker()
635+
const meta = { owner: 'acme', repo: 'myrepo', prNumber: 1005 }
636+
await checker(meta) // populate throttle map
637+
638+
// Invoke the registered completion hook with the same metadata so the
639+
// throttle entry is wiped, then verify the next checker call dispatches
640+
// gh CLI again instead of short-circuiting.
641+
const hookCalls = registerCompletionHookMock.mock.calls.filter(
642+
c => c[0] === 'autofix-pr',
643+
)
644+
const hook = hookCalls[hookCalls.length - 1]?.[1] as (
645+
id: string,
646+
metadata?: unknown,
647+
) => void
648+
hook('any-task-id', meta)
649+
650+
const callCountBefore = checkMock.mock.calls.length
651+
await checker(meta)
652+
expect(checkMock.mock.calls.length).toBe(callCountBefore + 1)
653+
})
654+
655+
test('completionHook without metadata still clears the active monitor lock', async () => {
656+
// Lock is set via callAutofixPr; hook then invoked with undefined metadata
657+
// to exercise the `if (meta)` short-circuit branch (the lock-clear half
658+
// still has to run regardless of metadata presence).
659+
await callAutofixPr(onDone, makeContext(), '42')
660+
expect(getActiveMonitor()).not.toBeNull()
661+
const hookCalls = registerCompletionHookMock.mock.calls.filter(
662+
c => c[0] === 'autofix-pr',
663+
)
664+
const hook = hookCalls[hookCalls.length - 1]?.[1] as (
665+
id: string,
666+
metadata?: unknown,
667+
) => void
668+
hook('framework-task-id', undefined)
669+
expect(getActiveMonitor()).toBeNull()
670+
})
671+
})
672+
532673
// Phase 3: content extractor wiring + initialMessage tag instruction
533674
describe('callAutofixPr · Phase 3 content extractor integration', () => {
534675
test('registerContentExtractor is called at module load with autofix-pr type', () => {

src/commands/autofix-pr/__tests__/prOutcomeCheck.test.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,6 +133,41 @@ describe('summariseAutofixOutcome · OPEN PR with push, CI variations', () => {
133133
}
134134
})
135135

136+
test('statusCheckRollup undefined → treated as no checks configured (success)', () => {
137+
// Distinct from empty-array: GitHub omits the field entirely on PRs
138+
// without any configured checks. The !rollup branch covers undefined.
139+
const result = summariseAutofixOutcome(
140+
basePayload({
141+
state: 'OPEN',
142+
headRefOid: 'sha-new',
143+
statusCheckRollup: undefined,
144+
}),
145+
identity(),
146+
)
147+
expect(result.completed).toBe(true)
148+
if (result.completed) {
149+
expect(result.summary).toContain('CI green')
150+
}
151+
})
152+
153+
test('check with COMPLETED status but empty conclusion → counted as pending', () => {
154+
// Edge case: GitHub sometimes reports a check as COMPLETED with a null/
155+
// missing conclusion (in-flight result mid-write). The defensive branch
156+
// treats empty conclusion after a passed status check as pending.
157+
const result = summariseAutofixOutcome(
158+
basePayload({
159+
state: 'OPEN',
160+
headRefOid: 'sha-new',
161+
statusCheckRollup: [
162+
{ status: 'COMPLETED', conclusion: null, name: 'ci-in-flight' },
163+
{ status: 'COMPLETED', conclusion: 'SUCCESS', name: 'lint' },
164+
],
165+
}),
166+
identity(),
167+
)
168+
expect(result).toEqual({ completed: false })
169+
})
170+
136171
test('neutral / skipped conclusions count as success (not failure)', () => {
137172
const result = summariseAutofixOutcome(
138173
basePayload({

0 commit comments

Comments
 (0)