@vexdo/cli 0.1.0 → 0.1.2

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 (51) hide show
  1. package/README.md +1 -1
  2. package/dist/index.d.ts +2 -0
  3. package/dist/index.js +1597 -0
  4. package/package.json +9 -1
  5. package/.eslintrc.json +0 -23
  6. package/.github/workflows/ci.yml +0 -84
  7. package/.idea/copilot.data.migration.ask2agent.xml +0 -6
  8. package/.idea/go.imports.xml +0 -11
  9. package/.idea/misc.xml +0 -6
  10. package/.idea/modules.xml +0 -8
  11. package/.idea/vcs.xml +0 -7
  12. package/.idea/vexdo-cli.iml +0 -9
  13. package/.prettierrc +0 -5
  14. package/CLAUDE.md +0 -93
  15. package/CONTRIBUTING.md +0 -62
  16. package/src/commands/abort.ts +0 -66
  17. package/src/commands/fix.ts +0 -106
  18. package/src/commands/init.ts +0 -142
  19. package/src/commands/logs.ts +0 -74
  20. package/src/commands/review.ts +0 -107
  21. package/src/commands/start.ts +0 -197
  22. package/src/commands/status.ts +0 -52
  23. package/src/commands/submit.ts +0 -38
  24. package/src/index.ts +0 -42
  25. package/src/lib/claude.ts +0 -259
  26. package/src/lib/codex.ts +0 -96
  27. package/src/lib/config.ts +0 -157
  28. package/src/lib/gh.ts +0 -78
  29. package/src/lib/git.ts +0 -119
  30. package/src/lib/logger.ts +0 -147
  31. package/src/lib/requirements.ts +0 -18
  32. package/src/lib/review-loop.ts +0 -154
  33. package/src/lib/state.ts +0 -121
  34. package/src/lib/submit-task.ts +0 -43
  35. package/src/lib/tasks.ts +0 -94
  36. package/src/prompts/arbiter.ts +0 -21
  37. package/src/prompts/reviewer.ts +0 -20
  38. package/src/types/index.ts +0 -96
  39. package/test/config.test.ts +0 -124
  40. package/test/state.test.ts +0 -147
  41. package/test/unit/claude.test.ts +0 -117
  42. package/test/unit/codex.test.ts +0 -67
  43. package/test/unit/gh.test.ts +0 -49
  44. package/test/unit/git.test.ts +0 -120
  45. package/test/unit/review-loop.test.ts +0 -198
  46. package/tests/integration/review.test.ts +0 -137
  47. package/tests/integration/start.test.ts +0 -220
  48. package/tests/unit/init.test.ts +0 -91
  49. package/tsconfig.json +0 -15
  50. package/tsup.config.ts +0 -8
  51. package/vitest.config.ts +0 -7
