@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.
- package/README.md +1 -1
- package/crosscheck.config.example.yml +9 -0
- package/dist/__tests__/adoption.test.d.ts +2 -0
- package/dist/__tests__/adoption.test.d.ts.map +1 -0
- package/dist/__tests__/adoption.test.js +175 -0
- package/dist/__tests__/adoption.test.js.map +1 -0
- package/dist/__tests__/detector.test.js +87 -1
- package/dist/__tests__/detector.test.js.map +1 -1
- package/dist/__tests__/runner.test.js +161 -1
- package/dist/__tests__/runner.test.js.map +1 -1
- package/dist/cli.js +8 -0
- package/dist/cli.js.map +1 -1
- package/dist/commands/adoption.d.ts +6 -0
- package/dist/commands/adoption.d.ts.map +1 -0
- package/dist/commands/adoption.js +132 -0
- package/dist/commands/adoption.js.map +1 -0
- package/dist/commands/onboard.d.ts.map +1 -1
- package/dist/commands/onboard.js +30 -3
- package/dist/commands/onboard.js.map +1 -1
- package/dist/commands/run.d.ts.map +1 -1
- package/dist/commands/run.js +30 -2
- package/dist/commands/run.js.map +1 -1
- package/dist/commands/watch.d.ts.map +1 -1
- package/dist/commands/watch.js +4 -0
- package/dist/commands/watch.js.map +1 -1
- package/dist/config/schema.d.ts.map +1 -1
- package/dist/config/schema.js +7 -0
- package/dist/config/schema.js.map +1 -1
- package/dist/github/detector.d.ts.map +1 -1
- package/dist/github/detector.js +28 -1
- package/dist/github/detector.js.map +1 -1
- package/dist/github/webhook.d.ts +1 -0
- package/dist/github/webhook.d.ts.map +1 -1
- package/dist/github/webhook.js.map +1 -1
- package/dist/lib/adoption.d.ts +64 -0
- package/dist/lib/adoption.d.ts.map +1 -0
- package/dist/lib/adoption.js +165 -0
- package/dist/lib/adoption.js.map +1 -0
- package/dist/lib/runner.d.ts +24 -0
- package/dist/lib/runner.d.ts.map +1 -1
- package/dist/lib/runner.js +115 -16
- package/dist/lib/runner.js.map +1 -1
- package/dist/lib/workflow.d.ts +4 -0
- package/dist/lib/workflow.d.ts.map +1 -1
- package/dist/lib/workflow.js.map +1 -1
- package/docs/metrics.md +115 -0
- package/get-started.md +67 -0
- 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 @@
|
|
|
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":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EACL,mBAAmB,EACnB,gBAAgB,EAChB,aAAa,EACb,UAAU,EACV,iBAAiB,GAElB,MAAM,oBAAoB,CAAA;AAE3B,MAAM,MAAM,GAAG,EAAE,IAAI,EAAE,YAAY,EAAE,EAAE,EAAE,YAAY,EAAE,SAAS,EAAE,CAAC,EAAE,CAAA;AAErE,SAAS,IAAI,CAAC,KAAa,EAAE,SAAmC,EAAE;IAChE,OAAO,EAAE,EAAE,EAAE,0BAA0B,EAAE,KAAK,EAAE,GAAG,MAAM,EAAE,CAAA;AAC7D,CAAC;AAED,QAAQ,CAAC,mBAAmB,EAAE,GAAG,EAAE;IACjC,MAAM,MAAM,GAAG,0BAA0B,CAAA;IACzC,MAAM,GAAG,GAAG,IAAI,CAAC,KAAK,CAAC,0BAA0B,CAAC,CAAA;IAElD,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,CAAC,iBAAiB,CAAC,MAAM,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,GAAG,IAAI,CAAC,CAAA;IACxE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,CAAC,iBAAiB,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;IAC9D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0DAA0D,EAAE,GAAG,EAAE;QAClE,MAAM,CAAC,iBAAiB,CAAC,SAAS,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;IACpE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gEAAgE,EAAE,GAAG,EAAE;QACxE,MAAM,CAAC,iBAAiB,CAAC,cAAc,EAAE,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;IACzE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,CAAC,iBAAiB,CAAC,0BAA0B,EAAE,SAAS,EAAE,GAAG,CAAC,CAAC,CAAC,aAAa,EAAE,CAAA;IACvF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,eAAe,EAAE,GAAG,EAAE;IAC7B,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,CAAC,aAAa,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IACtE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,CAAC,aAAa,CAAC,0BAA0B,CAAC,CAAC,CAAC,IAAI,CAAC,YAAY,CAAC,CAAA;IACtE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,aAAa,CAAC,YAAY,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IAChD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,YAAY,EAAE,GAAG,EAAE;IAC1B,EAAE,CAAC,sDAAsD,EAAE,GAAG,EAAE;QAC9D,MAAM,MAAM,GAAG,CAAC,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,EAAE,CAAC,CAAA;QACnC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;QACxC,MAAM,CAAC,UAAU,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+BAA+B,EAAE,GAAG,EAAE;QACvC,MAAM,CAAC,UAAU,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACrC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,qBAAqB,EAAE,GAAG,EAAE;IACnC,EAAE,CAAC,6EAA6E,EAAE,GAAG,EAAE;QACrF,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,IAAI,CAAC,iBAAiB,CAAC;YACvB,IAAI,CAAC,mBAAmB,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;YACnD,IAAI,CAAC,iBAAiB,CAAC;YACvB,IAAI,CAAC,mBAAmB,EAAE,EAAE,OAAO,EAAE,WAAW,EAAE,CAAC;YACnD,IAAI,CAAC,iBAAiB,CAAC;SACxB,EAAE,MAAM,CAAC,CAAA;QAEV,MAAM,CAAC,MAAM,CAAC,UAAU,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,SAAS,EAAE,CAAC,EAAE,CAAC,CAAA;IAC/E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,QAAQ,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YACpF,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,SAAS,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;SACxF,EAAE,MAAM,CAAC,CAAA;QAEV,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,kBAAkB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACpD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;YACjE,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YAC/D,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;SAClE,EAAE,MAAM,CAAC,CAAA;QAEV,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAC5C,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,CAAC,EAAE,IAAI,EAAE,YAAY,EAAE,YAAY,EAAE,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAC,CAAA;IACtF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,IAAI,CAAC,iBAAiB,EAAE,EAAE,EAAE,EAAE,0BAA0B,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;YACjG,IAAI,CAAC,iBAAiB,EAAE,EAAE,EAAE,EAAE,0BAA0B,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;SAClG,EAAE,MAAM,CAAC,CAAA;QAEV,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,OAAO,CAAC,CAAC,YAAY,EAAE,YAAY,CAAC,CAAC,CAAA;IAC9E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,yCAAyC,EAAE,GAAG,EAAE;QACjD,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,IAAI,CAAC,cAAc,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,cAAc,EAAE,EAAE,aAAa,EAAE,CAAC,EAAE,CAAC;YAC1C,IAAI,CAAC,cAAc,EAAE,EAAE,CAAC;SACzB,EAAE,MAAM,CAAC,CAAA;QAEV,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,aAAa,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2EAA2E,EAAE,GAAG,EAAE;QACnF,MAAM,MAAM,GAAG,MAAM,CAAA;QACrB,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,EAAE,GAAG,MAAM,EAAE,CAAC;YAClG,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,kBAAkB,EAAE,EAAE,GAAG,MAAM,EAAE,CAAC;YAChG,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,kBAAkB,EAAE,GAAG,GAAG,MAAM,EAAE,CAAC;YACnG,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;SAClE,EAAE,MAAM,CAAC,CAAA;QAEV,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,KAAK,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QAC5C,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,EAAE,GAAG,MAAM,CAAC,CAAA;QACvD,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,GAAG,GAAG,MAAM,CAAC,CAAA;QACxD,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2DAA2D,EAAE,GAAG,EAAE;QACnE,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;SAC9C,EAAE,MAAM,CAAC,CAAA;QAEV,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IACnD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,+CAA+C,EAAE,GAAG,EAAE;QACvD,MAAM,MAAM,GAAG,mBAAmB,CAAC;YACjC,IAAI,CAAC,yBAAyB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YACvE,IAAI,CAAC,yBAAyB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,YAAY,EAAE,CAAC;SAC7E,EAAE,MAAM,CAAC,CAAA;QAEV,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,wBAAwB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;IAC1D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,MAAM,GAAG,mBAAmB,CAAC,EAAE,EAAE,MAAM,CAAC,CAAA;QAE9C,MAAM,CAAC,MAAM,CAAC,QAAQ,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAA;QACjD,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;QACjC,MAAM,CAAC,MAAM,CAAC,eAAe,CAAC,CAAC,OAAO,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,UAAU,EAAE,CAAC,EAAE,CAAC,CAAA;QACpG,MAAM,CAAC,MAAM,CAAC,kBAAkB,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC/C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,kBAAkB,EAAE,GAAG,EAAE;IAChC,EAAE,CAAC,sEAAsE,EAAE,GAAG,EAAE;QAC9E,MAAM,QAAQ,GAAG,gBAAgB,CAAC;YAChC,IAAI,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YACzC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;YACnC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;YACtC,IAAI,CAAC,aAAa,CAAC;SACpB,CAAC,CAAA;QAEF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wDAAwD,EAAE,GAAG,EAAE;QAChE,MAAM,QAAQ,GAAG,gBAAgB,CAAC;YAChC,IAAI,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YAC3C,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC;YACjE,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,YAAY,EAAE,CAAC;YACzC,IAAI,CAAC,aAAa,CAAC;SACpB,CAAC,CAAA;QAEF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,CAAC,CAAA;IAC9B,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8DAA8D,EAAE,GAAG,EAAE;QACtE,MAAM,QAAQ,GAAG,gBAAgB,CAAC;YAChC,IAAI,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YACzC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,MAAM,EAAE,CAAC;YACnC,IAAI,CAAC,aAAa,CAAC;YACnB,IAAI,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YACzC,IAAI,CAAC,iBAAiB,EAAE,EAAE,IAAI,EAAE,UAAU,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YAC/D,IAAI,CAAC,aAAa,CAAC;SACpB,CAAC,CAAA;QAEF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,IAAI,EAAE,CAAC,EAAE,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qDAAqD,EAAE,GAAG,EAAE;QAC7D,MAAM,QAAQ,GAAG,gBAAgB,CAAC;YAChC,IAAI,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,OAAO,EAAE,CAAC;YAC3C,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,SAAS,EAAE,CAAC;SACvC,CAAC,CAAA;QAEF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uDAAuD,EAAE,GAAG,EAAE;QAC/D,MAAM,QAAQ,GAAG,gBAAgB,CAAC;YAChC,IAAI,CAAC,eAAe,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,CAAC;YACzC,IAAI,CAAC,OAAO,EAAE,EAAE,CAAC;SAClB,CAAC,CAAA;QAEF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,OAAO,EAAE,CAAC,EAAE,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,uEAAuE,EAAE,GAAG,EAAE;QAC/E,MAAM,QAAQ,GAAG,gBAAgB,CAAC;YAChC,IAAI,CAAC,OAAO,EAAE,EAAE,QAAQ,EAAE,KAAK,EAAE,CAAC;SACnC,CAAC,CAAA;QAEF,MAAM,CAAC,QAAQ,CAAC,CAAC,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC,EAAE,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
|
|
@@ -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":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,QAAQ,CAAA;AACjD,OAAO,KAAK,QAAQ,MAAM,uBAAuB,CAAA;AACjD,OAAO,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;
|
|
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
|