@motivation-labs/crosscheck 0.13.0-beta.7 → 0.13.1-beta.6

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 (66) hide show
  1. package/crosscheck.config.example.yml +6 -1
  2. package/dist/__tests__/board.test.js +115 -2
  3. package/dist/__tests__/board.test.js.map +1 -1
  4. package/dist/__tests__/conflict-resolve.test.d.ts +2 -0
  5. package/dist/__tests__/conflict-resolve.test.d.ts.map +1 -0
  6. package/dist/__tests__/conflict-resolve.test.js +123 -0
  7. package/dist/__tests__/conflict-resolve.test.js.map +1 -0
  8. package/dist/__tests__/detector.test.d.ts +2 -0
  9. package/dist/__tests__/detector.test.d.ts.map +1 -0
  10. package/dist/__tests__/detector.test.js +108 -0
  11. package/dist/__tests__/detector.test.js.map +1 -0
  12. package/dist/__tests__/onboard-preservation.test.js +67 -1
  13. package/dist/__tests__/onboard-preservation.test.js.map +1 -1
  14. package/dist/__tests__/pr-lock.test.js +65 -3
  15. package/dist/__tests__/pr-lock.test.js.map +1 -1
  16. package/dist/__tests__/runner.test.js +72 -2
  17. package/dist/__tests__/runner.test.js.map +1 -1
  18. package/dist/cli.js +4 -2
  19. package/dist/cli.js.map +1 -1
  20. package/dist/commands/init.d.ts.map +1 -1
  21. package/dist/commands/init.js +11 -9
  22. package/dist/commands/init.js.map +1 -1
  23. package/dist/commands/onboard.d.ts +2 -0
  24. package/dist/commands/onboard.d.ts.map +1 -1
  25. package/dist/commands/onboard.js +53 -6
  26. package/dist/commands/onboard.js.map +1 -1
  27. package/dist/commands/run.d.ts.map +1 -1
  28. package/dist/commands/run.js +124 -61
  29. package/dist/commands/run.js.map +1 -1
  30. package/dist/commands/serve.d.ts.map +1 -1
  31. package/dist/commands/serve.js +15 -0
  32. package/dist/commands/serve.js.map +1 -1
  33. package/dist/commands/watch.d.ts.map +1 -1
  34. package/dist/commands/watch.js +17 -0
  35. package/dist/commands/watch.js.map +1 -1
  36. package/dist/config/schema.d.ts.map +1 -1
  37. package/dist/config/schema.js +5 -0
  38. package/dist/config/schema.js.map +1 -1
  39. package/dist/github/client.d.ts +2 -1
  40. package/dist/github/client.d.ts.map +1 -1
  41. package/dist/github/client.js +55 -3
  42. package/dist/github/client.js.map +1 -1
  43. package/dist/github/detector.d.ts.map +1 -1
  44. package/dist/github/detector.js +18 -1
  45. package/dist/github/detector.js.map +1 -1
  46. package/dist/lib/board.d.ts +5 -0
  47. package/dist/lib/board.d.ts.map +1 -1
  48. package/dist/lib/board.js +128 -128
  49. package/dist/lib/board.js.map +1 -1
  50. package/dist/lib/pr-lock.d.ts +1 -0
  51. package/dist/lib/pr-lock.d.ts.map +1 -1
  52. package/dist/lib/pr-lock.js +28 -13
  53. package/dist/lib/pr-lock.js.map +1 -1
  54. package/dist/lib/runner.d.ts +2 -0
  55. package/dist/lib/runner.d.ts.map +1 -1
  56. package/dist/lib/runner.js +492 -211
  57. package/dist/lib/runner.js.map +1 -1
  58. package/dist/lib/workflow.d.ts +10 -8
  59. package/dist/lib/workflow.d.ts.map +1 -1
  60. package/dist/lib/workflow.js +7 -1
  61. package/dist/lib/workflow.js.map +1 -1
  62. package/dist/reviewers/conflict-resolve.d.ts +15 -0
  63. package/dist/reviewers/conflict-resolve.d.ts.map +1 -0
  64. package/dist/reviewers/conflict-resolve.js +218 -0
  65. package/dist/reviewers/conflict-resolve.js.map +1 -0
  66. package/package.json +1 -1
@@ -101,9 +101,14 @@ routing:
101
101
  # - my-org-bot # a dedicated bot account
102
102
 
103
103
  # Author-based routing fallback — maps GitHub login → vendor origin.
104
- # Used when no body pattern matches. Lets you skip the attribution footer requirement.
104
+ # Used when no body / commit / branch pattern matches. Lets you skip the attribution
105
+ # footer requirement when an author consistently uses a single agent.
105
106
  # beingzy: claude → PRs from beingzy treated as Claude-authored → Codex reviews them
106
107
  # my-bot: codex → PRs from my-bot treated as Codex-authored → Claude reviews them
