pr-shepherd 0.16.2 → 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.mjs +14 -17
- 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 +20 -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/ready-mergeability.mjs +30 -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,112 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Hoisted mocks
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
const { mockExecFile } = vi.hoisted(() => ({ mockExecFile: vi.fn() }));
|
|
7
|
+
vi.mock("node:child_process", () => ({
|
|
8
|
+
execFile: (cmd, args, optsOrCb, maybeCb) => {
|
|
9
|
+
const cb = typeof optsOrCb === "function" ? optsOrCb : maybeCb;
|
|
10
|
+
mockExecFile(cmd, args)
|
|
11
|
+
.then((result) => cb(null, result))
|
|
12
|
+
.catch((err) => cb(err, { stdout: "", stderr: err.stderr ?? "" }));
|
|
13
|
+
},
|
|
14
|
+
}));
|
|
15
|
+
vi.mock("node:fs/promises", () => ({
|
|
16
|
+
readFile: vi.fn(),
|
|
17
|
+
}));
|
|
18
|
+
vi.mock("../github/client.mts", () => ({
|
|
19
|
+
getRepoInfo: vi.fn().mockResolvedValue({ owner: "owner", name: "repo" }),
|
|
20
|
+
getCurrentPrNumber: vi.fn().mockResolvedValue(42),
|
|
21
|
+
getCurrentBranch: vi.fn().mockResolvedValue("feature/foo"),
|
|
22
|
+
}));
|
|
23
|
+
vi.mock("../github/batch.mts", () => ({
|
|
24
|
+
fetchPrBatch: vi.fn(),
|
|
25
|
+
}));
|
|
26
|
+
const { mockLoadConfig } = vi.hoisted(() => ({ mockLoadConfig: vi.fn() }));
|
|
27
|
+
vi.mock("../config/load.mts", () => ({
|
|
28
|
+
loadConfig: mockLoadConfig,
|
|
29
|
+
}));
|
|
30
|
+
import { runCommitSuggestion } from "./commit-suggestion.mjs";
|
|
31
|
+
import { getCurrentBranch, getCurrentPrNumber } from "../github/client.mjs";
|
|
32
|
+
import { fetchPrBatch } from "../github/batch.mjs";
|
|
33
|
+
import { readFile } from "node:fs/promises";
|
|
34
|
+
const mockGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
35
|
+
const mockGetCurrentPrNumber = vi.mocked(getCurrentPrNumber);
|
|
36
|
+
const mockFetchBatch = vi.mocked(fetchPrBatch);
|
|
37
|
+
const mockReadFile = vi.mocked(readFile);
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
// Fixtures
|
|
40
|
+
// ---------------------------------------------------------------------------
|
|
41
|
+
function makeThread(overrides = {}) {
|
|
42
|
+
return {
|
|
43
|
+
id: "PRRT_x",
|
|
44
|
+
isResolved: false,
|
|
45
|
+
isOutdated: false,
|
|
46
|
+
isMinimized: false,
|
|
47
|
+
path: "src/foo.ts",
|
|
48
|
+
line: 5,
|
|
49
|
+
startLine: null,
|
|
50
|
+
author: "alice",
|
|
51
|
+
authorType: "Unknown",
|
|
52
|
+
body: "Use a const here.\n\n```suggestion\nconst x = 10;\n```",
|
|
53
|
+
url: "",
|
|
54
|
+
createdAtUnix: 0,
|
|
55
|
+
...overrides,
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
function makeBatch(threads, headRepoWithOwner = "owner/repo") {
|
|
59
|
+
return {
|
|
60
|
+
nodeId: "PR_kgDOAAA",
|
|
61
|
+
number: 42,
|
|
62
|
+
state: "OPEN",
|
|
63
|
+
isDraft: false,
|
|
64
|
+
mergeable: "MERGEABLE",
|
|
65
|
+
mergeStateStatus: "CLEAN",
|
|
66
|
+
reviewDecision: "APPROVED",
|
|
67
|
+
headRefOid: "headsha",
|
|
68
|
+
headRefName: "feature/foo",
|
|
69
|
+
headRepoWithOwner,
|
|
70
|
+
baseRefName: "main",
|
|
71
|
+
reviewRequests: [],
|
|
72
|
+
latestReviews: [],
|
|
73
|
+
reviewThreads: threads,
|
|
74
|
+
checks: [],
|
|
75
|
+
comments: [],
|
|
76
|
+
changesRequestedReviews: [],
|
|
77
|
+
reviewSummaries: [],
|
|
78
|
+
approvedReviews: [],
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
const FILE_CONTENT = "line1\n" +
|
|
82
|
+
"line2\n" +
|
|
83
|
+
"line3\n" +
|
|
84
|
+
"line4\n" +
|
|
85
|
+
"const x = 1;\n" + // line 5 — matches the suggestion anchor
|
|
86
|
+
"line6\n" +
|
|
87
|
+
"line7\n";
|
|
88
|
+
const GLOBAL_OPTS = { format: "text" };
|
|
89
|
+
function makeGitSuccess(stdout = "") {
|
|
90
|
+
return Promise.resolve({ stdout, stderr: "" });
|
|
91
|
+
}
|
|
92
|
+
function setupHappyPath() {
|
|
93
|
+
mockFetchBatch.mockResolvedValue({ data: makeBatch([makeThread()]) });
|
|
94
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
95
|
+
mockReadFile.mockResolvedValue(FILE_CONTENT);
|
|
96
|
+
mockExecFile.mockImplementation((cmd, args) => {
|
|
97
|
+
if (cmd === "git" && args[0] === "rev-parse")
|
|
98
|
+
return makeGitSuccess("headsha\n");
|
|
99
|
+
if (cmd === "git" && args[0] === "status")
|
|
100
|
+
return makeGitSuccess(""); // file is clean
|
|
101
|
+
throw new Error(`Unexpected execFile call: ${cmd} ${args.join(" ")}`);
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
// ---------------------------------------------------------------------------
|
|
105
|
+
// Validation
|
|
106
|
+
// ---------------------------------------------------------------------------
|
|
107
|
+
export function registerHooks() {
|
|
108
|
+
beforeEach(() => {
|
|
109
|
+
mockLoadConfig.mockReturnValue({ cli: { runner: "auto" } });
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
export { FILE_CONTENT, GLOBAL_OPTS, fetchPrBatch, getCurrentBranch, getCurrentPrNumber, makeBatch, makeGitSuccess, makeThread, mockExecFile, mockFetchBatch, mockGetCurrentBranch, mockGetCurrentPrNumber, mockLoadConfig, mockReadFile, readFile, runCommitSuggestion, setupHappyPath, };
|
|
@@ -29,6 +29,7 @@ export async function runIterate(opts) {
|
|
|
29
29
|
const stallKey = { owner: repoOwner, repo: repoName, pr: prNumber };
|
|
30
30
|
if (report.mergeStatus.state !== "OPEN") {
|
|
31
31
|
const state = report.mergeStatus.state.toLowerCase();
|
|
32
|
+
await updateReadyDelay(report.pr, false, readyDelaySeconds, repoOwner, repoName);
|
|
32
33
|
await clearStallState(stallKey);
|
|
33
34
|
return {
|
|
34
35
|
pr: report.pr,
|
|
@@ -50,8 +51,25 @@ export async function runIterate(opts) {
|
|
|
50
51
|
log: `CANCEL: PR #${report.pr} is ${state} — stopping`,
|
|
51
52
|
};
|
|
52
53
|
}
|
|
53
|
-
const
|
|
54
|
-
|
|
54
|
+
const { minimizeIds: reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, } = classifyReviewSummaries({
|
|
55
|
+
firstLook: report.firstLookSummaries,
|
|
56
|
+
seen: report.reviewSummaries,
|
|
57
|
+
edited: report.editedSummaries,
|
|
58
|
+
}, report.approvedReviews, config.iterate.minimizeApprovals, config.iterate.minimizeComments);
|
|
59
|
+
const hasActionableWork = report.threads.actionable.length > 0 ||
|
|
60
|
+
report.threads.resolutionOnly.length > 0 ||
|
|
61
|
+
report.threads.firstLook.length > 0 ||
|
|
62
|
+
report.comments.actionable.length > 0 ||
|
|
63
|
+
(report.comments.minimizeIds?.length ?? 0) > 0 ||
|
|
64
|
+
report.comments.firstLook.length > 0 ||
|
|
65
|
+
report.changesRequestedReviews.length > 0 ||
|
|
66
|
+
report.checks.failing.length > 0 ||
|
|
67
|
+
report.mergeStatus.status === "CONFLICTS" ||
|
|
68
|
+
reviewSummaryIds.length > 0 ||
|
|
69
|
+
firstLookSummaries.length > 0 ||
|
|
70
|
+
editedSummaries.length > 0;
|
|
71
|
+
const isCleanReadyHandoff = report.status === "READY" && !hasActionableWork;
|
|
72
|
+
const readyState = await updateReadyDelay(report.pr, isCleanReadyHandoff, readyDelaySeconds, repoOwner, repoName);
|
|
55
73
|
const base = {
|
|
56
74
|
pr: report.pr,
|
|
57
75
|
repo: report.repo,
|
|
@@ -87,20 +105,6 @@ export async function runIterate(opts) {
|
|
|
87
105
|
};
|
|
88
106
|
}
|
|
89
107
|
const headSha = (await getCurrentHeadSha()) ?? "unknown";
|
|
90
|
-
const { minimizeIds: reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, } = classifyReviewSummaries({
|
|
91
|
-
firstLook: report.firstLookSummaries,
|
|
92
|
-
seen: report.reviewSummaries,
|
|
93
|
-
edited: report.editedSummaries,
|
|
94
|
-
}, report.approvedReviews, config.iterate.minimizeApprovals, config.iterate.minimizeComments);
|
|
95
|
-
const hasActionableWork = report.threads.actionable.length > 0 ||
|
|
96
|
-
report.threads.resolutionOnly.length > 0 ||
|
|
97
|
-
report.comments.actionable.length > 0 ||
|
|
98
|
-
report.changesRequestedReviews.length > 0 ||
|
|
99
|
-
report.checks.failing.length > 0 ||
|
|
100
|
-
report.mergeStatus.status === "CONFLICTS" ||
|
|
101
|
-
reviewSummaryIds.length > 0 ||
|
|
102
|
-
firstLookSummaries.length > 0 ||
|
|
103
|
-
editedSummaries.length > 0;
|
|
104
108
|
if (hasActionableWork) {
|
|
105
109
|
return handleFixCode({
|
|
106
110
|
base,
|
|
@@ -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,30 @@
|
|
|
1
|
+
import { getMergeableState } from "../github/client.mjs";
|
|
2
|
+
import { deriveMergeStatus } from "../merge-status/derive.mjs";
|
|
3
|
+
import { computeStatus } from "./check-status.mjs";
|
|
4
|
+
export async function refreshUnknownMergeability(prNumber, repo, batchData) {
|
|
5
|
+
if ((batchData.state ?? "OPEN") !== "OPEN" ||
|
|
6
|
+
(batchData.mergeable !== "UNKNOWN" && batchData.mergeStateStatus !== "UNKNOWN")) {
|
|
7
|
+
return { batchData, didRefresh: false };
|
|
8
|
+
}
|
|
9
|
+
return { batchData: await readMergeability(prNumber, repo, batchData), didRefresh: true };
|
|
10
|
+
}
|
|
11
|
+
export async function refreshReadyMergeability(prNumber, repo, batchData, verdict, unresolvedThreads, unresolvedComments) {
|
|
12
|
+
const refreshedBatchData = await readMergeability(prNumber, repo, batchData);
|
|
13
|
+
const mergeStatus = deriveMergeStatus(refreshedBatchData);
|
|
14
|
+
const status = computeStatus(verdict, unresolvedThreads, unresolvedComments, mergeStatus, refreshedBatchData.changesRequestedReviews.length);
|
|
15
|
+
return { batchData: refreshedBatchData, mergeStatus, status };
|
|
16
|
+
}
|
|
17
|
+
export function isBlockedByFilteredCheck(mergeStatus, verdict) {
|
|
18
|
+
return (mergeStatus.status === "BLOCKED" &&
|
|
19
|
+
!verdict.anyFailing &&
|
|
20
|
+
!verdict.anyInProgress &&
|
|
21
|
+
verdict.filteredNames.length > 0);
|
|
22
|
+
}
|
|
23
|
+
async function readMergeability(prNumber, repo, batchData) {
|
|
24
|
+
const restState = await getMergeableState(prNumber, repo.owner, repo.name);
|
|
25
|
+
return {
|
|
26
|
+
...batchData,
|
|
27
|
+
mergeable: restState.mergeable ?? batchData.mergeable,
|
|
28
|
+
mergeStateStatus: restState.mergeStateStatus ?? batchData.mergeStateStatus,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
@@ -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, };
|