@@ -1,96 +0,0 @@
1
- export type CommentSeverity = 'critical' | 'important' | 'minor' | 'noise';
2
-
3
- export type ArbiterDecision = 'fix' | 'submit' | 'escalate';
4
-
5
- export type StepStatus = 'pending' | 'in_progress' | 'done' | 'failed' | 'escalated';
6
-
7
- export type TaskStatus = 'in_progress' | 'review' | 'done' | 'blocked' | 'escalated';
8
-
9
- export interface ServiceConfig {
10
- name: string;
11
- path: string;
12
- }
13
-
14
- export interface ReviewConfig {
15
- model: string;
16
- max_iterations: number;
17
- auto_submit: boolean;
18
- }
19
-
20
- export interface CodexConfig {
21
- model: string;
22
- }
23
-
24
- export interface VexdoConfig {
25
- version: 1;
26
- services: ServiceConfig[];
27
- review: ReviewConfig;
28
- codex: CodexConfig;
29
- }
30
-
31
- export interface TaskStep {
32
- service: string;
33
- spec: string;
34
- depends_on?: string[];
35
- }
36
-
37
- export interface Task {
38
- id: string;
39
- title: string;
40
- steps: TaskStep[];
41
- depends_on?: string[];
42
- }
43
-
44
- export interface ReviewComment {
45
- severity: CommentSeverity;
46
- file?: string;
47
- line?: number;
48
- comment: string;
49
- suggestion?: string;
50
- }
51
-
52
- export interface ReviewResult {
53
- comments: ReviewComment[];
54
- }
55
-
56
- export interface ArbiterResult {
57
- decision: ArbiterDecision;
58
- reasoning: string;
59
- feedback_for_codex?: string;
60
- summary: string;
61
- }
62
-
63
- export interface StepState {
64
- service: string;
65
- status: StepStatus;
66
- iteration: number;
67
- branch?: string;
68
- lastReviewComments?: ReviewComment[];
69
- lastArbiterResult?: ArbiterResult;
70
- }
71
-
72
- export interface VexdoState {
73
- taskId: string;
74
- taskTitle: string;
75
- taskPath: string;
76
- status: TaskStatus;
77
- steps: StepState[];
78
- startedAt: string;
79
- updatedAt: string;
80
- }
81
-
82
- export interface StartOptions {
83
- dryRun?: boolean;
84
- verbose?: boolean;
85
- resume?: boolean;
86
- }
87
-
88
- export interface IterationLog {
89
- taskId: string;
90
- service: string;
91
- iteration: number;
92
- diff: string;
93
- review: ReviewResult;
94
- arbiter: ArbiterResult;
95
- timestamp: string;
96
- }
@@ -1,124 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- import { afterEach, describe, expect, it } from 'vitest';
6
-
7
- import { findProjectRoot, loadConfig } from '../src/lib/config.js';
8
-
9
- const tempDirs: string[] = [];
10
-
11
- function makeTempDir(): string {
12
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vexdo-config-'));
13
- tempDirs.push(dir);
14
- return dir;
15
- }
16
-
17
- function writeConfig(dir: string, content: string): void {
18
- fs.writeFileSync(path.join(dir, '.vexdo.yml'), content, 'utf8');
19
- }
20
-
21
- afterEach(() => {
22
- for (const dir of tempDirs.splice(0)) {
23
- fs.rmSync(dir, { recursive: true, force: true });
24
- }
25
- });
26
-
27
- describe('findProjectRoot', () => {
28
- it('finds a parent directory containing .vexdo.yml', () => {
29
- const root = makeTempDir();
30
- const nested = path.join(root, 'a', 'b', 'c');
31
- fs.mkdirSync(nested, { recursive: true });
32
- writeConfig(root, 'version: 1\nservices:\n - name: api\n path: ./api\n');
33
-
34
- expect(findProjectRoot(nested)).toBe(root);
35
- });
36
-
37
- it('returns null when no config exists', () => {
38
- const dir = makeTempDir();
39
- expect(findProjectRoot(dir)).toBeNull();
40
- });
41
- });
42
-
43
- describe('loadConfig', () => {
44
- it('loads valid config', () => {
45
- const root = makeTempDir();
46
- writeConfig(
47
- root,
48
- `version: 1
49
- services:
50
- - name: api
51
- path: ./services/api
52
- review:
53
- model: custom-review
54
- max_iterations: 7
55
- auto_submit: true
56
- codex:
57
- model: custom-codex
58
- `,
59
- );
60
-
61
- const result = loadConfig(root);
62
-
63
- expect(result).toEqual({
64
- version: 1,
65
- services: [{ name: 'api', path: './services/api' }],
66
- review: { model: 'custom-review', max_iterations: 7, auto_submit: true },
67
- codex: { model: 'custom-codex' },
68
- });
69
- });
70
-
71
- it('applies defaults for review and codex', () => {
72
- const root = makeTempDir();
73
- writeConfig(
74
- root,
75
- `version: 1
76
- services:
77
- - name: api
78
- path: ./services/api
79
- `,
80
- );
81
-
82
- const result = loadConfig(root);
83
-
84
- expect(result.review).toEqual({
85
- model: 'claude-haiku-4-5-20251001',
86
- max_iterations: 3,
87
- auto_submit: false,
88
- });
89
- expect(result.codex).toEqual({ model: 'gpt-4o' });
90
- });
91
-
92
- it('throws when config file is missing', () => {
93
- const root = makeTempDir();
94
- expect(() => loadConfig(root)).toThrowError(/Configuration file not found/);
95
- });
96
-
97
- it('throws for wrong version', () => {
98
- const root = makeTempDir();
99
- writeConfig(root, 'version: 2\nservices:\n - name: api\n path: ./api\n');
100
-
101
- expect(() => loadConfig(root)).toThrowError('version must be 1');
102
- });
103
-
104
- it('throws for empty services', () => {
105
- const root = makeTempDir();
106
- writeConfig(root, 'version: 1\nservices: []\n');
107
-
108
- expect(() => loadConfig(root)).toThrowError('services must be a non-empty array');
109
- });
110
-
111
- it('throws for missing service name', () => {
112
- const root = makeTempDir();
113
- writeConfig(root, 'version: 1\nservices:\n - path: ./api\n');
114
-
115
- expect(() => loadConfig(root)).toThrowError('services[0].name must be a non-empty string');
116
- });
117
-
118
- it('throws for invalid yaml', () => {
119
- const root = makeTempDir();
120
- writeConfig(root, 'version: 1\nservices:\n - name: api\n path: [\n');
121
-
122
- expect(() => loadConfig(root)).toThrowError(/Invalid YAML/);
123
- });
124
- });
@@ -1,147 +0,0 @@
1
- import fs from 'node:fs';
2
- import os from 'node:os';
3
- import path from 'node:path';
4
-
5
- import { afterEach, describe, expect, it } from 'vitest';
6
-
7
- import {
8
- clearState,
9
- createState,
10
- getStatePath,
11
- hasActiveTask,
12
- loadState,
13
- saveIterationLog,
14
- saveState,
15
- updateStep,
16
- } from '../src/lib/state.js';
17
- import type { TaskStatus, VexdoState } from '../src/types/index.js';
18
-
19
- const tempDirs: string[] = [];
20
-
21
- function makeTempDir(): string {
22
- const dir = fs.mkdtempSync(path.join(os.tmpdir(), 'vexdo-state-'));
23
- tempDirs.push(dir);
24
- return dir;
25
- }
26
-
27
- afterEach(() => {
28
- for (const dir of tempDirs.splice(0)) {
29
- fs.rmSync(dir, { recursive: true, force: true });
30
- }
31
- });
32
-
33
- describe('state', () => {
34
- it('returns null when absent', () => {
35
- const root = makeTempDir();
36
- expect(loadState(root)).toBeNull();
37
- });
38
-
39
- it('save/load roundtrip works', () => {
40
- const root = makeTempDir();
41
- const created = createState('task-1', 'Task 1', '/tmp/task.yml', [
42
- { service: 'api', status: 'pending', iteration: 0 },
43
- ]);
44
-
45
- saveState(root, created);
46
- const loaded = loadState(root);
47
-
48
- expect(loaded).not.toBeNull();
49
- expect(loaded?.taskId).toBe('task-1');
50
- expect(loaded?.taskTitle).toBe('Task 1');
51
- expect(loaded?.steps[0]?.service).toBe('api');
52
- });
53
-
54
- it('throws on corrupt JSON', () => {
55
- const root = makeTempDir();
56
- const statePath = getStatePath(root);
57
- fs.mkdirSync(path.dirname(statePath), { recursive: true });
58
- fs.writeFileSync(statePath, '{ not-json', 'utf8');
59
-
60
- expect(() => loadState(root)).toThrowError(/Corrupt state file/);
61
- });
62
-
63
- it('clearState removes file and is idempotent', () => {
64
- const root = makeTempDir();
65
- const state = createState('task-1', 'Task 1', '/tmp/task.yml', []);
66
- saveState(root, state);
67
-
68
- clearState(root);
69
- clearState(root);
70
-
71
- expect(loadState(root)).toBeNull();
72
- });
73
-
74
- it('hasActiveTask handles all statuses', () => {
75
- const root = makeTempDir();
76
- const statuses: TaskStatus[] = ['in_progress', 'review', 'done', 'blocked', 'escalated'];
77
-
78
- for (const status of statuses) {
79
- const base: VexdoState = {
80
- taskId: 'id',
81
- taskTitle: 'title',
82
- taskPath: '/tmp/task.yml',
83
- status,
84
- steps: [],
85
- startedAt: new Date().toISOString(),
86
- updatedAt: new Date().toISOString(),
87
- };
88
- saveState(root, base);
89
- const expected = status === 'in_progress' || status === 'review';
90
- expect(hasActiveTask(root)).toBe(expected);
91
- }
92
- });
93
-
94
- it('createState sets expected fields', () => {
95
- const state = createState('task-1', 'Task 1', '/tmp/task.yml', [
96
- { service: 'api', status: 'pending', iteration: 0 },
97
- ]);
98
-
99
- expect(state.taskId).toBe('task-1');
100
- expect(state.taskTitle).toBe('Task 1');
101
- expect(state.taskPath).toBe('/tmp/task.yml');
102
- expect(state.status).toBe('in_progress');
103
- expect(state.startedAt).toBeTruthy();
104
- expect(state.updatedAt).toBe(state.startedAt);
105
- });
106
-
107
- it('updateStep is immutable', () => {
108
- const original = createState('task-1', 'Task 1', '/tmp/task.yml', [
109
- { service: 'api', status: 'pending', iteration: 0 },
110
- { service: 'web', status: 'pending', iteration: 0 },
111
- ]);
112
-
113
- const updated = updateStep(original, 'api', {
114
- status: 'in_progress',
115
- iteration: 1,
116
- });
117
-
118
- expect(updated).not.toBe(original);
119
- expect(updated.steps).not.toBe(original.steps);
120
- expect(updated.steps[0]).not.toBe(original.steps[0]);
121
- expect(updated.steps[1]).toBe(original.steps[1]);
122
- expect(original.steps[0]?.status).toBe('pending');
123
- expect(updated.steps[0]?.status).toBe('in_progress');
124
- expect(updated.steps[0]?.iteration).toBe(1);
125
- });
126
-
127
- it('saveIterationLog creates diff, review, and arbiter files', () => {
128
- const root = makeTempDir();
129
-
130
- saveIterationLog(root, 'task-1', 'api', 2, {
131
- diff: 'diff content',
132
- review: {
133
- comments: [{ severity: 'important', comment: 'Needs work' }],
134
- },
135
- arbiter: {
136
- decision: 'fix',
137
- reasoning: 'Please address comments',
138
- summary: 'One important issue',
139
- },
140
- });
141
-
142
- const logsDir = path.join(root, '.vexdo', 'logs', 'task-1');
143
- expect(fs.existsSync(path.join(logsDir, 'api-iteration-2-diff.txt'))).toBe(true);
144
- expect(fs.existsSync(path.join(logsDir, 'api-iteration-2-review.json'))).toBe(true);
145
- expect(fs.existsSync(path.join(logsDir, 'api-iteration-2-arbiter.json'))).toBe(true);
146
- });
147
- });
@@ -1,117 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const { messagesCreateMock, anthropicCtorMock } = vi.hoisted(() => {
4
- const create = vi.fn();
5
- const ctor = vi.fn(() => ({
6
- messages: {
7
- create,
8
- },
9
- }));
10
-
11
- return {
12
- messagesCreateMock: create,
13
- anthropicCtorMock: ctor,
14
- };
15
- });
16
-
17
- vi.mock('@anthropic-ai/sdk', () => ({
18
- default: anthropicCtorMock,
19
- }));
20
-
21
- import { ClaudeClient, ClaudeError } from '../../src/lib/claude.js';
22
-
23
- describe('ClaudeClient', () => {
24
- beforeEach(() => {
25
- messagesCreateMock.mockReset();
26
- anthropicCtorMock.mockClear();
27
- });
28
-
29
- it('runReviewer parses valid JSON response correctly', async () => {
30
- messagesCreateMock.mockResolvedValue({
31
- content: [{ type: 'text', text: '{"comments":[{"severity":"important","comment":"bad","file":"a.ts","line":4}]}' }],
32
- });
33
-
34
- const client = new ClaudeClient('test-key');
35
- const result = await client.runReviewer({ spec: 'spec', diff: 'diff', model: 'claude' });
36
-
37
- expect(result).toEqual({
38
- comments: [{ severity: 'important', comment: 'bad', file: 'a.ts', line: 4 }],
39
- });
40
- });
41
-
42
- it('runReviewer strips markdown code fences before parsing', async () => {
43
- messagesCreateMock.mockResolvedValue({
44
- content: [
45
- {
46
- type: 'text',
47
- text: '```json\n{"comments":[{"severity":"minor","comment":"nit"}]}\n```',
48
- },
49
- ],
50
- });
51
-
52
- const client = new ClaudeClient('test-key');
53
- const result = await client.runReviewer({ spec: 'spec', diff: 'diff', model: 'claude' });
54
-
55
- expect(result.comments).toHaveLength(1);
56
- expect(result.comments[0]).toMatchObject({ severity: 'minor', comment: 'nit' });
57
- });
58
-
59
- it('runReviewer retries on network error and succeeds within 3 attempts', async () => {
60
- messagesCreateMock
61
- .mockRejectedValueOnce(new Error('network down'))
62
- .mockRejectedValueOnce(new Error('timeout'))
63
- .mockResolvedValueOnce({ content: [{ type: 'text', text: '{"comments":[]}' }] });
64
-
65
- const client = new ClaudeClient('test-key');
66
- const resultPromise = client.runReviewer({ spec: 'spec', diff: 'diff', model: 'claude' });
67
-
68
- await expect(resultPromise).resolves.toEqual({ comments: [] });
69
- expect(messagesCreateMock).toHaveBeenCalledTimes(3);
70
- });
71
-
72
- it('runReviewer throws ClaudeError after 3 failures', async () => {
73
- messagesCreateMock.mockRejectedValue(new Error('always down'));
74
-
75
- const client = new ClaudeClient('test-key');
76
- await expect(client.runReviewer({ spec: 'spec', diff: 'diff', model: 'claude' })).rejects.toMatchObject({
77
- name: 'ClaudeError',
78
- attempt: 3,
79
- message: expect.stringContaining('Claude API failed after 3 attempts'),
80
- });
81
- });
82
-
83
- it('runReviewer does NOT retry on 401', async () => {
84
- const unauthorized = Object.assign(new Error('unauthorized'), { status: 401 });
85
- messagesCreateMock.mockRejectedValue(unauthorized);
86
-
87
- const client = new ClaudeClient('test-key');
88
- await expect(client.runReviewer({ spec: 'spec', diff: 'diff', model: 'claude' })).rejects.toBeInstanceOf(ClaudeError);
89
- expect(messagesCreateMock).toHaveBeenCalledTimes(1);
90
- });
91
-
92
- it('runArbiter parses valid JSON response correctly', async () => {
93
- messagesCreateMock.mockResolvedValue({
94
- content: [
95
- {
96
- type: 'text',
97
- text: '{"decision":"fix","reasoning":"spec violation","feedback_for_codex":"change src/a.ts","summary":"Need fix"}',
98
- },
99
- ],
100
- });
101
-
102
- const client = new ClaudeClient('test-key');
103
- const result = await client.runArbiter({
104
- spec: 'spec',
105
- diff: 'diff',
106
- model: 'claude',
107
- reviewComments: [{ severity: 'critical', comment: 'broken' }],
108
- });
109
-
110
- expect(result).toEqual({
111
- decision: 'fix',
112
- reasoning: 'spec violation',
113
- feedback_for_codex: 'change src/a.ts',
114
- summary: 'Need fix',
115
- });
116
- });
117
- });
@@ -1,67 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const { execFileMock } = vi.hoisted(() => ({
4
- execFileMock: vi.fn(),
5
- }));
6
- const { debugMock } = vi.hoisted(() => ({
7
- debugMock: vi.fn(),
8
- }));
9
-
10
- vi.mock('node:child_process', () => ({
11
- execFile: execFileMock,
12
- }));
13
-
14
- vi.mock('../../src/lib/logger.js', () => ({
15
- debug: debugMock,
16
- }));
17
-
18
- import { CodexError, CodexNotFoundError, checkCodexAvailable, exec } from '../../src/lib/codex.js';
19
-
20
- beforeEach(() => {
21
- execFileMock.mockReset();
22
- debugMock.mockReset();
23
- });
24
-
25
- describe('checkCodexAvailable', () => {
26
- it('resolves when codex --version exits 0', async () => {
27
- execFileMock.mockImplementation((_cmd, _args, _opts, cb) => cb(null, '1.0.0', ''));
28
-
29
- await expect(checkCodexAvailable()).resolves.toBeUndefined();
30
- expect(execFileMock).toHaveBeenCalledWith('codex', ['--version'], expect.any(Object), expect.any(Function));
31
- });
32
-
33
- it('throws CodexNotFoundError when codex not found', async () => {
34
- const error = Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
35
- execFileMock.mockImplementation((_cmd, _args, _opts, cb) => cb(error, '', ''));
36
-
37
- await expect(checkCodexAvailable()).rejects.toBeInstanceOf(CodexNotFoundError);
38
- });
39
- });
40
-
41
- describe('codex.exec', () => {
42
- it('resolves CodexResult on success', async () => {
43
- execFileMock.mockImplementation((_cmd, _args, _opts, cb) => cb(null, 'done\n', 'warn\n'));
44
-
45
- await expect(exec({ spec: 'do it', model: 'gpt-4o', cwd: '/repo' })).resolves.toEqual({
46
- stdout: 'done',
47
- stderr: 'warn',
48
- exitCode: 0,
49
- });
50
- });
51
-
52
- it('throws CodexError on non-zero exit', async () => {
53
- const error = Object.assign(new Error('bad'), { code: 9 });
54
- execFileMock.mockImplementation((_cmd, _args, _opts, cb) => cb(error, 'partial', 'failed'));
55
-
56
- await expect(exec({ spec: 'do it', model: 'gpt-4o', cwd: '/repo' })).rejects.toBeInstanceOf(CodexError);
57
- });
58
-
59
- it('verbose mode passes output to logger.debug', async () => {
60
- execFileMock.mockImplementation((_cmd, _args, _opts, cb) => cb(null, 'out', 'err'));
61
-
62
- await exec({ spec: 'do it', model: 'gpt-4o', cwd: '/repo', verbose: true });
63
-
64
- expect(debugMock).toHaveBeenCalledWith('out');
65
- expect(debugMock).toHaveBeenCalledWith('err');
66
- });
67
- });
@@ -1,49 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from 'vitest';
2
-
3
- const { execFileMock } = vi.hoisted(() => ({
4
- execFileMock: vi.fn(),
5
- }));
6
-
7
- vi.mock('node:child_process', () => ({
8
- execFile: execFileMock,
9
- }));
10
-
11
- import { GhNotFoundError, checkGhAvailable, createPr, getPrUrl } from '../../src/lib/gh.js';
12
-
13
- beforeEach(() => {
14
- execFileMock.mockReset();
15
- });
16
-
17
- describe('checkGhAvailable', () => {
18
- it('resolves when gh is present', async () => {
19
- execFileMock.mockImplementation((_cmd, _args, _opts, cb) => cb(null, 'gh version', ''));
20
-
21
- await expect(checkGhAvailable()).resolves.toBeUndefined();
22
- });
23
-
24
- it('throws GhNotFoundError when gh not found', async () => {
25
- const error = Object.assign(new Error('ENOENT'), { code: 'ENOENT' });
26
- execFileMock.mockImplementation((_cmd, _args, _opts, cb) => cb(error, '', ''));
27
-
28
- await expect(checkGhAvailable()).rejects.toBeInstanceOf(GhNotFoundError);
29
- });
30
- });
31
-
32
- describe('gh helpers', () => {
33
- it('createPr returns URL from stdout', async () => {
34
- execFileMock.mockImplementation((_cmd, _args, _opts, cb) => cb(null, 'https://github.com/org/repo/pull/1\n', ''));
35
-
36
- await expect(createPr({ title: 't', body: 'b', cwd: '/repo' })).resolves.toBe(
37
- 'https://github.com/org/repo/pull/1',
38
- );
39
- });
40
-
41
- it('getPrUrl returns URL or null', async () => {
42
- execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(null, 'https://github.com/org/repo/pull/2\n', ''));
43
- await expect(getPrUrl('branch', '/repo')).resolves.toBe('https://github.com/org/repo/pull/2');
44
-
45
- const error = Object.assign(new Error('none'), { code: 1 });
46
- execFileMock.mockImplementationOnce((_cmd, _args, _opts, cb) => cb(error, '', ''));
47
- await expect(getPrUrl('branch', '/repo')).resolves.toBeNull();
48
- });
49
- });