@humanbased/crosscheck 1.3.0-beta.91 → 1.3.0-beta.93

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +1 -1
  2. package/crosscheck.config.example.yml +9 -0
  3. package/dist/__tests__/adoption.test.d.ts +2 -0
  4. package/dist/__tests__/adoption.test.d.ts.map +1 -0
  5. package/dist/__tests__/adoption.test.js +175 -0
  6. package/dist/__tests__/adoption.test.js.map +1 -0
  7. package/dist/__tests__/detector.test.js +87 -1
  8. package/dist/__tests__/detector.test.js.map +1 -1
  9. package/dist/__tests__/runner.test.js +161 -1
  10. package/dist/__tests__/runner.test.js.map +1 -1
  11. package/dist/cli.js +8 -0
  12. package/dist/cli.js.map +1 -1
  13. package/dist/commands/adoption.d.ts +6 -0
  14. package/dist/commands/adoption.d.ts.map +1 -0
  15. package/dist/commands/adoption.js +132 -0
  16. package/dist/commands/adoption.js.map +1 -0
  17. package/dist/commands/onboard.d.ts.map +1 -1
  18. package/dist/commands/onboard.js +30 -3
  19. package/dist/commands/onboard.js.map +1 -1
  20. package/dist/commands/run.d.ts.map +1 -1
  21. package/dist/commands/run.js +30 -2
  22. package/dist/commands/run.js.map +1 -1
  23. package/dist/commands/watch.d.ts.map +1 -1
  24. package/dist/commands/watch.js +4 -0
  25. package/dist/commands/watch.js.map +1 -1
  26. package/dist/config/schema.d.ts.map +1 -1
  27. package/dist/config/schema.js +7 -0
  28. package/dist/config/schema.js.map +1 -1
  29. package/dist/github/detector.d.ts.map +1 -1
  30. package/dist/github/detector.js +28 -1
  31. package/dist/github/detector.js.map +1 -1
  32. package/dist/github/webhook.d.ts +1 -0
  33. package/dist/github/webhook.d.ts.map +1 -1
  34. package/dist/github/webhook.js.map +1 -1
  35. package/dist/lib/adoption.d.ts +64 -0
  36. package/dist/lib/adoption.d.ts.map +1 -0
  37. package/dist/lib/adoption.js +165 -0
  38. package/dist/lib/adoption.js.map +1 -0
  39. package/dist/lib/runner.d.ts +24 -0
  40. package/dist/lib/runner.d.ts.map +1 -1
  41. package/dist/lib/runner.js +115 -16
  42. package/dist/lib/runner.js.map +1 -1
  43. package/dist/lib/workflow.d.ts +4 -0
  44. package/dist/lib/workflow.d.ts.map +1 -1
  45. package/dist/lib/workflow.js.map +1 -1
  46. package/docs/metrics.md +115 -0
  47. package/get-started.md +67 -0
  48. package/package.json +1 -1
package/README.md CHANGED
@@ -179,7 +179,7 @@ Full walkthrough: **[docs/linear-identity.md](./docs/linear-identity.md)**.
179
179
  | `crosscheck alter <repo>` | Set a per-repo pipeline depth |
180
180
  | `crosscheck detect-step <pr>` | Show step history and the next step to run |
181
181
  | `crosscheck linear-test [issue]` | Dry-run Linear write-back |
182
- | `crosscheck diagnose` / `optimize` / `impact` / `issue` | Analyse logs, tune config, report value, file tickets |
182
+ | `crosscheck diagnose` / `optimize` / `impact` / `adoption` / `issue` | Analyse logs, tune config, report value, report usage, file tickets |
183
183
 
184
184
  Multi-PR forms work where sensible — comma lists, bare numbers, and ranges:
185
185
 
@@ -176,6 +176,15 @@ routing:
176
176
 
177
177
  # Body and commit pattern matching — checked first, before branch prefix.
178
178
  # Patterns are matched against both the PR body and commit messages (Co-Authored-By trailers).
179
+ # These identify who WROTE the code, not who reviewed it. Do not loosen them to
180
+ # match "Reviewed with [Claude Code]" — every PR Claude reviews would then look
181
+ # Claude-authored and get routed back to the other vendor.
182
+ #
183
+ # Crosscheck's own attribution (its fix-PR footer and Crosscheck-Reviewer commit
184
+ # trailers) is recognised ALWAYS and is deliberately not listed here: setting
185
+ # either list replaces the defaults entirely, and an install that pinned its own
186
+ # patterns would then misread crosscheck's own fix PRs as human-authored. Your
187
+ # patterns below are still checked first, so they outrank it.
179
188
  codex_reviews_patterns:
