Skip to content

Commit c076de5

Browse files
authored
Merge pull request #1826 from quantified-uncertainty/claude/stupefied-chaum
fix(pr-patrol): reduce compute waste with issue-type budgets and timeout abandonment
2 parents 3e54e82 + 0221084 commit c076de5

File tree

2 files changed

+273
-44
lines changed

2 files changed

+273
-44
lines changed

crux/pr-patrol/index.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,8 @@ import {
33
checkMergeEligibility,
44
findMergeCandidates,
55
detectIssues,
6+
computeBudget,
7+
looksLikeNoOp,
68
type GqlPrNode,
79
} from './index.ts';
810

@@ -383,6 +385,46 @@ describe('findMergeCandidates', () => {
383385
});
384386
});
385387

388+
// ── computeBudget ────────────────────────────────────────────────────────────
389+
390+
describe('computeBudget', () => {
391+
it('gives small budget for missing-issue-ref only', () => {
392+
const budget = computeBudget(['missing-issue-ref']);
393+
expect(budget.maxTurns).toBe(5);
394+
expect(budget.timeoutMinutes).toBe(3);
395+
});
396+
397+
it('gives small budget for missing-testplan only', () => {
398+
const budget = computeBudget(['missing-testplan']);
399+
expect(budget.maxTurns).toBe(8);
400+
expect(budget.timeoutMinutes).toBe(5);
401+
});
402+
403+
it('gives medium budget for ci-failure', () => {
404+
const budget = computeBudget(['ci-failure']);
405+
expect(budget.maxTurns).toBe(25);
406+
expect(budget.timeoutMinutes).toBe(15);
407+
});
408+
409+
it('gives full budget for conflict', () => {
410+
const budget = computeBudget(['conflict']);
411+
expect(budget.maxTurns).toBe(40);
412+
expect(budget.timeoutMinutes).toBe(30);
413+
});
414+
415+
it('uses highest budget when multiple issues present', () => {
416+
const budget = computeBudget(['missing-issue-ref', 'ci-failure']);
417+
expect(budget.maxTurns).toBe(25);
418+
expect(budget.timeoutMinutes).toBe(15);
419+
});
420+
421+
it('conflict dominates when mixed with smaller issues', () => {
422+
const budget = computeBudget(['missing-testplan', 'conflict', 'missing-issue-ref']);
423+
expect(budget.maxTurns).toBe(40);
424+
expect(budget.timeoutMinutes).toBe(30);
425+
});
426+
});
427+
386428
// ── detectIssues (regression test after refactor) ────────────────────────────
387429

388430
describe('detectIssues', () => {
@@ -431,3 +473,43 @@ describe('detectIssues', () => {
431473
expect(result.issues).toEqual([]);
432474
});
433475
});
476+
477+
// ── looksLikeNoOp ───────────────────────────────────────────────────────────
478+
479+
describe('looksLikeNoOp', () => {
480+
it('detects "no action needed" in output tail', () => {
481+
expect(looksLikeNoOp('Analyzed the issue. No action needed for this PR.')).toBe(true);
482+
});
483+
484+
it('detects "requires human intervention"', () => {
485+
expect(looksLikeNoOp('The check-protected-paths check requires human intervention to add the label.')).toBe(true);
486+
});
487+
488+
it('detects "pre-existing failure"', () => {
489+
expect(looksLikeNoOp('This is a pre-existing failure also present on main.')).toBe(true);
490+
});
491+
492+
it('detects "also failing on main"', () => {
493+
expect(looksLikeNoOp('The CI check is also failing on main, so this is not introduced by this PR.')).toBe(true);
494+
});
495+
496+
it('detects "stopping early"', () => {
497+
expect(looksLikeNoOp('Stopping early because the issue cannot be resolved automatically.')).toBe(true);
498+
});
499+
500+
it('does NOT flag normal fix output', () => {
501+
expect(looksLikeNoOp('Fixed the TypeScript error in src/index.ts. All tests passing now.')).toBe(false);
502+
});
503+
504+
it('does NOT flag output with "no" in unrelated context', () => {
505+
expect(looksLikeNoOp('Added the missing test. No regressions found after running the suite.')).toBe(false);
506+
});
507+
508+
it('only checks last 1000 chars of output', () => {
509+
const longOutput = 'x'.repeat(2000) + 'No action needed.';
510+
expect(looksLikeNoOp(longOutput)).toBe(true);
511+
// Pattern in the first 1000 chars but not the last 1000
512+
const earlyMatch = 'No action needed.' + 'x'.repeat(2000);
513+
expect(looksLikeNoOp(earlyMatch)).toBe(false);
514+
});
515+
});

0 commit comments

Comments
 (0)