108
+ # NOTE: in `mode: cross-vendor` with BOTH vendors enabled, this map is bypassed —
109
+ # if the same author commits via both agents, a static route is structurally wrong.
110
+ # In that case, rely on attribution detection (body / commits / branch) and
111
+ # `fallback_reviewer` below for PRs with no attribution markers.
107
112
  # author_routes:
108
113
  # beingzy: claude
109
114
 
@@ -1,5 +1,5 @@
1
- import { describe, it, expect } from 'vitest';
2
- import { fmtTokens } from '../lib/board.js';
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import { PRBoard, fmtTokens } from '../lib/board.js';
3
3
  describe('fmtTokens', () => {
4
4
  it('returns empty string for undefined', () => {
5
5
  expect(fmtTokens(undefined)).toBe('');
@@ -33,4 +33,117 @@ describe('fmtTokens', () => {
33
33
  expect(fmtTokens(2_000_000)).toBe('(2M)');
34
34
  });
35
35
  });
36
+ // ── PRBoard rendering + retention ───────────────────────────────────────────
37
+ const baseConfig = {
38
+ mode: 'crosscheck',
39
+ quality: { tier: 'balanced' },
40
+ vendors: { claude: { enabled: true }, codex: { enabled: false } },
41
+ display: {
42
+ theme: {
43
+ bar_fill: 'blue',
44
+ bar_empty: 'dim',
45
+ cr_approve: 'green',
46
+ cr_needs_work: 'yellow',
47
+ cr_block: 'red',
48
+ fix_fill: 'cyan',
49
+ },
50
+ },
51
+ };
52
+ const stripAnsi = (s) => s.replace(/\x1B\[[0-9;]*m/g, '');
53
+ const reviewStep = {
54
+ type: 'review',
55
+ name: 'review',
56
+ reviewer: 'auto',
57
+ max_rounds: 1,
58
+ };
59
+ describe('PRBoard — TTY workspace retention', () => {
60
+ let board;
61
+ let originalIsTTY;
62
+ let originalWrite;
63
+ beforeEach(() => {
64
+ originalIsTTY = process.stdout.isTTY;
65
+ Object.defineProperty(process.stdout, 'isTTY', { value: true, configurable: true });
66
+ originalWrite = process.stdout.write.bind(process.stdout);
67
+ process.stdout.write = (() => true);
68
+ board = new PRBoard();
69
+ board.setConfig(baseConfig, [reviewStep]);
70
+ });
71
+ afterEach(() => {
72
+ board.stop();
73
+ process.stdout.write = originalWrite;
74
+ Object.defineProperty(process.stdout, 'isTTY', { value: originalIsTTY, configurable: true });
75
+ });
76
+ // Tests reach into private state because the relevant invariants live
77
+ // on the slots map and there is no public read API.
78
+ const slots = () => board.slots;
79
+ const invokeRender = () => board.render();
80
+ const invokeFolded = (key) => board.renderPRSlotFolded(slots().get(key));
81
+ it('keeps completed slot in the workspace (no auto-clear)', () => {
82
+ board.addPR('k1', 1, 'a/b', 'main');
83
+ board.completePR('k1', { elapsedMs: 1000, url: 'https://github.com/a/b/pull/1' });
84
+ expect(slots().size).toBe(1);
85
+ expect(slots().has('k1')).toBe(true);
86
+ });
87
+ it('evicts oldest completed slots when total exceeds workspace cap', () => {
88
+ for (let i = 0; i < 30; i++) {
89
+ board.addPR(`k${i}`, i, 'a/b', `branch-${i}`);
90
+ board.completePR(`k${i}`, { elapsedMs: 1000, url: `https://github.com/a/b/pull/${i}` });
91
+ }
92
+ invokeRender();
93
+ expect(slots().size).toBe(25);
94
+ // The 5 oldest should be evicted (0..4); 5..29 retained.
95
+ for (let i = 0; i < 5; i++)
96
+ expect(slots().has(`k${i}`)).toBe(false);
97
+ for (let i = 5; i < 30; i++)
98
+ expect(slots().has(`k${i}`)).toBe(true);
99
+ });
100
+ it('never evicts active slots even at overflow', () => {
101
+ for (let i = 0; i < 24; i++) {
102
+ board.addPR(`done-${i}`, i, 'a/b', `branch-${i}`);
103
+ board.completePR(`done-${i}`, { elapsedMs: 1000, url: `url-${i}` });
104
+ }
105
+ for (let i = 0; i < 5; i++) {
106
+ board.addPR(`active-${i}`, 100 + i, 'a/b', `active-${i}`);
107
+ }
108
+ expect(slots().size).toBe(29);
109
+ invokeRender();
110
+ expect(slots().size).toBe(25);
111
+ for (let i = 0; i < 5; i++) {
112
+ expect(slots().has(`active-${i}`)).toBe(true);
113
+ }
114
+ });
115
+ it('renders a folded line with verdict, fix count, recheck and url', () => {
116
+ board.addPR('k1', 142, 'acme/api', 'chore/deps');
117
+ board.updatePR('k1', { verdict: 'APPROVE', commentCount: 0, fixCount: 3, recheckVerdict: 'APPROVE' });
118
+ board.completePR('k1', { elapsedMs: 45_000, url: 'https://github.com/acme/api/pull/142' });
119
+ const folded = stripAnsi(invokeFolded('k1'));
120
+ expect(folded).toContain('#142');
121
+ expect(folded).toContain('acme/api');
122
+ expect(folded).toContain('chore/deps');
123
+ expect(folded).toContain('CR: APPROVE');
124
+ expect(folded).toContain('fix 3');
125
+ expect(folded).toContain('recheck APPROVE');
126
+ expect(folded).toMatch(/\(\d+s\)/);
127
+ expect(folded).toContain('https://github.com/acme/api/pull/142');
128
+ });
129
+ it('renders the PR URL in the expanded completed slot (not just folded)', () => {
130
+ board.addPR('k1', 142, 'acme/api', 'chore/deps');
131
+ board.updatePR('k1', { verdict: 'APPROVE', commentCount: 0 });
132
+ board.completePR('k1', { elapsedMs: 45_000, url: 'https://github.com/acme/api/pull/142' });
133
+ const output = stripAnsi(invokeRender());
134
+ // With a single completed PR (below FOLD_THRESHOLD), the slot stays expanded
135
+ // via renderPRSlot. The URL must still surface so operators can click through.
136
+ expect(output).toContain('https://github.com/acme/api/pull/142');
137
+ });
138
+ it('orders sections top-to-bottom: config → stats → PR workspace', () => {
139
+ board.addPR('k1', 1, 'acme/api', 'feat/x');
140
+ const output = stripAnsi(invokeRender());
141
+ const idxBrand = output.indexOf('crosscheck');
142
+ const idxStats = output.indexOf('PRs:');
143
+ const idxPR = output.indexOf('#1');
144
+ expect(idxBrand).toBeGreaterThanOrEqual(0);
145
+ expect(idxStats).toBeGreaterThan(idxBrand);
146
+ expect(idxPR).toBeGreaterThan(idxStats);
147
+ });
148
+ });
36
149
  //# sourceMappingURL=board.test.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"board.test.js","sourceRoot":"","sources":["../../src/__tests__/board.test.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,QAAQ,EAAE,EAAE,EAAE,MAAM,EAAE,MAAM,QAAQ,CAAA;AAC7C,OAAO,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAA;AAE3C,QAAQ,CAAC,WAAW,EAAE,GAAG,EAAE;IACzB,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,CAAC,CAAA;IACvC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAA;QAChC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACpC,MAAM,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;QACpC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACxC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6BAA6B,EAAE,GAAG,EAAE;QACrC,MAAM,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACtC,MAAM,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,IAAI,CAAC,SAAS,CAAC,CAAA;IAC1C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oCAAoC,EAAE,GAAG,EAAE;QAC5C,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,wBAAwB,EAAE,GAAG,EAAE;QAChC,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kCAAkC,EAAE,GAAG,EAAE;QAC1C,MAAM,CAAC,SAAS,CAAC,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IAC3C,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
1
+ {"version":3,"file":"board.test.js","sourceRoot":"","sources":["../../src/__tests__/board.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=conflict-resolve.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conflict-resolve.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/conflict-resolve.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,123 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractConflictWindows, parseResolverEdits, PROMPT_TEMPLATE } from '../reviewers/conflict-resolve.js';
3
+ describe('extractConflictWindows', () => {
4
+ it('returns empty string when no markers are present', () => {
5
+ expect(extractConflictWindows('line a\nline b\nline c\n')).toBe('');
6
+ });
7
+ it('extracts a single conflict region with surrounding context', () => {
8
+ const content = [
9
+ 'top context 1',
10
+ 'top context 2',
11
+ '<<<<<<< HEAD',
12
+ 'ours',
13
+ '=======',
14
+ 'theirs',
15
+ '>>>>>>> branch',
16
+ 'bottom context 1',
17
+ 'bottom context 2',
18
+ ].join('\n');
19
+ const out = extractConflictWindows(content);
20
+ expect(out).toContain('<<<<<<< HEAD');
21
+ expect(out).toContain('=======');
22
+ expect(out).toContain('>>>>>>> branch');
23
+ expect(out).toContain('top context 1');
24
+ expect(out).toContain('bottom context 1');
25
+ });
26
+ it('surfaces conflicts that appear past the previous 4KB prefix cap', () => {
27
+ const filler = Array.from({ length: 200 }, (_, i) => `noise line ${i}`).join('\n');
28
+ const conflict = '<<<<<<< HEAD\nours\n=======\ntheirs\n>>>>>>> branch';
29
+ const content = `${filler}\n${conflict}\n${filler}`;
30
+ expect(content.length).toBeGreaterThan(4000);
31
+ const out = extractConflictWindows(content);
32
+ expect(out).toContain('<<<<<<< HEAD');
33
+ expect(out).toContain('>>>>>>> branch');
34
+ });
35
+ it('merges overlapping conflict windows instead of duplicating context', () => {
36
+ const lines = [];
37
+ for (let i = 0; i < 3; i++)
38
+ lines.push(`ctx ${i}`);
39
+ lines.push('<<<<<<< HEAD', 'a', '=======', 'b', '>>>>>>> branch');
40
+ lines.push('mid 1', 'mid 2');
41
+ lines.push('<<<<<<< HEAD', 'c', '=======', 'd', '>>>>>>> branch');
42
+ for (let i = 0; i < 3; i++)
43
+ lines.push(`tail ${i}`);
44
+ const out = extractConflictWindows(lines.join('\n'));
45
+ const startCount = (out.match(/<<<<<<< HEAD/g) ?? []).length;
46
+ expect(startCount).toBe(2);
47
+ // mid section between adjacent conflicts should appear exactly once (merged window)
48
+ expect((out.match(/mid 1/g) ?? []).length).toBe(1);
49
+ });
50
+ });
51
+ // Used by runner.ts to detect remaining conflict markers in resolved files.
52
+ // Kept in sync with the regex literal there; this test guards the contract.
53
+ const MARKER_RE = /^(<<<<<<<|=======|>>>>>>>)( |$)/m;
54
+ describe('conflict marker regex (runner.ts)', () => {
55
+ it('matches all three marker variants on their own line', () => {
56
+ expect(MARKER_RE.test('foo\n<<<<<<< HEAD\nbar')).toBe(true);
57
+ expect(MARKER_RE.test('foo\n=======\nbar')).toBe(true);
58
+ expect(MARKER_RE.test('foo\n>>>>>>> branch\nbar')).toBe(true);
59
+ });
60
+ it('matches a marker line with no trailing label (end of line)', () => {
61
+ expect(MARKER_RE.test('foo\n<<<<<<<\nbar')).toBe(true);
62
+ expect(MARKER_RE.test('foo\n=======\nbar')).toBe(true);
63
+ });
64
+ it('does not match a Markdown setext-style separator inside docs', () => {
65
+ // "=======" is a legitimate H1 setext underline in Markdown; only flag when
66
+ // the resolver actually leaves a marker behind in the resolved file.
67
+ // The regex DOES match a bare "=======" line — the false-positive defense is
68
+ // that runner.ts scopes the check to originally-conflicted files, not the
69
+ // whole worktree. Verify the marker line itself still matches so the scope
70
+ // restriction is doing the real work.
71
+ expect(MARKER_RE.test('Heading\n=======\n')).toBe(true);
72
+ });
73
+ it('does not match conflict markers appearing mid-line (e.g. in URLs or strings)', () => {
74
+ expect(MARKER_RE.test('see https://example.com/<<<<<<<-pin')).toBe(false);
75
+ expect(MARKER_RE.test('const s = "=======middle"')).toBe(false);
76
+ });
77
+ });
78
+ describe('PROMPT_TEMPLATE side preference', () => {
79
+ // The runner does `git merge origin/<base>` while the PR's branch is checked
80
+ // out as HEAD, so the `<<<<<<< HEAD` side is the PR author's changes and the
81
+ // `>>>>>>>` side is the base branch. The prompt must default to preserving
82
+ // the PR author's intent — preferring `>>>>>>>` would silently discard the
83
+ // PR's conflicting changes in favor of base.
84
+ it('prefers the PR author\'s side (<<<<<<< HEAD) over the base branch (>>>>>>>)', () => {
85
+ expect(PROMPT_TEMPLATE).toMatch(/prefer the PR author's changes \(the `?<<<<<<< HEAD/i);
86
+ // Guard against regression — must not advise preferring the >>>>>>> side
87
+ expect(PROMPT_TEMPLATE).not.toMatch(/prefer the incoming branch changes \(the >>>>>>>/i);
88
+ expect(PROMPT_TEMPLATE).not.toMatch(/prefer the >>>>>>> side/i);
89
+ });
90
+ it('explains which side is which so the resolver does not invert the convention', () => {
91
+ expect(PROMPT_TEMPLATE).toMatch(/<<<<<<< HEAD.*PR/s);
92
+ expect(PROMPT_TEMPLATE).toMatch(/>>>>>>>.*base/s);
93
+ });
94
+ });
95
+ describe('parseResolverEdits', () => {
96
+ const wrap = (path, oldText, newText) => `<edit path="${path}">\n<old>\n${oldText}\n</old>\n<new>\n${newText}\n</new>\n</edit>`;
97
+ it('parses a well-formed edit block', () => {
98
+ const out = wrap('src/a.ts', 'old line', 'new line');
99
+ const edits = parseResolverEdits(out, ['src/a.ts']);
100
+ expect(edits).toEqual([{ filePath: 'src/a.ts', oldText: 'old line', newText: 'new line' }]);
101
+ });
102
+ it('drops edits whose path is not in the conflicted set', () => {
103
+ // Defense against a buggy or prompt-injected resolver emitting <edit> blocks
104
+ // for tracked files that were never in U state. Those would otherwise be
105
+ // written + staged and could carry unresolved markers into the commit.
106
+ const out = wrap('src/legit.ts', 'a', 'b') + wrap('src/sneaky.ts', 'c', 'd');
107
+ const edits = parseResolverEdits(out, ['src/legit.ts']);
108
+ expect(edits.map(e => e.filePath)).toEqual(['src/legit.ts']);
109
+ });
110
+ it('drops path-escape attempts', () => {
111
+ const out = wrap('../etc/passwd', 'a', 'b') + wrap('/etc/passwd', 'c', 'd');
112
+ expect(parseResolverEdits(out, ['../etc/passwd', '/etc/passwd'])).toEqual([]);
113
+ });
114
+ it('drops edits with an empty <old> block', () => {
115
+ const out = wrap('src/a.ts', '', 'new');
116
+ expect(parseResolverEdits(out, ['src/a.ts'])).toEqual([]);
117
+ });
118
+ it('skips edits missing <old> or <new> tags', () => {
119
+ const broken = '<edit path="src/a.ts"><old>only old</old></edit>';
120
+ expect(parseResolverEdits(broken, ['src/a.ts'])).toEqual([]);
121
+ });
122
+ });
123
+ //# sourceMappingURL=conflict-resolve.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"conflict-resolve.test.js","sourceRoot":"","sources":["../../src/__tests__/conflict-resolve.test.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=detector.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"detector.test.d.ts","sourceRoot":"","sources":["../../src/__tests__/detector.test.ts"],"names":[],"mappings":""}
@@ -0,0 +1,108 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { detectOriginFromBody, detectOriginFromBranch, detectOriginFull, assignReviewer } from '../github/detector.js';
3
+ import { ConfigSchema } from '../config/schema.js';
4
+ function buildConfig(overrides = {}) {
5
+ return ConfigSchema.parse({
6
+ mode: 'cross-vendor',
7
+ vendors: { claude: { enabled: true }, codex: { enabled: true } },
8
+ ...overrides,
9
+ });
10
+ }
11
+ vi.mock('../github/client.js', () => ({
12
+ getPRCommits: vi.fn(async () => []),
13
+ }));
14
+ describe('detectOriginFromBody', () => {
15
+ it('detects claude origin from PR body footer', () => {
16
+ expect(detectOriginFromBody('Generated with [Claude Code]', buildConfig())).toBe('claude');
17
+ });
18
+ it('detects codex origin from PR body footer', () => {
19
+ expect(detectOriginFromBody('Generated with [OpenAI Codex]', buildConfig())).toBe('codex');
20
+ });
21
+ it('returns null when no pattern matches', () => {
22
+ expect(detectOriginFromBody('just a normal PR body', buildConfig())).toBeNull();
23
+ });
24
+ });
25
+ describe('detectOriginFromBranch', () => {
26
+ it('detects claude origin from claude/ branch prefix', () => {
27
+ expect(detectOriginFromBranch('claude/feat-foo', buildConfig())).toBe('claude');
28
+ });
29
+ it('detects codex origin from codex/ branch prefix', () => {
30
+ expect(detectOriginFromBranch('codex/feat-foo', buildConfig())).toBe('codex');
31
+ });
32
+ it('returns null when no prefix matches', () => {
33
+ expect(detectOriginFromBranch('feature/foo', buildConfig())).toBeNull();
34
+ });
35
+ });
36
+ describe('detectOriginFull — author_routes behavior', () => {
37
+ it('cross-vendor mode with both vendors enabled: bypasses author_routes', async () => {
38
+ const cfg = buildConfig({
39
+ mode: 'cross-vendor',
40
+ vendors: { claude: { enabled: true }, codex: { enabled: true } },
41
+ routing: { author_routes: { beingzy: 'claude' } },
42
+ });
43
+ const result = await detectOriginFull('', 'feature/foo', 'owner', 'repo', 1, cfg, 'token', 'beingzy');
44
+ expect(result.origin).toBe('human');
45
+ expect(result.method).toBe('author_routes_bypassed');
46
+ });
47
+ it('single-vendor mode: applies author_routes normally', async () => {
48
+ const cfg = buildConfig({
49
+ mode: 'single-vendor',
50
+ vendors: { claude: { enabled: true }, codex: { enabled: false } },
51
+ routing: { author_routes: { beingzy: 'claude' } },
52
+ });
53
+ const result = await detectOriginFull('', 'feature/foo', 'owner', 'repo', 1, cfg, 'token', 'beingzy');
54
+ expect(result.origin).toBe('claude');
55
+ expect(result.method).toBe('author_routes');
56
+ });
57
+ it('cross-vendor with only one vendor enabled: applies author_routes normally', async () => {
58
+ const cfg = buildConfig({
59
+ mode: 'cross-vendor',
60
+ vendors: { claude: { enabled: true }, codex: { enabled: false } },
61
+ routing: { author_routes: { beingzy: 'claude' } },
62
+ });
63
+ const result = await detectOriginFull('', 'feature/foo', 'owner', 'repo', 1, cfg, 'token', 'beingzy');
64
+ expect(result.origin).toBe('claude');
65
+ expect(result.method).toBe('author_routes');
66
+ });
67
+ it('cross-vendor with both enabled: attribution signals still win over author_routes', async () => {
68
+ const cfg = buildConfig({
69
+ mode: 'cross-vendor',
70
+ vendors: { claude: { enabled: true }, codex: { enabled: true } },
71
+ routing: { author_routes: { beingzy: 'claude' } },
72
+ });
73
+ const result = await detectOriginFull('', 'codex/feat', 'owner', 'repo', 1, cfg, 'token', 'beingzy');
74
+ expect(result.origin).toBe('codex');
75
+ expect(result.method).toBe('branch');
76
+ });
77
+ it('cross-vendor without author_routes: falls through to human', async () => {
78
+ const cfg = buildConfig({
79
+ mode: 'cross-vendor',
80
+ vendors: { claude: { enabled: true }, codex: { enabled: true } },
81
+ routing: { author_routes: {} },
82
+ });
83
+ const result = await detectOriginFull('', 'feature/foo', 'owner', 'repo', 1, cfg, 'token', 'beingzy');
84
+ expect(result.origin).toBe('human');
85
+ expect(result.method).toBe('none');
86
+ });
87
+ });
88
+ describe('assignReviewer', () => {
89
+ it('cross-vendor: claude origin → codex reviewer', async () => {
90
+ expect(await assignReviewer('claude', buildConfig())).toBe('codex');
91
+ });
92
+ it('cross-vendor: codex origin → claude reviewer', async () => {
93
+ expect(await assignReviewer('codex', buildConfig())).toBe('claude');
94
+ });
95
+ it('cross-vendor: explicit fallback_reviewer is honored for human origin', async () => {
96
+ const cfg = buildConfig({
97
+ routing: { fallback_reviewer: 'claude' },
98
+ });
99
+ expect(await assignReviewer('human', cfg)).toBe('claude');
100
+ });
101
+ it('cross-vendor: fallback_reviewer=null skips human-origin PRs', async () => {
102
+ const cfg = buildConfig({
103
+ routing: { fallback_reviewer: null },
104
+ });
105
+ expect(await assignReviewer('human', cfg)).toBeNull();
106
+ });
107
+ });
108
+ //# sourceMappingURL=detector.test.js.map
@@ -0,0 +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,EAAE,oBAAoB,EAAE,sBAAsB,EAAE,gBAAgB,EAAE,cAAc,EAAE,MAAM,uBAAuB,CAAA;AACtH,OAAO,EAAE,YAAY,EAAe,MAAM,qBAAqB,CAAA;AAE/D,SAAS,WAAW,CAAC,YAAqC,EAAE;IAC1D,OAAO,YAAY,CAAC,KAAK,CAAC;QACxB,IAAI,EAAE,cAAc;QACpB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;QAChE,GAAG,SAAS;KACb,CAAC,CAAA;AACJ,CAAC;AAED,EAAE,CAAC,IAAI,CAAC,qBAAqB,EAAE,GAAG,EAAE,CAAC,CAAC;IACpC,YAAY,EAAE,EAAE,CAAC,EAAE,CAAC,KAAK,IAAI,EAAE,CAAC,EAAE,CAAC;CACpC,CAAC,CAAC,CAAA;AAEH,QAAQ,CAAC,sBAAsB,EAAE,GAAG,EAAE;IACpC,EAAE,CAAC,2CAA2C,EAAE,GAAG,EAAE;QACnD,MAAM,CAAC,oBAAoB,CAAC,8BAA8B,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC5F,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,0CAA0C,EAAE,GAAG,EAAE;QAClD,MAAM,CAAC,oBAAoB,CAAC,+BAA+B,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC5F,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sCAAsC,EAAE,GAAG,EAAE;QAC9C,MAAM,CAAC,oBAAoB,CAAC,uBAAuB,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACjF,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,wBAAwB,EAAE,GAAG,EAAE;IACtC,EAAE,CAAC,kDAAkD,EAAE,GAAG,EAAE;QAC1D,MAAM,CAAC,sBAAsB,CAAC,iBAAiB,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACjF,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,gDAAgD,EAAE,GAAG,EAAE;QACxD,MAAM,CAAC,sBAAsB,CAAC,gBAAgB,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IAC/E,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,qCAAqC,EAAE,GAAG,EAAE;QAC7C,MAAM,CAAC,sBAAsB,CAAC,aAAa,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACzE,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,2CAA2C,EAAE,GAAG,EAAE;IACzD,EAAE,CAAC,qEAAqE,EAAE,KAAK,IAAI,EAAE;QACnF,MAAM,GAAG,GAAG,WAAW,CAAC;YACtB,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;YAChE,OAAO,EAAE,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;SAClD,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;QACrG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACnC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,wBAAwB,CAAC,CAAA;IACtD,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,oDAAoD,EAAE,KAAK,IAAI,EAAE;QAClE,MAAM,GAAG,GAAG,WAAW,CAAC;YACtB,IAAI,EAAE,eAAe;YACrB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YACjE,OAAO,EAAE,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;SAClD,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;QACrG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACpC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,2EAA2E,EAAE,KAAK,IAAI,EAAE;QACzF,MAAM,GAAG,GAAG,WAAW,CAAC;YACtB,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,KAAK,EAAE,EAAE;YACjE,OAAO,EAAE,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;SAClD,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;QACrG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;QACpC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,eAAe,CAAC,CAAA;IAC7C,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,kFAAkF,EAAE,KAAK,IAAI,EAAE;QAChG,MAAM,GAAG,GAAG,WAAW,CAAC;YACtB,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;YAChE,OAAO,EAAE,EAAE,aAAa,EAAE,EAAE,OAAO,EAAE,QAAQ,EAAE,EAAE;SAClD,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,EAAE,EAAE,YAAY,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;QACpG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACnC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACtC,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,4DAA4D,EAAE,KAAK,IAAI,EAAE;QAC1E,MAAM,GAAG,GAAG,WAAW,CAAC;YACtB,IAAI,EAAE,cAAc;YACpB,OAAO,EAAE,EAAE,MAAM,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE,KAAK,EAAE,EAAE,OAAO,EAAE,IAAI,EAAE,EAAE;YAChE,OAAO,EAAE,EAAE,aAAa,EAAE,EAAE,EAAE;SAC/B,CAAC,CAAA;QACF,MAAM,MAAM,GAAG,MAAM,gBAAgB,CAAC,EAAE,EAAE,aAAa,EAAE,OAAO,EAAE,MAAM,EAAE,CAAC,EAAE,GAAG,EAAE,OAAO,EAAE,SAAS,CAAC,CAAA;QACrG,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;QACnC,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAA;IACpC,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA;AAEF,QAAQ,CAAC,gBAAgB,EAAE,GAAG,EAAE;IAC9B,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,CAAC,MAAM,cAAc,CAAC,QAAQ,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAA;IACrE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,8CAA8C,EAAE,KAAK,IAAI,EAAE;QAC5D,MAAM,CAAC,MAAM,cAAc,CAAC,OAAO,EAAE,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IACrE,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,sEAAsE,EAAE,KAAK,IAAI,EAAE;QACpF,MAAM,GAAG,GAAG,WAAW,CAAC;YACtB,OAAO,EAAE,EAAE,iBAAiB,EAAE,QAAQ,EAAE;SACzC,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,QAAQ,CAAC,CAAA;IAC3D,CAAC,CAAC,CAAA;IAEF,EAAE,CAAC,6DAA6D,EAAE,KAAK,IAAI,EAAE;QAC3E,MAAM,GAAG,GAAG,WAAW,CAAC;YACtB,OAAO,EAAE,EAAE,iBAAiB,EAAE,IAAI,EAAE;SACrC,CAAC,CAAA;QACF,MAAM,CAAC,MAAM,cAAc,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC,CAAC,QAAQ,EAAE,CAAA;IACvD,CAAC,CAAC,CAAA;AACJ,CAAC,CAAC,CAAA"}
@@ -3,7 +3,7 @@ import { mkdtempSync, rmSync, writeFileSync, readFileSync, existsSync } from 'fs
3
3
  import { join } from 'path';
4
4
  import { tmpdir } from 'os';
5
5
  import yaml from 'js-yaml';
6
- import { applyOnboardConfig, detectCurrentPreset } from '../commands/onboard.js';
6
+ import { applyOnboardConfig, detectCurrentPreset, detectConflictResolveEnabled } from '../commands/onboard.js';
7
7
  import { mkdirSync } from 'fs';
8
8
  const BASE_DECISIONS = {
9
9
  deployment: 'personal',
@@ -14,6 +14,7 @@ const BASE_DECISIONS = {
14
14
  authorVendor: 'claude',
15
15
  qualityTier: 'balanced',
16
16
  pipelinePreset: 'review-only',
17
+ conflictResolve: false,
17
18
  tunnelBackend: 'localhost.run',
18
19
  smeeChannel: '',
19
20
  cloneProtocol: 'ssh',
@@ -437,4 +438,69 @@ describe('applyOnboardConfig — max_rounds in workflow.yml', () => {
437
438
  expect(raw.steps.find(s => s.type === 'recheck')?.instructions).toBe('custom recheck');
438
439
  });
439
440
  });
441
+ describe('applyOnboardConfig — conflict-resolve', () => {
442
+ it('conflictResolve: true prepends conflict-resolve step as first step', () => {
443
+ const dir = join(workflowDir, 'cr-enabled');
444
+ applyOnboardConfig(configPath, { ...BASE_DECISIONS, pipelinePreset: 'review-fix', conflictResolve: true }, dir);
445
+ const raw = yaml.load(readFileSync(join(dir, 'workflow.yml'), 'utf8'));
446
+ expect(raw.steps[0].type).toBe('conflict-resolve');
447
+ expect(raw.steps).toHaveLength(3); // conflict-resolve + review + fix
448
+ });
449
+ it('conflictResolve: false does not add conflict-resolve step', () => {
450
+ const dir = join(workflowDir, 'cr-disabled');
451
+ applyOnboardConfig(configPath, { ...BASE_DECISIONS, pipelinePreset: 'review-fix', conflictResolve: false }, dir);
452
+ const raw = yaml.load(readFileSync(join(dir, 'workflow.yml'), 'utf8'));
453
+ expect(raw.steps.some(s => s.type === 'conflict-resolve')).toBe(false);
454
+ expect(raw.steps).toHaveLength(2);
455
+ });
456
+ it('enabling conflict-resolve on re-run regenerates workflow', () => {
457
+ const dir = join(workflowDir, 'cr-enable-rerun');
458
+ applyOnboardConfig(configPath, { ...BASE_DECISIONS, pipelinePreset: 'review-fix', conflictResolve: false }, dir);
459
+ applyOnboardConfig(configPath, { ...BASE_DECISIONS, pipelinePreset: 'review-fix', conflictResolve: true }, dir);
460
+ const raw = yaml.load(readFileSync(join(dir, 'workflow.yml'), 'utf8'));
461
+ expect(raw.steps[0].type).toBe('conflict-resolve');
462
+ });
463
+ it('disabling conflict-resolve on re-run regenerates workflow', () => {
464
+ const dir = join(workflowDir, 'cr-disable-rerun');
465
+ applyOnboardConfig(configPath, { ...BASE_DECISIONS, pipelinePreset: 'review-fix', conflictResolve: true }, dir);
466
+ applyOnboardConfig(configPath, { ...BASE_DECISIONS, pipelinePreset: 'review-fix', conflictResolve: false }, dir);
467
+ const raw = yaml.load(readFileSync(join(dir, 'workflow.yml'), 'utf8'));
468
+ expect(raw.steps.some(s => s.type === 'conflict-resolve')).toBe(false);
469
+ });
470
+ it('conflict-resolve has max_rounds: 3 and reviewer: origin', () => {
471
+ const dir = join(workflowDir, 'cr-config');
472
+ applyOnboardConfig(configPath, { ...BASE_DECISIONS, pipelinePreset: 'review-only', conflictResolve: true }, dir);
473
+ const raw = yaml.load(readFileSync(join(dir, 'workflow.yml'), 'utf8'));
474
+ const crStep = raw.steps.find(s => s.type === 'conflict-resolve');
475
+ expect(crStep?.max_rounds).toBe(3);
476
+ expect(crStep?.reviewer).toBe('origin');
477
+ });
478
+ });
479
+ describe('detectConflictResolveEnabled', () => {
480
+ function seedWorkflow(content) {
481
+ mkdirSync(workflowDir, { recursive: true });
482
+ writeFileSync(join(workflowDir, 'workflow.yml'), content);
483
+ }
484
+ it('returns false when no workflow file exists', () => {
485
+ expect(detectConflictResolveEnabled(workflowDir)).toBe(false);
486
+ });
487
+ it('returns false when workflow has no conflict-resolve step', () => {
488
+ seedWorkflow(yaml.dump({ on: ['opened'], steps: [{ name: 'review', type: 'review' }] }));
489
+ expect(detectConflictResolveEnabled(workflowDir)).toBe(false);
490
+ });
491
+ it('returns true when workflow has a conflict-resolve step', () => {
492
+ seedWorkflow(yaml.dump({
493
+ on: ['opened'],
494
+ steps: [
495
+ { name: 'conflict-resolve', type: 'conflict-resolve' },
496
+ { name: 'review', type: 'review' },
497
+ ],
498
+ }));
499
+ expect(detectConflictResolveEnabled(workflowDir)).toBe(true);
500
+ });
501
+ it('returns false for malformed workflow', () => {
502
+ seedWorkflow('not: valid: yaml: at all: [');
503
+ expect(detectConflictResolveEnabled(workflowDir)).toBe(false);
504
+ });
505
+ });
440
506
  //# sourceMappingURL=onboard-preservation.test.js.map