pr-shepherd 0.16.3 → 0.16.4
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/.claude-plugin/plugin.json +1 -1
- package/bin/checks/triage.test-support.mjs +61 -0
- package/bin/cli/iterate-lean.test-support.mjs +5 -0
- package/bin/cli-parser.commit-suggestion.test-support.mjs +66 -0
- package/bin/cli-parser.iterate-fix.test-support.mjs +48 -0
- package/bin/cli-parser.iterate.test-support.mjs +49 -0
- package/bin/cli-parser.test-support.mjs +43 -0
- package/bin/commands/check.test-support.mjs +140 -0
- package/bin/commands/commit-suggestion.apply.test-support.mjs +87 -0
- package/bin/commands/commit-suggestion.test-support.mjs +112 -0
- package/bin/commands/iterate/index.mjs +19 -16
- package/bin/commands/iterate-stall.test-support.mjs +25 -0
- package/bin/commands/iterate-test-support.mjs +149 -0
- package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +118 -0
- package/bin/commands/resolve.test-support.mjs +114 -0
- package/bin/commands/shepherd-journal.test-support.mjs +9 -0
- package/bin/comments/resolve.test-support.mjs +40 -0
- package/bin/github/batch-parsers.test-support.mjs +67 -0
- package/bin/github/batch.test-support.mjs +67 -0
- package/bin/github/graphql-http.mjs +73 -0
- package/bin/github/http-auth.mjs +48 -0
- package/bin/github/http-request.mjs +15 -0
- package/bin/github/http-utils.mjs +34 -0
- package/bin/github/http.mjs +4 -319
- package/bin/github/http.test-support.mjs +52 -0
- package/bin/github/rest-http.mjs +131 -0
- package/bin/suggestions/patch.test-support.mjs +4 -0
- package/package.json +2 -2
- package/plugins/pr-shepherd/.codex-plugin/plugin.json +1 -1
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
import { NOW, makeOpts } from "./iterate-test-support.mjs";
|
|
4
|
+
import { readStallState, writeStallState } from "../state/iterate-stall.mjs";
|
|
5
|
+
const mockReadStallState = vi.mocked(readStallState);
|
|
6
|
+
const mockWriteStallState = vi.mocked(writeStallState);
|
|
7
|
+
const STALL_TIMEOUT_S = 1800;
|
|
8
|
+
const RESOLUTION_ONLY_THREAD = {
|
|
9
|
+
id: "thread-resolution-only",
|
|
10
|
+
isResolved: false,
|
|
11
|
+
isOutdated: true,
|
|
12
|
+
isMinimized: false,
|
|
13
|
+
path: "src/old.mts",
|
|
14
|
+
line: null,
|
|
15
|
+
startLine: null,
|
|
16
|
+
author: "reviewer",
|
|
17
|
+
authorType: "Unknown",
|
|
18
|
+
body: "Already addressed on an old diff",
|
|
19
|
+
url: "",
|
|
20
|
+
createdAtUnix: NOW - 3600,
|
|
21
|
+
};
|
|
22
|
+
function makeOpts30mStall(overrides = {}) {
|
|
23
|
+
return makeOpts({ stallTimeoutSeconds: STALL_TIMEOUT_S, noAutoMarkReady: true, ...overrides });
|
|
24
|
+
}
|
|
25
|
+
export { RESOLUTION_ONLY_THREAD, STALL_TIMEOUT_S, makeOpts30mStall, mockReadStallState, mockWriteStallState, };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { vi, beforeEach, afterEach } from "vitest";
|
|
2
|
+
const mockFetch = vi.fn();
|
|
3
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
4
|
+
const { mockExecFile } = vi.hoisted(() => ({ mockExecFile: vi.fn() }));
|
|
5
|
+
vi.mock("node:child_process", () => ({
|
|
6
|
+
execFile: (cmd, args, optsOrCb, maybeCb) => {
|
|
7
|
+
const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
|
|
8
|
+
mockExecFile(cmd, args)
|
|
9
|
+
.then((result) => cb(null, result))
|
|
10
|
+
.catch((err) => cb(err, { stdout: "", stderr: err.stderr ?? "" }));
|
|
11
|
+
},
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("./check.mts", () => ({ runCheck: vi.fn() }));
|
|
14
|
+
vi.mock("./ready-delay.mts", () => ({ updateReadyDelay: vi.fn() }));
|
|
15
|
+
vi.mock("../github/client.mts", () => ({ getCurrentPrNumber: vi.fn().mockResolvedValue(42) }));
|
|
16
|
+
vi.mock("../state/fix-attempts.mts", () => ({
|
|
17
|
+
readFixAttempts: vi.fn().mockResolvedValue(null),
|
|
18
|
+
writeFixAttempts: vi.fn().mockResolvedValue(undefined),
|
|
19
|
+
}));
|
|
20
|
+
vi.mock("../state/iterate-stall.mts", () => ({
|
|
21
|
+
readStallState: vi.fn().mockResolvedValue(null),
|
|
22
|
+
writeStallState: vi.fn().mockResolvedValue(undefined),
|
|
23
|
+
clearStallState: vi.fn().mockResolvedValue(undefined),
|
|
24
|
+
}));
|
|
25
|
+
const { mockLoadConfig } = vi.hoisted(() => ({ mockLoadConfig: vi.fn() }));
|
|
26
|
+
vi.mock("../config/load.mts", () => ({ loadConfig: mockLoadConfig }));
|
|
27
|
+
import { runCheck } from "./check.mjs";
|
|
28
|
+
import { updateReadyDelay } from "./ready-delay.mjs";
|
|
29
|
+
import { getCurrentPrNumber } from "../github/client.mjs";
|
|
30
|
+
import { readFixAttempts, writeFixAttempts } from "../state/fix-attempts.mjs";
|
|
31
|
+
import { clearStallState, readStallState, writeStallState } from "../state/iterate-stall.mjs";
|
|
32
|
+
import { buildEscalateHumanMessage, buildEscalateSuggestion, checkEscalateTriggers, } from "./iterate/escalate.mjs";
|
|
33
|
+
import { buildRelevantChecks, buildWaitLog, getCurrentHeadSha } from "./iterate/helpers.mjs";
|
|
34
|
+
const mockRunCheck = vi.mocked(runCheck);
|
|
35
|
+
const mockUpdateReadyDelay = vi.mocked(updateReadyDelay);
|
|
36
|
+
const mockGetCurrentPrNumber = vi.mocked(getCurrentPrNumber);
|
|
37
|
+
const mockReadFixAttempts = vi.mocked(readFixAttempts);
|
|
38
|
+
const mockWriteFixAttempts = vi.mocked(writeFixAttempts);
|
|
39
|
+
const mockReadStallState = vi.mocked(readStallState);
|
|
40
|
+
const mockWriteStallState = vi.mocked(writeStallState);
|
|
41
|
+
const mockClearStallState = vi.mocked(clearStallState);
|
|
42
|
+
const NOW = 1_700_000_000;
|
|
43
|
+
const READY_STATE_DEFAULT = { isReady: true, shouldCancel: false, remainingSeconds: 300 };
|
|
44
|
+
function makeReport(overrides = {}) {
|
|
45
|
+
return {
|
|
46
|
+
pr: 42,
|
|
47
|
+
nodeId: "PR_kgDOAAA",
|
|
48
|
+
repo: "owner/repo",
|
|
49
|
+
status: "READY",
|
|
50
|
+
baseBranch: "main",
|
|
51
|
+
mergeStatus: {
|
|
52
|
+
status: "CLEAN",
|
|
53
|
+
state: "OPEN",
|
|
54
|
+
isDraft: false,
|
|
55
|
+
mergeable: "MERGEABLE",
|
|
56
|
+
reviewDecision: "APPROVED",
|
|
57
|
+
blockingBotReviewInProgress: false,
|
|
58
|
+
mergeStateStatus: "CLEAN",
|
|
59
|
+
},
|
|
60
|
+
checks: {
|
|
61
|
+
passing: [
|
|
62
|
+
{
|
|
63
|
+
name: "ci",
|
|
64
|
+
status: "COMPLETED",
|
|
65
|
+
conclusion: "SUCCESS",
|
|
66
|
+
detailsUrl: "https://github.com/owner/repo/actions/runs/1",
|
|
67
|
+
event: "pull_request",
|
|
68
|
+
runId: "run-1",
|
|
69
|
+
category: "passed",
|
|
70
|
+
},
|
|
71
|
+
],
|
|
72
|
+
failing: [],
|
|
73
|
+
inProgress: [],
|
|
74
|
+
skipped: [],
|
|
75
|
+
filtered: [],
|
|
76
|
+
filteredNames: [],
|
|
77
|
+
blockedByFilteredCheck: false,
|
|
78
|
+
},
|
|
79
|
+
threads: {
|
|
80
|
+
actionable: [],
|
|
81
|
+
resolutionOnly: [],
|
|
82
|
+
autoResolved: [],
|
|
83
|
+
autoResolveErrors: [],
|
|
84
|
+
firstLook: [],
|
|
85
|
+
},
|
|
86
|
+
comments: { actionable: [], firstLook: [] },
|
|
87
|
+
changesRequestedReviews: [],
|
|
88
|
+
reviewSummaries: [],
|
|
89
|
+
firstLookSummaries: [],
|
|
90
|
+
editedSummaries: [],
|
|
91
|
+
approvedReviews: [],
|
|
92
|
+
...overrides,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
function makeOpts(overrides = {}) {
|
|
96
|
+
return { prNumber: 42, format: "json", readyDelaySeconds: 600, ...overrides };
|
|
97
|
+
}
|
|
98
|
+
function makeReview(id, author, body) {
|
|
99
|
+
return { id, author, authorType: "Unknown", body };
|
|
100
|
+
}
|
|
101
|
+
function defaultConfig() {
|
|
102
|
+
return {
|
|
103
|
+
iterate: {
|
|
104
|
+
fixAttemptsPerThread: 3,
|
|
105
|
+
stallTimeoutMinutes: 30,
|
|
106
|
+
minimizeApprovals: false,
|
|
107
|
+
minimizeComments: "all",
|
|
108
|
+
},
|
|
109
|
+
watch: { readyDelayMinutes: 10 },
|
|
110
|
+
resolve: {
|
|
111
|
+
concurrency: 4,
|
|
112
|
+
shaPoll: { intervalMs: 2000, maxAttempts: 10 },
|
|
113
|
+
fetchReviewSummaries: true,
|
|
114
|
+
},
|
|
115
|
+
checks: { ciTriggerEvents: ["pull_request", "pull_request_target"] },
|
|
116
|
+
mergeStatus: { blockingReviewerLogins: ["copilot"] },
|
|
117
|
+
actions: { autoResolveOutdated: true, autoMarkReady: true, commitSuggestions: true },
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
function registerIterateHooks(config = defaultConfig) {
|
|
121
|
+
beforeEach(() => {
|
|
122
|
+
vi.clearAllMocks();
|
|
123
|
+
mockLoadConfig.mockReturnValue(config());
|
|
124
|
+
process.env["GH_TOKEN"] = "test-token";
|
|
125
|
+
mockExecFile.mockImplementation((cmd, args) => {
|
|
126
|
+
if (cmd === "git" && args[0] === "rev-parse") {
|
|
127
|
+
return Promise.resolve({ stdout: "abc123", stderr: "" });
|
|
128
|
+
}
|
|
129
|
+
return Promise.resolve({ stdout: "", stderr: "" });
|
|
130
|
+
});
|
|
131
|
+
mockFetch.mockResolvedValue({
|
|
132
|
+
ok: true,
|
|
133
|
+
status: 200,
|
|
134
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
135
|
+
json: () => Promise.resolve({ data: {} }),
|
|
136
|
+
text: () => Promise.resolve('{"data":{}}'),
|
|
137
|
+
});
|
|
138
|
+
vi.useFakeTimers();
|
|
139
|
+
vi.setSystemTime(NOW * 1000);
|
|
140
|
+
mockUpdateReadyDelay.mockResolvedValue(READY_STATE_DEFAULT);
|
|
141
|
+
mockReadFixAttempts.mockResolvedValue(null);
|
|
142
|
+
mockWriteFixAttempts.mockResolvedValue(undefined);
|
|
143
|
+
mockReadStallState.mockResolvedValue(null);
|
|
144
|
+
mockWriteStallState.mockResolvedValue(undefined);
|
|
145
|
+
mockClearStallState.mockResolvedValue(undefined);
|
|
146
|
+
});
|
|
147
|
+
afterEach(() => vi.useRealTimers());
|
|
148
|
+
}
|
|
149
|
+
export { NOW, buildEscalateHumanMessage, buildEscalateSuggestion, buildRelevantChecks, buildWaitLog, checkEscalateTriggers, defaultConfig, getCurrentHeadSha, makeOpts, makeReport, makeReview, mockExecFile, mockFetch, mockGetCurrentPrNumber, mockClearStallState, mockLoadConfig, mockReadFixAttempts, mockReadStallState, mockRunCheck, mockUpdateReadyDelay, mockWriteFixAttempts, mockWriteStallState, registerIterateHooks, };
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
const mockFetch = vi.fn();
|
|
4
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
5
|
+
const { mockExecFile } = vi.hoisted(() => ({ mockExecFile: vi.fn() }));
|
|
6
|
+
vi.mock("node:child_process", () => ({
|
|
7
|
+
execFile: (cmd, args, optsOrCb, maybeCb) => {
|
|
8
|
+
const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
|
|
9
|
+
mockExecFile(cmd, args)
|
|
10
|
+
.then((result) => cb(null, result))
|
|
11
|
+
.catch((err) => cb(err, { stdout: "", stderr: err.stderr ?? "" }));
|
|
12
|
+
},
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("./check.mts", () => ({ runCheck: vi.fn() }));
|
|
15
|
+
vi.mock("./ready-delay.mts", () => ({ updateReadyDelay: vi.fn() }));
|
|
16
|
+
vi.mock("../github/client.mts", () => ({
|
|
17
|
+
getCurrentPrNumber: vi.fn().mockResolvedValue(42),
|
|
18
|
+
}));
|
|
19
|
+
vi.mock("../state/fix-attempts.mts", () => ({
|
|
20
|
+
readFixAttempts: vi.fn().mockResolvedValue(null),
|
|
21
|
+
writeFixAttempts: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock("../state/iterate-stall.mts", () => ({
|
|
24
|
+
readStallState: vi.fn().mockResolvedValue(null),
|
|
25
|
+
writeStallState: vi.fn().mockResolvedValue(undefined),
|
|
26
|
+
}));
|
|
27
|
+
const { mockLoadConfig } = vi.hoisted(() => ({ mockLoadConfig: vi.fn() }));
|
|
28
|
+
vi.mock("../config/load.mts", () => ({ loadConfig: mockLoadConfig }));
|
|
29
|
+
import { runIterate } from "./iterate/index.mjs";
|
|
30
|
+
import { runCheck } from "./check.mjs";
|
|
31
|
+
import { updateReadyDelay } from "./ready-delay.mjs";
|
|
32
|
+
import { readFixAttempts, writeFixAttempts } from "../state/fix-attempts.mjs";
|
|
33
|
+
import { readStallState, writeStallState } from "../state/iterate-stall.mjs";
|
|
34
|
+
const mockRunCheck = vi.mocked(runCheck);
|
|
35
|
+
const mockUpdateReadyDelay = vi.mocked(updateReadyDelay);
|
|
36
|
+
const mockReadFixAttempts = vi.mocked(readFixAttempts);
|
|
37
|
+
const mockWriteFixAttempts = vi.mocked(writeFixAttempts);
|
|
38
|
+
const mockReadStallState = vi.mocked(readStallState);
|
|
39
|
+
const mockWriteStallState = vi.mocked(writeStallState);
|
|
40
|
+
function makeReport(overrides = {}) {
|
|
41
|
+
return {
|
|
42
|
+
pr: 42,
|
|
43
|
+
nodeId: "PR_kgDOAAA",
|
|
44
|
+
repo: "owner/repo",
|
|
45
|
+
status: "READY",
|
|
46
|
+
baseBranch: "main",
|
|
47
|
+
mergeStatus: {
|
|
48
|
+
status: "CLEAN",
|
|
49
|
+
state: "OPEN",
|
|
50
|
+
isDraft: false,
|
|
51
|
+
mergeable: "MERGEABLE",
|
|
52
|
+
reviewDecision: "APPROVED",
|
|
53
|
+
blockingBotReviewInProgress: false,
|
|
54
|
+
mergeStateStatus: "CLEAN",
|
|
55
|
+
},
|
|
56
|
+
checks: {
|
|
57
|
+
passing: [],
|
|
58
|
+
failing: [],
|
|
59
|
+
inProgress: [],
|
|
60
|
+
skipped: [],
|
|
61
|
+
filtered: [],
|
|
62
|
+
filteredNames: [],
|
|
63
|
+
blockedByFilteredCheck: false,
|
|
64
|
+
},
|
|
65
|
+
threads: {
|
|
66
|
+
actionable: [],
|
|
67
|
+
resolutionOnly: [],
|
|
68
|
+
autoResolved: [],
|
|
69
|
+
autoResolveErrors: [],
|
|
70
|
+
firstLook: [],
|
|
71
|
+
},
|
|
72
|
+
comments: { actionable: [], firstLook: [] },
|
|
73
|
+
changesRequestedReviews: [],
|
|
74
|
+
reviewSummaries: [],
|
|
75
|
+
firstLookSummaries: [],
|
|
76
|
+
editedSummaries: [],
|
|
77
|
+
approvedReviews: [],
|
|
78
|
+
...overrides,
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
function makeOpts(overrides = {}) {
|
|
82
|
+
return {
|
|
83
|
+
prNumber: 42,
|
|
84
|
+
format: "json",
|
|
85
|
+
readyDelaySeconds: 600,
|
|
86
|
+
...overrides,
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
export function registerHooks() {
|
|
90
|
+
beforeEach(() => {
|
|
91
|
+
vi.clearAllMocks();
|
|
92
|
+
mockExecFile.mockResolvedValue({ stdout: "abc1234\n", stderr: "" });
|
|
93
|
+
mockLoadConfig.mockReturnValue({
|
|
94
|
+
iterate: {
|
|
95
|
+
fixAttemptsPerThread: 3,
|
|
96
|
+
stallTimeoutMinutes: 60,
|
|
97
|
+
minimizeApprovals: false,
|
|
98
|
+
},
|
|
99
|
+
watch: { readyDelayMinutes: 10 },
|
|
100
|
+
resolve: {
|
|
101
|
+
concurrency: 4,
|
|
102
|
+
shaPoll: { intervalMs: 2000, maxAttempts: 10 },
|
|
103
|
+
fetchReviewSummaries: true,
|
|
104
|
+
},
|
|
105
|
+
checks: { ciTriggerEvents: ["pull_request"] },
|
|
106
|
+
mergeStatus: { blockingReviewerLogins: [] },
|
|
107
|
+
actions: { autoResolveOutdated: false, autoMarkReady: false, commitSuggestions: false },
|
|
108
|
+
});
|
|
109
|
+
mockReadFixAttempts.mockResolvedValue(null);
|
|
110
|
+
mockWriteFixAttempts.mockResolvedValue(undefined);
|
|
111
|
+
mockReadStallState.mockResolvedValue(null);
|
|
112
|
+
mockWriteStallState.mockResolvedValue(undefined);
|
|
113
|
+
});
|
|
114
|
+
afterEach(() => {
|
|
115
|
+
vi.restoreAllMocks();
|
|
116
|
+
});
|
|
117
|
+
}
|
|
118
|
+
export { makeOpts, makeReport, mockExecFile, mockFetch, mockLoadConfig, mockReadFixAttempts, mockReadStallState, mockRunCheck, mockUpdateReadyDelay, mockWriteFixAttempts, mockWriteStallState, readFixAttempts, readStallState, runCheck, runIterate, updateReadyDelay, writeFixAttempts, writeStallState, };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
vi.mock("../github/client.mts", () => ({
|
|
4
|
+
getRepoInfo: vi.fn().mockResolvedValue({ owner: "owner", name: "repo" }),
|
|
5
|
+
getCurrentPrNumber: vi.fn().mockResolvedValue(42),
|
|
6
|
+
}));
|
|
7
|
+
vi.mock("../state/seen-comments.mts", async (importOriginal) => {
|
|
8
|
+
const actual = await importOriginal();
|
|
9
|
+
return {
|
|
10
|
+
...actual,
|
|
11
|
+
loadSeenMap: vi.fn().mockResolvedValue(new Map()),
|
|
12
|
+
markSeen: vi.fn().mockResolvedValue(undefined),
|
|
13
|
+
};
|
|
14
|
+
});
|
|
15
|
+
vi.mock("../github/batch.mts", () => ({
|
|
16
|
+
fetchPrBatch: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("../comments/resolve.mts", () => ({
|
|
19
|
+
autoResolveOutdated: vi.fn(),
|
|
20
|
+
applyResolveOptions: vi.fn(),
|
|
21
|
+
}));
|
|
22
|
+
vi.mock("../config/load.mts", () => ({
|
|
23
|
+
loadConfig: vi.fn().mockReturnValue({
|
|
24
|
+
resolve: {
|
|
25
|
+
shaPoll: { intervalMs: 2000, maxAttempts: 10 },
|
|
26
|
+
fetchReviewSummaries: true,
|
|
27
|
+
},
|
|
28
|
+
actions: {
|
|
29
|
+
autoResolveOutdated: true,
|
|
30
|
+
autoMarkReady: true,
|
|
31
|
+
commitSuggestions: true,
|
|
32
|
+
},
|
|
33
|
+
}),
|
|
34
|
+
}));
|
|
35
|
+
import { runResolveFetch, runResolveMutate } from "./resolve.mjs";
|
|
36
|
+
import { getCurrentPrNumber } from "../github/client.mjs";
|
|
37
|
+
import { fetchPrBatch } from "../github/batch.mjs";
|
|
38
|
+
import { autoResolveOutdated, applyResolveOptions } from "../comments/resolve.mjs";
|
|
39
|
+
import { loadConfig } from "../config/load.mjs";
|
|
40
|
+
import { loadSeenMap, markSeen, hashBody } from "../state/seen-comments.mjs";
|
|
41
|
+
const mockGetCurrentPrNumber = vi.mocked(getCurrentPrNumber);
|
|
42
|
+
const mockFetchPrBatch = vi.mocked(fetchPrBatch);
|
|
43
|
+
const mockAutoResolveOutdated = vi.mocked(autoResolveOutdated);
|
|
44
|
+
const mockApplyResolveOptions = vi.mocked(applyResolveOptions);
|
|
45
|
+
const mockLoadConfig = vi.mocked(loadConfig);
|
|
46
|
+
const mockLoadSeenMap = vi.mocked(loadSeenMap);
|
|
47
|
+
const mockMarkSeen = vi.mocked(markSeen);
|
|
48
|
+
const BASE_OPTS = { format: "text" };
|
|
49
|
+
function makeBatchData(overrides = {}) {
|
|
50
|
+
return {
|
|
51
|
+
nodeId: "PR_kgDOAAA",
|
|
52
|
+
number: 42,
|
|
53
|
+
state: "OPEN",
|
|
54
|
+
isDraft: false,
|
|
55
|
+
mergeable: "MERGEABLE",
|
|
56
|
+
mergeStateStatus: "CLEAN",
|
|
57
|
+
reviewDecision: "APPROVED",
|
|
58
|
+
headRefOid: "abc123",
|
|
59
|
+
headRefName: "feature",
|
|
60
|
+
headRepoWithOwner: "owner/repo",
|
|
61
|
+
baseRefName: "main",
|
|
62
|
+
reviewRequests: [],
|
|
63
|
+
latestReviews: [],
|
|
64
|
+
reviewThreads: [],
|
|
65
|
+
comments: [],
|
|
66
|
+
changesRequestedReviews: [],
|
|
67
|
+
reviewSummaries: [],
|
|
68
|
+
approvedReviews: [],
|
|
69
|
+
checks: [],
|
|
70
|
+
...overrides,
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
function makeThread(overrides = {}) {
|
|
74
|
+
return {
|
|
75
|
+
id: "t-1",
|
|
76
|
+
isResolved: false,
|
|
77
|
+
isOutdated: false,
|
|
78
|
+
isMinimized: false,
|
|
79
|
+
path: "src/foo.ts",
|
|
80
|
+
line: 1,
|
|
81
|
+
startLine: null,
|
|
82
|
+
author: "alice",
|
|
83
|
+
authorType: "Unknown",
|
|
84
|
+
body: "fix this",
|
|
85
|
+
url: "",
|
|
86
|
+
createdAtUnix: 1_700_000_000,
|
|
87
|
+
...overrides,
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
function makeComment(overrides = {}) {
|
|
91
|
+
return {
|
|
92
|
+
id: "c-1",
|
|
93
|
+
isMinimized: false,
|
|
94
|
+
author: "bob",
|
|
95
|
+
authorType: "Unknown",
|
|
96
|
+
body: "nit",
|
|
97
|
+
url: "",
|
|
98
|
+
createdAtUnix: 1_700_000_000,
|
|
99
|
+
...overrides,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
export function registerHooks() {
|
|
103
|
+
beforeEach(() => {
|
|
104
|
+
vi.clearAllMocks();
|
|
105
|
+
mockAutoResolveOutdated.mockResolvedValue({ resolved: [], errors: [] });
|
|
106
|
+
mockApplyResolveOptions.mockResolvedValue({
|
|
107
|
+
resolvedThreads: [],
|
|
108
|
+
minimizedComments: [],
|
|
109
|
+
dismissedReviews: [],
|
|
110
|
+
errors: [],
|
|
111
|
+
});
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
export { BASE_OPTS, applyResolveOptions, autoResolveOutdated, fetchPrBatch, getCurrentPrNumber, hashBody, loadConfig, loadSeenMap, makeBatchData, makeComment, makeThread, markSeen, mockApplyResolveOptions, mockAutoResolveOutdated, mockFetchPrBatch, mockGetCurrentPrNumber, mockLoadConfig, mockLoadSeenMap, mockMarkSeen, runResolveFetch, runResolveMutate, };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, expect, it } from "vitest";
|
|
3
|
+
import { buildShepherdJournalInstruction, SHEPHERD_JOURNAL_APPEND_HINT, SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEM_HEADINGS, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEMS, SHEPHERD_JOURNAL_SECTION, SHEPHERD_JOURNAL_SECTION_PATTERN, } from "./shepherd-journal.mjs";
|
|
4
|
+
import { buildFixInstructions } from "./iterate/render.mjs";
|
|
5
|
+
import { buildFetchInstructions } from "./resolve-instructions.mjs";
|
|
6
|
+
function countMentions(text, phrase) {
|
|
7
|
+
return (text.match(new RegExp(phrase, "g")) ?? []).length;
|
|
8
|
+
}
|
|
9
|
+
export { SHEPHERD_JOURNAL_APPEND_HINT, SHEPHERD_JOURNAL_FIRST_LOOK_GUIDANCE, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEMS, SHEPHERD_JOURNAL_REFERENCE_GUIDANCE_THREADS_AND_COMMENTS_IN_ITEM_HEADINGS, SHEPHERD_JOURNAL_SECTION, SHEPHERD_JOURNAL_SECTION_PATTERN, buildFetchInstructions, buildFixInstructions, buildShepherdJournalInstruction, countMentions, };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Mock github/client.mts before any imports.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
vi.mock("../github/client.mts", () => ({
|
|
7
|
+
graphqlWithRateLimit: vi.fn(),
|
|
8
|
+
getPrHeadSha: vi.fn(),
|
|
9
|
+
}));
|
|
10
|
+
import { applyResolveOptions, autoResolveOutdated } from "./resolve.mjs";
|
|
11
|
+
import { graphqlWithRateLimit, getPrHeadSha } from "../github/client.mjs";
|
|
12
|
+
const mockGraphql = vi.mocked(graphqlWithRateLimit);
|
|
13
|
+
const mockGetPrHeadSha = vi.mocked(getPrHeadSha);
|
|
14
|
+
const REPO = { owner: "owner", name: "repo" };
|
|
15
|
+
/** Build a mock response with the correct nested shape for each alias type (r/m/d). */
|
|
16
|
+
function makeBulkResponse(doc) {
|
|
17
|
+
const str = typeof doc === "string" ? doc : "";
|
|
18
|
+
const data = {};
|
|
19
|
+
for (const [, alias] of str.matchAll(/^\s+([a-z]\d+):/gm)) {
|
|
20
|
+
if (alias.startsWith("r"))
|
|
21
|
+
data[alias] = { thread: { isResolved: true } };
|
|
22
|
+
else if (alias.startsWith("m"))
|
|
23
|
+
data[alias] = { minimizedComment: { isMinimized: true } };
|
|
24
|
+
else if (alias.startsWith("d"))
|
|
25
|
+
data[alias] = { pullRequestReview: { state: "DISMISSED" } };
|
|
26
|
+
else
|
|
27
|
+
data[alias] = {};
|
|
28
|
+
}
|
|
29
|
+
return { data };
|
|
30
|
+
}
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// applyResolveOptions
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
export function registerHooks() {
|
|
35
|
+
beforeEach(() => {
|
|
36
|
+
vi.clearAllMocks();
|
|
37
|
+
mockGraphql.mockImplementation(async (doc) => makeBulkResponse(doc));
|
|
38
|
+
});
|
|
39
|
+
}
|
|
40
|
+
export { REPO, applyResolveOptions, autoResolveOutdated, getPrHeadSha, graphqlWithRateLimit, makeBulkResponse, mockGetPrHeadSha, mockGraphql, };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
vi.mock("./client.mts", () => ({
|
|
4
|
+
graphql: vi.fn(),
|
|
5
|
+
graphqlWithRateLimit: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
import { fetchPrBatch } from "./batch.mjs";
|
|
8
|
+
import { graphql, graphqlWithRateLimit } from "./client.mjs";
|
|
9
|
+
const mockGraphql = vi.mocked(graphql);
|
|
10
|
+
const mockGraphqlWithRateLimit = vi.mocked(graphqlWithRateLimit);
|
|
11
|
+
const REPO = { owner: "owner", name: "repo" };
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function makeRawPr(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
id: "PR_kgDOAAA",
|
|
18
|
+
number: 42,
|
|
19
|
+
state: "OPEN",
|
|
20
|
+
isDraft: false,
|
|
21
|
+
mergeable: "MERGEABLE",
|
|
22
|
+
mergeStateStatus: "CLEAN",
|
|
23
|
+
reviewDecision: "APPROVED",
|
|
24
|
+
headRefOid: "abc123",
|
|
25
|
+
headRefName: "feature",
|
|
26
|
+
headRepository: { nameWithOwner: "owner/repo" },
|
|
27
|
+
baseRefName: "main",
|
|
28
|
+
reviewRequests: { nodes: [] },
|
|
29
|
+
latestReviews: { nodes: [] },
|
|
30
|
+
reviewThreads: {
|
|
31
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
32
|
+
nodes: [],
|
|
33
|
+
},
|
|
34
|
+
comments: {
|
|
35
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
36
|
+
nodes: [],
|
|
37
|
+
},
|
|
38
|
+
changesRequestedReviews: {
|
|
39
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
40
|
+
nodes: [],
|
|
41
|
+
},
|
|
42
|
+
reviewSummaries: {
|
|
43
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
44
|
+
nodes: [],
|
|
45
|
+
},
|
|
46
|
+
approvedReviews: {
|
|
47
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
48
|
+
nodes: [],
|
|
49
|
+
},
|
|
50
|
+
commits: { nodes: [{ commit: { statusCheckRollup: null } }] },
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function makeResponse(pr = makeRawPr()) {
|
|
55
|
+
return { data: { repository: { pullRequest: pr } } };
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// PR not found
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
export function registerHooks() {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
vi.clearAllMocks();
|
|
63
|
+
mockGraphql.mockResolvedValue(makeResponse());
|
|
64
|
+
mockGraphqlWithRateLimit.mockResolvedValue(makeResponse());
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export { REPO, fetchPrBatch, graphql, graphqlWithRateLimit, makeRawPr, makeResponse, mockGraphql, mockGraphqlWithRateLimit, };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
vi.mock("./client.mts", () => ({
|
|
4
|
+
graphql: vi.fn(),
|
|
5
|
+
graphqlWithRateLimit: vi.fn(),
|
|
6
|
+
}));
|
|
7
|
+
import { fetchPrBatch } from "./batch.mjs";
|
|
8
|
+
import { graphql, graphqlWithRateLimit } from "./client.mjs";
|
|
9
|
+
const mockGraphql = vi.mocked(graphql);
|
|
10
|
+
const mockGraphqlWithRateLimit = vi.mocked(graphqlWithRateLimit);
|
|
11
|
+
const REPO = { owner: "owner", name: "repo" };
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Helpers
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
function makeRawPr(overrides = {}) {
|
|
16
|
+
return {
|
|
17
|
+
id: "PR_kgDOAAA",
|
|
18
|
+
number: 42,
|
|
19
|
+
state: "OPEN",
|
|
20
|
+
isDraft: false,
|
|
21
|
+
mergeable: "MERGEABLE",
|
|
22
|
+
mergeStateStatus: "CLEAN",
|
|
23
|
+
reviewDecision: "APPROVED",
|
|
24
|
+
headRefOid: "abc123",
|
|
25
|
+
headRefName: "feature",
|
|
26
|
+
headRepository: { nameWithOwner: "owner/repo" },
|
|
27
|
+
baseRefName: "main",
|
|
28
|
+
reviewRequests: { nodes: [] },
|
|
29
|
+
latestReviews: { nodes: [] },
|
|
30
|
+
reviewThreads: {
|
|
31
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
32
|
+
nodes: [],
|
|
33
|
+
},
|
|
34
|
+
comments: {
|
|
35
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
36
|
+
nodes: [],
|
|
37
|
+
},
|
|
38
|
+
changesRequestedReviews: {
|
|
39
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
40
|
+
nodes: [],
|
|
41
|
+
},
|
|
42
|
+
reviewSummaries: {
|
|
43
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
44
|
+
nodes: [],
|
|
45
|
+
},
|
|
46
|
+
approvedReviews: {
|
|
47
|
+
pageInfo: { hasPreviousPage: false, startCursor: null },
|
|
48
|
+
nodes: [],
|
|
49
|
+
},
|
|
50
|
+
commits: { nodes: [{ commit: { statusCheckRollup: null } }] },
|
|
51
|
+
...overrides,
|
|
52
|
+
};
|
|
53
|
+
}
|
|
54
|
+
function makeResponse(pr = makeRawPr()) {
|
|
55
|
+
return { data: { repository: { pullRequest: pr } } };
|
|
56
|
+
}
|
|
57
|
+
// ---------------------------------------------------------------------------
|
|
58
|
+
// reviewSummaries — COMMENTED reviews surfaced for agent-driven minimize
|
|
59
|
+
// ---------------------------------------------------------------------------
|
|
60
|
+
export function registerHooks() {
|
|
61
|
+
beforeEach(() => {
|
|
62
|
+
vi.clearAllMocks();
|
|
63
|
+
mockGraphql.mockResolvedValue(makeResponse());
|
|
64
|
+
mockGraphqlWithRateLimit.mockResolvedValue(makeResponse());
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
export { REPO, fetchPrBatch, graphql, graphqlWithRateLimit, makeRawPr, makeResponse, mockGraphql, mockGraphqlWithRateLimit, };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { appendEntry, nextEntry } from "../log/log-file.mjs";
|
|
2
|
+
import { formatRequestEntry, formatResponseEntry } from "../log/session.mjs";
|
|
3
|
+
import { GitHubRequestError } from "./errors.mjs";
|
|
4
|
+
import { makeHeaders } from "./http-auth.mjs";
|
|
5
|
+
import { requestWithTokenRetry } from "./http-request.mjs";
|
|
6
|
+
import { parseRateLimit, parseRetryAfter, redactToken, sanitizeBody, } from "./http-utils.mjs";
|
|
7
|
+
const BASE_URL = "https://api.github.com";
|
|
8
|
+
async function graphqlInner(query, vars) {
|
|
9
|
+
const url = `${BASE_URL}/graphql`;
|
|
10
|
+
const n = nextEntry();
|
|
11
|
+
appendEntry(formatRequestEntry({
|
|
12
|
+
n,
|
|
13
|
+
kind: "GraphQL",
|
|
14
|
+
method: "POST",
|
|
15
|
+
url,
|
|
16
|
+
body: { query, variables: vars },
|
|
17
|
+
}));
|
|
18
|
+
const t0 = performance.now();
|
|
19
|
+
const { res, attempt, retryT0 } = await requestWithTokenRetry(async () => fetch(url, {
|
|
20
|
+
method: "POST",
|
|
21
|
+
headers: await makeHeaders(),
|
|
22
|
+
body: JSON.stringify({ query, variables: vars }),
|
|
23
|
+
}), t0, (status, durationMs) => appendEntry(formatResponseEntry({ n, kind: "GraphQL", method: "POST", url, status, durationMs })));
|
|
24
|
+
const durationMs = Math.round(performance.now() - retryT0);
|
|
25
|
+
const rateLimit = parseRateLimit(res.headers);
|
|
26
|
+
const retryAfterSeconds = parseRetryAfter(res.headers);
|
|
27
|
+
if (!res.ok) {
|
|
28
|
+
const body = await res.text();
|
|
29
|
+
appendEntry(formatResponseEntry({
|
|
30
|
+
n,
|
|
31
|
+
kind: "GraphQL",
|
|
32
|
+
method: "POST",
|
|
33
|
+
url,
|
|
34
|
+
status: res.status,
|
|
35
|
+
durationMs,
|
|
36
|
+
textBody: redactToken(body),
|
|
37
|
+
attempt: attempt > 1 ? attempt : undefined,
|
|
38
|
+
}));
|
|
39
|
+
throw new GitHubRequestError(`GitHub GraphQL request failed: ${res.status} ${sanitizeBody(body)}`, { status: res.status, rateLimit: rateLimit ?? undefined, retryAfterSeconds });
|
|
40
|
+
}
|
|
41
|
+
const parsed = (await res.json());
|
|
42
|
+
appendEntry(formatResponseEntry({
|
|
43
|
+
n,
|
|
44
|
+
kind: "GraphQL",
|
|
45
|
+
method: "POST",
|
|
46
|
+
url,
|
|
47
|
+
status: res.status,
|
|
48
|
+
durationMs,
|
|
49
|
+
body: parsed,
|
|
50
|
+
attempt: attempt > 1 ? attempt : undefined,
|
|
51
|
+
}));
|
|
52
|
+
if (parsed.data == null) {
|
|
53
|
+
const messages = (parsed.errors ?? []).map((e) => e.message).join("; ");
|
|
54
|
+
throw new GitHubRequestError(`GitHub GraphQL error (no data): ${messages}`, {
|
|
55
|
+
status: res.status,
|
|
56
|
+
rateLimit: rateLimit ?? undefined,
|
|
57
|
+
retryAfterSeconds,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
if (parsed.errors?.length) {
|
|
61
|
+
const messages = parsed.errors.map((e) => e.message).join("; ");
|
|
62
|
+
process.stderr.write(`pr-shepherd: GraphQL non-fatal errors: ${messages}\n`);
|
|
63
|
+
}
|
|
64
|
+
return { data: parsed.data, rateLimit, retryAfterSeconds, errors: parsed.errors };
|
|
65
|
+
}
|
|
66
|
+
export async function graphql(query, vars = {}) {
|
|
67
|
+
const { data } = await graphqlInner(query, vars);
|
|
68
|
+
return { data };
|
|
69
|
+
}
|
|
70
|
+
export async function graphqlWithRateLimit(query, vars = {}) {
|
|
71
|
+
const { data, rateLimit, retryAfterSeconds, errors } = await graphqlInner(query, vars);
|
|
72
|
+
return { data, rateLimit: rateLimit ?? undefined, retryAfterSeconds, errors };
|
|
73
|
+
}
|