180
189
  - "Generated with \\[Claude Code\\]" # PR body attribution footer
181
190
  - "Co-Authored-By: Claude" # commit trailer added by Claude Code
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=adoption.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adoption.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/adoption.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,175 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { buildAdoptionReport, firstRunFailures, isoWeekMonday, percentile, prOpenToVerdictMs, } from '../lib/adoption.js';
3
+ const PERIOD = { from: '2026-07-01', to: '2026-07-14', log_files: 2 };
4
+ function line(event, fields = {}) {
5
+ return { ts: '2026-07-08T10:00:00.000Z', event, ...fields };
6
+ }
7
+ describe('prOpenToVerdictMs', () => {
8
+ const opened = '2026-07-08T09:00:00.000Z';
9
+ const now = Date.parse('2026-07-08T10:30:00.000Z');
10
+ it('measures from PR open to the moment the verdict is posted', () => {
11
+ expect(prOpenToVerdictMs(opened, 'APPROVE', now)).toBe(90 * 60 * 1000);
12
+ });
13
+ it('returns undefined when the review produced no verdict', () => {
14
+ expect(prOpenToVerdictMs(opened, null, now)).toBeUndefined();
15
+ });
16
+ it('returns undefined when the PR event carried no open time', () => {
17
+ expect(prOpenToVerdictMs(undefined, 'BLOCK', now)).toBeUndefined();
18
+ });
19
+ it('returns undefined for an unparseable timestamp rather than NaN', () => {
20
+ expect(prOpenToVerdictMs('last Tuesday', 'BLOCK', now)).toBeUndefined();
21
+ });
22
+ it('drops a negative elapsed time from clock skew instead of reporting it', () => {
23
+ expect(prOpenToVerdictMs('2026-07-08T11:00:00.000Z', 'APPROVE', now)).toBeUndefined();
24
+ });
25
+ });
26
+ describe('isoWeekMonday', () => {
27
+ it('maps a mid-week timestamp to that week Monday', () => {
28
+ expect(isoWeekMonday('2026-07-08T10:00:00.000Z')).toBe('2026-07-06');
29
+ });
30
+ it('maps Sunday back to the Monday that started its week', () => {
31
+ expect(isoWeekMonday('2026-07-12T23:59:00.000Z')).toBe('2026-07-06');
32
+ });
33
+ it('returns null for an unparseable timestamp', () => {
34
+ expect(isoWeekMonday('not-a-date')).toBeNull();
35
+ });
36
+ });
37
+ describe('percentile', () => {
38
+ it('returns a value that actually occurred in the sample', () => {
39
+ const sorted = [10, 20, 30, 40, 50];
40
+ expect(percentile(sorted, 0.5)).toBe(30);
41
+ expect(percentile(sorted, 0.9)).toBe(50);
42
+ });
43
+ it('returns 0 for an empty sample', () => {
44
+ expect(percentile([], 0.5)).toBe(0);
45
+ });
46
+ });
47
+ describe('buildAdoptionReport', () => {
48
+ it('counts the activation funnel and treats a missing completion as abandonment', () => {
49
+ const report = buildAdoptionReport([
50
+ line('onboard_started'),
51
+ line('onboard_completed', { outcome: 'completed' }),
52
+ line('onboard_started'),
53
+ line('onboard_completed', { outcome: 'abandoned' }),
54
+ line('onboard_started'),
55
+ ], PERIOD);
56
+ expect(report.onboarding).toEqual({ started: 3, completed: 1, abandoned: 2 });
57
+ });
58
+ it('separates rechecks from initial reviews via step_type', () => {
59
+ const report = buildAdoptionReport([
60
+ line('review_complete', { repo: 'acme/web', step_type: 'review', verdict: 'BLOCK' }),
61
+ line('review_complete', { repo: 'acme/web', step_type: 'recheck', verdict: 'APPROVE' }),
62
+ ], PERIOD);
63
+ expect(report.activity.reviews_completed).toBe(2);
64
+ expect(report.activity.rechecks_completed).toBe(1);
65
+ });
66
+ it('counts distinct repos, not reviews, as active repos', () => {
67
+ const report = buildAdoptionReport([
68
+ line('review_complete', { repo: 'acme/web', verdict: 'APPROVE' }),
69
+ line('review_complete', { repo: 'acme/web', verdict: 'BLOCK' }),
70
+ line('review_complete', { repo: 'acme/api', verdict: 'APPROVE' }),
71
+ ], PERIOD);
72
+ expect(report.activity.active_repos).toBe(2);
73
+ expect(report.weekly).toEqual([{ week: '2026-07-06', active_repos: 2, reviews: 3 }]);
74
+ });
75
+ it('buckets weekly activity by ISO week', () => {
76
+ const report = buildAdoptionReport([
77
+ line('review_complete', { ts: '2026-07-08T10:00:00.000Z', repo: 'acme/web', verdict: 'APPROVE' }),
78
+ line('review_complete', { ts: '2026-07-15T10:00:00.000Z', repo: 'acme/api', verdict: 'APPROVE' }),
79
+ ], PERIOD);
80
+ expect(report.weekly.map(w => w.week)).toEqual(['2026-07-06', '2026-07-13']);
81
+ });
82
+ it('counts a fix only when it changed files', () => {
83
+ const report = buildAdoptionReport([
84
+ line('fix_complete', { applied_count: 3 }),
85
+ line('fix_complete', { applied_count: 0 }),
86
+ line('fix_complete', {}),
87
+ ], PERIOD);
88
+ expect(report.activity.fixes_applied).toBe(1);
89
+ });
90
+ it('summarises open-to-verdict latency and reports what could not be measured', () => {
91
+ const minute = 60_000;
92
+ const report = buildAdoptionReport([
93
+ line('review_complete', { repo: 'acme/web', verdict: 'APPROVE', open_to_verdict_ms: 10 * minute }),
94
+ line('review_complete', { repo: 'acme/web', verdict: 'BLOCK', open_to_verdict_ms: 30 * minute }),
95
+ line('review_complete', { repo: 'acme/web', verdict: 'APPROVE', open_to_verdict_ms: 120 * minute }),
96
+ line('review_complete', { repo: 'acme/web', verdict: 'APPROVE' }),
97
+ ], PERIOD);
98
+ expect(report.open_to_verdict.count).toBe(3);
99
+ expect(report.open_to_verdict.p50_ms).toBe(30 * minute);
100
+ expect(report.open_to_verdict.max_ms).toBe(120 * minute);
101
+ expect(report.open_to_verdict.unmeasured).toBe(1);
102
+ });
103
+ it('does not count a verdictless review as unmeasured latency', () => {
104
+ const report = buildAdoptionReport([
105
+ line('review_complete', { repo: 'acme/web' }),
106
+ ], PERIOD);
107
+ expect(report.open_to_verdict.unmeasured).toBe(0);
108
+ });
109
+ it('counts blocking findings from their own event', () => {
110
+ const report = buildAdoptionReport([
111
+ line('blocking_finding_posted', { repo: 'acme/web', verdict: 'BLOCK' }),
112
+ line('blocking_finding_posted', { repo: 'acme/api', verdict: 'NEEDS WORK' }),
113
+ ], PERIOD);
114
+ expect(report.activity.blocking_findings_posted).toBe(2);
115
+ });
116
+ it('reports an empty shape rather than throwing on no logs', () => {
117
+ const report = buildAdoptionReport([], PERIOD);
118
+ expect(report.activity.reviews_completed).toBe(0);
119
+ expect(report.weekly).toEqual([]);
120
+ expect(report.open_to_verdict).toEqual({ count: 0, p50_ms: 0, p90_ms: 0, max_ms: 0, unmeasured: 0 });
121
+ expect(report.first_run_failures).toEqual({});
122
+ });
123
+ });
124
+ describe('firstRunFailures', () => {
125
+ it('attributes a session that never reached a verdict to its first error', () => {
126
+ const failures = firstRunFailures([
127
+ line('session_start', { command: 'run' }),
128
+ line('error', { category: 'auth' }),
129
+ line('error', { category: 'network' }),
130
+ line('session_end'),
131
+ ]);
132
+ expect(failures).toEqual({ auth: 1 });
133
+ });
134
+ it('ignores errors in a session that did complete a review', () => {
135
+ const failures = firstRunFailures([
136
+ line('session_start', { command: 'watch' }),
137
+ line('review_complete', { repo: 'acme/web', verdict: 'APPROVE' }),
138
+ line('error', { category: 'rate_limit' }),
139
+ line('session_end'),
140
+ ]);
141
+ expect(failures).toEqual({});
142
+ });
143
+ it('keeps sessions separate when one fails and the next succeeds', () => {
144
+ const failures = firstRunFailures([
145
+ line('session_start', { command: 'run' }),
146
+ line('error', { category: 'auth' }),
147
+ line('session_end'),
148
+ line('session_start', { command: 'run' }),
149
+ line('review_complete', { repo: 'acme/web', verdict: 'BLOCK' }),
150
+ line('session_end'),
151
+ ]);
152
+ expect(failures).toEqual({ auth: 1 });
153
+ });
154
+ it('counts a session that crashed without a session_end', () => {
155
+ const failures = firstRunFailures([
156
+ line('session_start', { command: 'watch' }),
157
+ line('error', { category: 'timeout' }),
158
+ ]);
159
+ expect(failures).toEqual({ timeout: 1 });
160
+ });
161
+ it('labels an uncategorised error rather than dropping it', () => {
162
+ const failures = firstRunFailures([
163
+ line('session_start', { command: 'run' }),
164
+ line('error', {}),
165
+ ]);
166
+ expect(failures).toEqual({ unknown: 1 });
167
+ });
168
+ it('still attributes errors in logs written before session events existed', () => {
169
+ const failures = firstRunFailures([
170
+ line('error', { category: 'git' }),
171
+ ]);
172
+ expect(failures).toEqual({ git: 1 });
173
+ });
174
+ });
175
+ //# sourceMappingURL=adoption.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"adoption.test.js","sourceRoot":"","sources":["../../src/__tests__/adoption.test.ts"],"names":[],"mappings":"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"}
@@ -1,7 +1,9 @@
1
1
  import { describe, it, expect, vi } from 'vitest';
2
2
  import * as detector from '../github/detector.js';
3
- import { detectOriginFromBody, detectOriginFromBranch, detectOriginFull, assignReviewer } from '../github/detector.js';
3
+ import { detectOriginFromBody, detectOriginFromBranch, detectOriginFromCommits, detectOriginFull, assignReviewer } from '../github/detector.js';
4
4
  import { ConfigSchema } from '../config/schema.js';
5
+ import { buildAttributionFooter } from '../lib/comment-bodies.js';
6
+ import { buildCommitTrailers } from '../lib/annotation.js';
5
7
  function buildConfig(overrides = {}) {
6
8
  return ConfigSchema.parse({
7
9
  mode: 'cross-vendor',
@@ -23,6 +25,90 @@ describe('detectOriginFromBody', () => {
23
25
  expect(detectOriginFromBody('just a normal PR body', buildConfig())).toBeNull();
24
26
  });
25
27
  });
28
+ // Crosscheck was blind to the PRs and commits it writes itself: its fix-PR body
29
+ // says "Fixed with [Claude Code]", not "Generated with [Claude Code]", and its
30
+ // commits carry Crosscheck-* trailers rather than Co-Authored-By. Such a PR
31
+ // detected as origin:human, which skipped conflict-resolve for want of a vendor.
32
+ // These build their fixtures with the real producers so the patterns cannot
33
+ // drift away from the strings crosscheck actually emits.
34
+ describe('origin detection recognises crosscheck-authored work', () => {
35
+ const fixPrBody = (vendor) => [
36
+ 'Auto-fix by crosscheck for CR issues found in #2444.',
37
+ '',
38
+ 'Review: https://github.com/owner/repo/pull/2444',
39
+ '',
40
+ buildAttributionFooter({ action: 'Fixed', vendor, model: 'claude-sonnet-5', effort: 'high' }),
41
+ ].join('\n');
42
+ it('detects claude origin from a crosscheck fix-PR body', () => {
43
+ expect(detectOriginFromBody(fixPrBody('claude'), buildConfig())).toBe('claude');
44
+ });
45
+ it('detects codex origin from a crosscheck fix-PR body', () => {
46
+ expect(detectOriginFromBody(fixPrBody('codex'), buildConfig())).toBe('codex');
47
+ });
48
+ it('detects claude origin from a crosscheck commit trailer', () => {
49
+ const message = [
50
+ '[crosscheck] fix: apply CR fixes from review of PR #2444',
51
+ '',
52
+ buildCommitTrailers({ reviewer: 'claude', model: 'claude-sonnet-5', step: 'fix', service: 'crosscheck' }),
53
+ ].join('\n');
54
+ expect(detectOriginFromCommits([message], buildConfig())).toBe('claude');
55
+ });
56
+ it('detects codex origin from a crosscheck commit trailer', () => {
57
+ const message = [
58
+ '[crosscheck] fix: apply CR fixes from review of PR #2444',
59
+ '',
60
+ buildCommitTrailers({ reviewer: 'codex', model: 'gpt-5.6-sol', step: 'fix', service: 'crosscheck' }),
61
+ ].join('\n');
62
+ expect(detectOriginFromCommits([message], buildConfig())).toBe('codex');
63
+ });
64
+ // The load-bearing negative: reviewing a PR says nothing about who wrote it.
65
+ // A pattern loose enough to match "Reviewed with [Claude Code]" would route
66
+ // every claude-reviewed PR back to codex as if claude had authored it.
67
+ it('does not treat a review attribution as authorship', () => {
68
+ const reviewFooter = buildAttributionFooter({ action: 'Reviewed', vendor: 'claude', model: 'opus' });
69
+ expect(detectOriginFromBody(reviewFooter, buildConfig())).toBeNull();
70
+ });
71
+ // 'Attempted' is the footer on a fix that failed — no code was produced, so it
72
+ // is no evidence of authorship either.
73
+ it('does not treat a failed fix attribution as authorship', () => {
74
+ const attemptedFooter = buildAttributionFooter({ action: 'Attempted', vendor: 'claude', model: 'opus' });
75
+ expect(detectOriginFromBody(attemptedFooter, buildConfig())).toBeNull();
76
+ });
77
+ it('leaves an ordinary human PR body undetected', () => {
78
+ expect(detectOriginFromBody('Fixed the crosscheck config so the reviewer picks up.', buildConfig())).toBeNull();
79
+ });
80
+ // Recognising crosscheck's own output is a fact about that output, not a
81
+ // routing preference, so it does not live in routing.*_reviews_patterns: Zod
82
+ // defaults apply only when a field is absent, so an install that pinned its
83
+ // own pattern list would never receive these and would keep reading its own
84
+ // fix PRs as human — the exact misclassification this fixes.
85
+ const pinnedPatterns = buildConfig({
86
+ routing: { codex_reviews_patterns: ['^ONLY-THIS$'], claude_reviews_patterns: ['^ONLY-THAT$'] },
87
+ });
88
+ it('recognises its own fix-PR body even when the pattern lists are pinned', () => {
89
+ expect(detectOriginFromBody(fixPrBody('claude'), pinnedPatterns)).toBe('claude');
90
+ expect(detectOriginFromBody(fixPrBody('codex'), pinnedPatterns)).toBe('codex');
91
+ });
92
+ it('recognises its own commit trailer even when the pattern lists are pinned', () => {
93
+ const message = (vendor) => [
94
+ '[crosscheck] fix: apply CR fixes from review of PR #2444',
95
+ '',
96
+ buildCommitTrailers({ reviewer: vendor, model: 'm', step: 'fix', service: 'crosscheck' }),
97
+ ].join('\n');
98
+ expect(detectOriginFromCommits([message('claude')], pinnedPatterns)).toBe('claude');
99
+ expect(detectOriginFromCommits([message('codex')], pinnedPatterns)).toBe('codex');
100
+ });
101
+ it('still lets a configured pattern win over the self-attribution', () => {
102
+ // A user's own pattern is explicit routing intent and is checked first.
103
+ const cfg = buildConfig({ routing: { claude_reviews_patterns: ['Auto-fix by crosscheck'] } });
104
+ expect(detectOriginFromBody(fixPrBody('claude'), cfg)).toBe('codex');
105
+ });
106
+ it('does not read a Crosscheck-Reviewer mention inside prose as a trailer', () => {
107
+ // The trailer is a line of its own. Quoting the field name in a PR body —
108
+ // this repo's own docs and changelog do exactly that — must not attribute it.
109
+ expect(detectOriginFromBody('The commit carries a Crosscheck-Reviewer: claude trailer, which we now read.', buildConfig())).toBeNull();
110
+ });
111
+ });
26
112
  describe('detectOriginFromBranch', () => {
27
113
  it('detects claude origin from claude/ branch prefix', () => {
28
114
  expect(detectOriginFromBranch('claude/feat-foo', buildConfig())).toBe('claude');
@@ -1 +1 @@
1
- {"version":3,"file":"detector.test.js","sourceRoot":"","sources":["../../src/__tests__/detector.test.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"detector.test.js","sourceRoot":"","sources":["../../src/__tests__/detector.test.ts"],"names":[],"mappings":"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"}
@@ -3,7 +3,7 @@ import { execFileSync } from 'child_process';
3
3
  import { mkdtempSync, rmSync, writeFileSync } from 'fs';
4
4
  import { tmpdir } from 'os';
5
5
  import { join } from 'path';
6
- import { isRetryableFixError, getEffectiveStepType, exceedsMaxRounds, anyFixApplied, countCrosscheckCommitsForPR, countCrosscheckCommitsForPRDetailed, buildWorkflowCompleteEvent, resolveFixVendor, resolveFixLanding, } from '../lib/runner.js';
6
+ import { isRetryableFixError, getEffectiveStepType, exceedsMaxRounds, anyFixApplied, countCrosscheckCommitsForPR, countCrosscheckCommitsForPRDetailed, buildWorkflowCompleteEvent, resolveFixVendor, resolveConflictResolveVendor, summariseStepOutcomes, mergeStepOutcomes, resolveFixLanding, } from '../lib/runner.js';
7
7
  describe('isRetryableFixError', () => {
8
8
  it('returns false for auth failure errors', () => {
9
9
  expect(isRetryableFixError(new Error('claude auth failure during fix step — run: claude auth login'))).toBe(false);
@@ -384,4 +384,164 @@ describe('resolveFixVendor', () => {
384
384
  });
385
385
  });
386
386
  });
387
+ describe('resolveConflictResolveVendor', () => {
388
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
389
+ const cfg = (claudeEnabled, codexEnabled, fallbackReviewer = 'auto') => ({
390
+ vendors: {
391
+ claude: { enabled: claudeEnabled },
392
+ codex: { enabled: codexEnabled },
393
+ },
394
+ routing: { fallback_reviewer: fallbackReviewer },
395
+ });
396
+ describe('human-origin fallback — the no_vendor regression', () => {
397
+ // The default workflow gives conflict-resolve `reviewer: origin`. A PR whose
398
+ // origin cannot be attributed resolved to null and skipped with 'no_vendor',
399
+ // even with routing.fallback_reviewer set — the fix step honoured it, this
400
+ // step did not.
401
+ it('explicit claude: resolves instead of skipping with no_vendor', () => {
402
+ expect(resolveConflictResolveVendor('origin', 'human', cfg(true, true, 'claude')))
403
+ .toEqual({ vendor: 'claude', usedHumanFallback: true });
404
+ });
405
+ it('auto: picks claude even with codex enabled — codex cannot resolve conflicts', () => {
406
+ // Diverges from resolveFixVendor's codex-first auto path on purpose: routing
407
+ // to codex here only trades a 'no_vendor' skip for a
408
+ // 'codex_conflict_resolve_unsupported' one.
409
+ expect(resolveConflictResolveVendor('origin', 'human', cfg(true, true)))
410
+ .toEqual({ vendor: 'claude', usedHumanFallback: true });
411
+ });
412
+ it('auto with claude disabled: no vendor supports the step', () => {
413
+ expect(resolveConflictResolveVendor('origin', 'human', cfg(false, true)))
414
+ .toEqual({ vendor: null, usedHumanFallback: false });
415
+ });
416
+ it('explicit codex: honoured, so the caller reports the precise unsupported skip', () => {
417
+ // An explicit fallback_reviewer is an operator decision. Silently swapping it
418
+ // for claude would hide a misconfiguration behind a step that works anyway.
419
+ expect(resolveConflictResolveVendor('origin', 'human', cfg(true, true, 'codex')))
420
+ .toEqual({ vendor: 'codex', usedHumanFallback: true });
421
+ });
422
+ it('null fallback_reviewer: still skips — opting out stays opt-out', () => {
423
+ expect(resolveConflictResolveVendor('origin', 'human', cfg(true, true, null)))
424
+ .toEqual({ vendor: null, usedHumanFallback: false });
425
+ });
426
+ it('explicit claude but claude disabled: no fallback invented', () => {
427
+ expect(resolveConflictResolveVendor('origin', 'human', cfg(false, true, 'claude')))
428
+ .toEqual({ vendor: null, usedHumanFallback: false });
429
+ });
430
+ it('returns null when both vendors disabled regardless of fallback_reviewer', () => {
431
+ expect(resolveConflictResolveVendor('origin', 'human', cfg(false, false)))
432
+ .toEqual({ vendor: null, usedHumanFallback: false });
433
+ });
434
+ });
435
+ describe('scoped to reviewer:origin only', () => {
436
+ it('reviewer:claude with human origin resolves directly, no fallback', () => {
437
+ expect(resolveConflictResolveVendor('claude', 'human', cfg(true, true)))
438
+ .toEqual({ vendor: 'claude', usedHumanFallback: false });
439
+ });
440
+ it('reviewer:auto with human origin uses the resolveReviewer auto path unchanged', () => {
441
+ // 'auto' encodes explicit intent, so it keeps resolveReviewer's codex-first
442
+ // path and the caller skips with codex_conflict_resolve_unsupported.
443
+ expect(resolveConflictResolveVendor('auto', 'human', cfg(true, true)))
444
+ .toEqual({ vendor: 'codex', usedHumanFallback: false });
445
+ });
446
+ });
447
+ describe('attributed origins', () => {
448
+ it('claude origin resolves to claude', () => {
449
+ expect(resolveConflictResolveVendor('origin', 'claude', cfg(true, true)))
450
+ .toEqual({ vendor: 'claude', usedHumanFallback: false });
451
+ });
452
+ // Regression (#284): `Crosscheck-Reviewer: codex` detection makes a
453
+ // crosscheck-authored Codex fix PR resolve to origin 'codex'. Conflict
454
+ // resolution is Claude-only, so returning codex would skip the step and leave
455
+ // the PR's conflicts unresolved (before #284 these PRs were 'human' and the
456
+ // auto fallback picked Claude). Because the assignment came from origin
457
+ // detection, substitute the capable vendor rather than skip.
458
+ it('codex origin substitutes claude — codex cannot resolve conflicts', () => {
459
+ expect(resolveConflictResolveVendor('origin', 'codex', cfg(true, true)))
460
+ .toEqual({ vendor: 'claude', usedHumanFallback: false, substitutedOriginVendor: 'codex' });
461
+ });
462
+ it('codex origin with claude disabled: no capable vendor, stays codex so the caller reports the precise skip', () => {
463
+ expect(resolveConflictResolveVendor('origin', 'codex', cfg(false, true)))
464
+ .toEqual({ vendor: 'codex', usedHumanFallback: false });
465
+ });
466
+ });
467
+ describe('smartSwitchFallback takes precedence', () => {
468
+ it('resolves via resolveReviewer before the human-origin branch fires', () => {
469
+ expect(resolveConflictResolveVendor('origin', 'human', cfg(true, true, 'claude'), 'codex'))
470
+ .toEqual({ vendor: 'codex', usedHumanFallback: false });
471
+ });
472
+ });
473
+ });
474
+ describe('summariseStepOutcomes', () => {
475
+ const results = (entries) => entries;
476
+ it('separates steps that ran from steps that skipped', () => {
477
+ const out = summariseStepOutcomes(['review', 'fix', 'recheck'], results({
478
+ review: { verdict: 'BLOCK' },
479
+ fix: { applied_count: 2 },
480
+ recheck: { skipped: true, skipReason: 'when_condition' },
481
+ }));
482
+ expect(out.ran).toEqual(['review', 'fix']);
483
+ expect(out.skipped).toEqual([{ step: 'recheck', reason: 'when_condition' }]);
484
+ });
485
+ // The reported defect: `crosscheck resolve` dispatches conflict-resolve alone,
486
+ // it skips for want of a vendor, and verdict is null either way — so only the
487
+ // empty `ran` list distinguishes this from a review that found nothing.
488
+ it('reports an empty ran list when every dispatched step skipped', () => {
489
+ const out = summariseStepOutcomes(['conflict-resolve'], results({
490
+ 'conflict-resolve': { skipped: true, skipReason: 'no_vendor' },
491
+ }));
492
+ expect(out.ran).toEqual([]);
493
+ expect(out.skipped).toEqual([{ step: 'conflict-resolve', reason: 'no_vendor' }]);
494
+ });
495
+ it('counts a dispatched step with no recorded result as ran', () => {
496
+ // Absence of a result is not evidence of a skip; only an explicit skip is.
497
+ // Guessing the other way would report a working run as a no-op.
498
+ const out = summariseStepOutcomes(['review'], results({}));
499
+ expect(out.ran).toEqual(['review']);
500
+ expect(out.skipped).toEqual([]);
501
+ });
502
+ it('falls back to "unknown" when a skip recorded no reason', () => {
503
+ const out = summariseStepOutcomes(['fix'], results({ fix: { skipped: true } }));
504
+ expect(out.skipped).toEqual([{ step: 'fix', reason: 'unknown' }]);
505
+ });
506
+ it('deduplicates a step dispatched more than once', () => {
507
+ const out = summariseStepOutcomes(['fix', 'fix'], results({ fix: { applied_count: 1 } }));
508
+ expect(out.ran).toEqual(['fix']);
509
+ });
510
+ it('returns empty lists when no step was dispatched', () => {
511
+ expect(summariseStepOutcomes([], results({}))).toEqual({ ran: [], skipped: [] });
512
+ });
513
+ });
514
+ describe('mergeStepOutcomes', () => {
515
+ // The crazy/halfcrazy loops re-enter runWorkflow, so the completion line has to
516
+ // reflect the whole invocation. Reporting only the first round called a run
517
+ // that later applied a fix "no step ran".
518
+ it('counts a step that ran in a later round as ran', () => {
519
+ const merged = mergeStepOutcomes({ ran: [], skipped: [{ step: 'fix', reason: 'no_review_comment' }] }, { ran: ['fix'], skipped: [] });
520
+ expect(merged).toEqual({ ran: ['fix'], skipped: [] });
521
+ });
522
+ it('keeps a step skipped when it skipped in every round', () => {
523
+ const merged = mergeStepOutcomes({ ran: [], skipped: [{ step: 'conflict-resolve', reason: 'no_vendor' }] }, { ran: [], skipped: [{ step: 'conflict-resolve', reason: 'no_vendor' }] });
524
+ expect(merged?.skipped).toEqual([{ step: 'conflict-resolve', reason: 'no_vendor' }]);
525
+ });
526
+ // Listing one step twice reads as two distinct problems.
527
+ it('reports a repeatedly skipped step once, with its latest reason', () => {
528
+ const merged = mergeStepOutcomes({ ran: [], skipped: [{ step: 'fix', reason: 'when_condition' }] }, { ran: [], skipped: [{ step: 'fix', reason: 'no_vendor' }] });
529
+ expect(merged?.skipped).toEqual([{ step: 'fix', reason: 'no_vendor' }]);
530
+ });
531
+ it('does not duplicate a step that ran in both rounds', () => {
532
+ const merged = mergeStepOutcomes({ ran: ['review'], skipped: [] }, { ran: ['review'], skipped: [] });
533
+ expect(merged?.ran).toEqual(['review']);
534
+ });
535
+ it('keeps first-seen ordering across both sides', () => {
536
+ const merged = mergeStepOutcomes({ ran: ['review'], skipped: [{ step: 'fix', reason: 'when_condition' }] }, { ran: ['recheck'], skipped: [{ step: 'conflict-resolve', reason: 'no_conflicts' }] });
537
+ expect(merged?.ran).toEqual(['review', 'recheck']);
538
+ expect(merged?.skipped.map(s => s.step)).toEqual(['fix', 'conflict-resolve']);
539
+ });
540
+ it('passes through when either side is absent', () => {
541
+ const only = { ran: ['review'], skipped: [] };
542
+ expect(mergeStepOutcomes(undefined, only)).toBe(only);
543
+ expect(mergeStepOutcomes(only, undefined)).toBe(only);
544
+ expect(mergeStepOutcomes(undefined, undefined)).toBeUndefined();
545
+ });
546
+ });
387
547
  //# sourceMappingURL=runner.test.js.map