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,61 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Stub fetch globally so http.mts uses our mock.
|
|
5
|
+
// ---------------------------------------------------------------------------
|
|
6
|
+
const mockFetch = vi.fn();
|
|
7
|
+
vi.stubGlobal("fetch", mockFetch);
|
|
8
|
+
import { fetchStartupFailureChecks, triageFailingChecks } from "./triage.mjs";
|
|
9
|
+
import { mergeStartupFailureChecks } from "./startup-failures.mjs";
|
|
10
|
+
const REPO = { owner: "owner", name: "repo" };
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// Helpers
|
|
13
|
+
// ---------------------------------------------------------------------------
|
|
14
|
+
function makeCheck(overrides = {}) {
|
|
15
|
+
return {
|
|
16
|
+
name: "tests",
|
|
17
|
+
status: "COMPLETED",
|
|
18
|
+
conclusion: "FAILURE",
|
|
19
|
+
detailsUrl: "https://github.com/owner/repo/actions/runs/99/jobs/1",
|
|
20
|
+
event: "pull_request",
|
|
21
|
+
runId: "run-99",
|
|
22
|
+
category: "failing",
|
|
23
|
+
...overrides,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function makeJobsResponse(jobs) {
|
|
27
|
+
return {
|
|
28
|
+
ok: true,
|
|
29
|
+
status: 200,
|
|
30
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
31
|
+
json: () => Promise.resolve({ jobs }),
|
|
32
|
+
text: () => Promise.resolve(JSON.stringify({ jobs })),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
function makeErrorResponse(status) {
|
|
36
|
+
return {
|
|
37
|
+
ok: false,
|
|
38
|
+
status,
|
|
39
|
+
headers: new Headers(),
|
|
40
|
+
text: () => Promise.resolve("error"),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
function makeWorkflowRunsResponse(runs) {
|
|
44
|
+
return {
|
|
45
|
+
ok: true,
|
|
46
|
+
status: 200,
|
|
47
|
+
headers: new Headers({ "content-type": "application/json" }),
|
|
48
|
+
json: () => Promise.resolve({ workflow_runs: runs }),
|
|
49
|
+
text: () => Promise.resolve(JSON.stringify({ workflow_runs: runs })),
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
// Tests
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
export function registerHooks() {
|
|
56
|
+
beforeEach(() => {
|
|
57
|
+
mockFetch.mockReset();
|
|
58
|
+
process.env["GH_TOKEN"] = "test-token";
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
export { REPO, fetchStartupFailureChecks, makeCheck, makeErrorResponse, makeJobsResponse, makeWorkflowRunsResponse, mergeStartupFailureChecks, mockFetch, triageFailingChecks, };
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect } from "vitest";
|
|
3
|
+
import { projectIterateLean, projectIterateVerbose } from "./iterate-lean.mjs";
|
|
4
|
+
import { makeIterateResult } from "../cli-parser.iterate-fixtures.mjs";
|
|
5
|
+
export { makeIterateResult, projectIterateLean, projectIterateVerbose };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
|
|
4
|
+
vi.mock("./commands/resolve.mts", () => ({
|
|
5
|
+
runResolveFetch: vi.fn(),
|
|
6
|
+
runResolveMutate: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("./commands/commit-suggestion.mts", () => ({
|
|
9
|
+
runCommitSuggestion: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("./commands/iterate/index.mts", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal();
|
|
13
|
+
return { ...actual, runIterate: vi.fn() };
|
|
14
|
+
});
|
|
15
|
+
vi.mock("./github/client.mts", () => ({
|
|
16
|
+
getRepoInfo: vi.fn().mockResolvedValue({ owner: "owner", name: "repo" }),
|
|
17
|
+
}));
|
|
18
|
+
import { main } from "./cli-parser.mjs";
|
|
19
|
+
import { runCommitSuggestion } from "./commands/commit-suggestion.mjs";
|
|
20
|
+
const mockRunCommitSuggestion = vi.mocked(runCommitSuggestion);
|
|
21
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
22
|
+
let stdoutSpy;
|
|
23
|
+
let stderrSpy;
|
|
24
|
+
function getStdout() {
|
|
25
|
+
return stdoutSpy.mock.calls.map((c) => c[0]).join("");
|
|
26
|
+
}
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
// Fixtures
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
const SUGGESTION_RESULT = {
|
|
31
|
+
pr: 42,
|
|
32
|
+
repo: "owner/repo",
|
|
33
|
+
threadId: "t1",
|
|
34
|
+
path: "a.ts",
|
|
35
|
+
startLine: 5,
|
|
36
|
+
endLine: 5,
|
|
37
|
+
author: "alice",
|
|
38
|
+
patch: "--- a/a.ts\n+++ b/a.ts\n@@ -5,1 +5,1 @@\n-old\n+new\n",
|
|
39
|
+
commitMessage: "apply fix",
|
|
40
|
+
commitBody: "Co-authored-by: alice <alice@users.noreply.github.com>",
|
|
41
|
+
filesToStage: ["a.ts"],
|
|
42
|
+
postActionInstructions: [
|
|
43
|
+
"Apply the patch to `a.ts`: run `git apply` with the diff shown above.",
|
|
44
|
+
"Stage the file: `git add -- a.ts`",
|
|
45
|
+
'Commit: `git commit -m "apply fix" -m "Co-authored-by: alice <alice@users.noreply.github.com>"`',
|
|
46
|
+
"Resolve the thread on GitHub: `npx pr-shepherd resolve 42 --resolve-thread-ids t1`",
|
|
47
|
+
"Push when ready: `git push` (or `git push --force-with-lease` after rebasing).",
|
|
48
|
+
],
|
|
49
|
+
};
|
|
50
|
+
// ---------------------------------------------------------------------------
|
|
51
|
+
// commit-suggestion dispatch
|
|
52
|
+
// ---------------------------------------------------------------------------
|
|
53
|
+
export function registerHooks() {
|
|
54
|
+
beforeEach(() => {
|
|
55
|
+
vi.clearAllMocks();
|
|
56
|
+
process.exitCode = undefined;
|
|
57
|
+
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
58
|
+
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
59
|
+
});
|
|
60
|
+
afterEach(() => {
|
|
61
|
+
process.exitCode = undefined;
|
|
62
|
+
stdoutSpy.mockRestore();
|
|
63
|
+
stderrSpy.mockRestore();
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
export { SUGGESTION_RESULT, getStdout, main, mockRunCommitSuggestion, runCommitSuggestion, stderrSpy, stdoutSpy, };
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
|
|
4
|
+
vi.mock("./commands/resolve.mts", () => ({
|
|
5
|
+
runResolveFetch: vi.fn(),
|
|
6
|
+
runResolveMutate: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("./commands/commit-suggestion.mts", () => ({
|
|
9
|
+
runCommitSuggestion: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("./commands/iterate/index.mts", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal();
|
|
13
|
+
return { ...actual, runIterate: vi.fn() };
|
|
14
|
+
});
|
|
15
|
+
vi.mock("./github/client.mts", () => ({
|
|
16
|
+
getRepoInfo: vi.fn().mockResolvedValue({ owner: "owner", name: "repo" }),
|
|
17
|
+
}));
|
|
18
|
+
import { main } from "./cli-parser.mjs";
|
|
19
|
+
import { runIterate } from "./commands/iterate/index.mjs";
|
|
20
|
+
import { makeIterateResult } from "./cli-parser.iterate-fixtures.mjs";
|
|
21
|
+
const mockRunIterate = vi.mocked(runIterate);
|
|
22
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
23
|
+
let stdoutSpy;
|
|
24
|
+
let stderrSpy;
|
|
25
|
+
function getStdout() {
|
|
26
|
+
return stdoutSpy.mock.calls.map((c) => c[0]).join("");
|
|
27
|
+
}
|
|
28
|
+
// ---------------------------------------------------------------------------
|
|
29
|
+
// formatIterateResult — fix_code actions and ## Checks section
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
export function registerHooks() {
|
|
32
|
+
beforeEach(() => {
|
|
33
|
+
vi.clearAllMocks();
|
|
34
|
+
process.exitCode = undefined;
|
|
35
|
+
delete process.env.AGENT;
|
|
36
|
+
delete process.env.CODEX_CI;
|
|
37
|
+
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
38
|
+
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
39
|
+
});
|
|
40
|
+
afterEach(() => {
|
|
41
|
+
process.exitCode = undefined;
|
|
42
|
+
delete process.env.AGENT;
|
|
43
|
+
delete process.env.CODEX_CI;
|
|
44
|
+
stdoutSpy.mockRestore();
|
|
45
|
+
stderrSpy.mockRestore();
|
|
46
|
+
});
|
|
47
|
+
}
|
|
48
|
+
export { getStdout, main, makeIterateResult, mockRunIterate, runIterate, stderrSpy, stdoutSpy };
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
3
|
+
vi.mock("./commands/check.mts", () => ({ runCheck: vi.fn() }));
|
|
4
|
+
vi.mock("./commands/resolve.mts", () => ({
|
|
5
|
+
runResolveFetch: vi.fn(),
|
|
6
|
+
runResolveMutate: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("./commands/commit-suggestion.mts", () => ({
|
|
9
|
+
runCommitSuggestion: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("./commands/iterate/index.mts", async (importOriginal) => {
|
|
12
|
+
const actual = await importOriginal();
|
|
13
|
+
return { ...actual, runIterate: vi.fn() };
|
|
14
|
+
});
|
|
15
|
+
import { main } from "./cli-parser.mjs";
|
|
16
|
+
import { runIterate } from "./commands/iterate/index.mjs";
|
|
17
|
+
import { formatIterateResult } from "./cli/iterate-formatter.mjs";
|
|
18
|
+
import { makeIterateResult } from "./cli-parser.iterate-fixtures.mjs";
|
|
19
|
+
const mockRunIterate = vi.mocked(runIterate);
|
|
20
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
21
|
+
let stdoutSpy;
|
|
22
|
+
let stderrSpy;
|
|
23
|
+
function getStdout() {
|
|
24
|
+
return stdoutSpy.mock.calls.map((c) => c[0]).join("");
|
|
25
|
+
}
|
|
26
|
+
function getStderr() {
|
|
27
|
+
return stderrSpy.mock.calls.map((c) => c[0]).join("");
|
|
28
|
+
}
|
|
29
|
+
// ---------------------------------------------------------------------------
|
|
30
|
+
// iterate dispatch
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
export function registerHooks() {
|
|
33
|
+
beforeEach(() => {
|
|
34
|
+
vi.clearAllMocks();
|
|
35
|
+
process.exitCode = undefined;
|
|
36
|
+
delete process.env.AGENT;
|
|
37
|
+
delete process.env.CODEX_CI;
|
|
38
|
+
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
39
|
+
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
40
|
+
});
|
|
41
|
+
afterEach(() => {
|
|
42
|
+
process.exitCode = undefined;
|
|
43
|
+
delete process.env.AGENT;
|
|
44
|
+
delete process.env.CODEX_CI;
|
|
45
|
+
stdoutSpy.mockRestore();
|
|
46
|
+
stderrSpy.mockRestore();
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
export { formatIterateResult, getStderr, getStdout, main, makeIterateResult, mockRunIterate, runIterate, stderrSpy, stdoutSpy, };
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
|
4
|
+
vi.mock("./commands/resolve.mts", () => ({
|
|
5
|
+
runResolveFetch: vi.fn(),
|
|
6
|
+
runResolveMutate: vi.fn(),
|
|
7
|
+
}));
|
|
8
|
+
vi.mock("./commands/log-file.mts", () => ({
|
|
9
|
+
runLogFile: vi.fn(),
|
|
10
|
+
}));
|
|
11
|
+
vi.mock("./commands/commit-suggestion.mts", () => ({
|
|
12
|
+
runCommitSuggestion: vi.fn(),
|
|
13
|
+
}));
|
|
14
|
+
vi.mock("./commands/iterate/index.mts", async (importOriginal) => {
|
|
15
|
+
const actual = await importOriginal();
|
|
16
|
+
return { ...actual, runIterate: vi.fn() };
|
|
17
|
+
});
|
|
18
|
+
import { main } from "./cli-parser.mjs";
|
|
19
|
+
import { runLogFile } from "./commands/log-file.mjs";
|
|
20
|
+
import { runResolveFetch, runResolveMutate } from "./commands/resolve.mjs";
|
|
21
|
+
const mockRunResolveFetch = vi.mocked(runResolveFetch);
|
|
22
|
+
const mockRunResolveMutate = vi.mocked(runResolveMutate);
|
|
23
|
+
const mockRunLogFile = vi.mocked(runLogFile);
|
|
24
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
25
|
+
let stdoutSpy;
|
|
26
|
+
let stderrSpy;
|
|
27
|
+
function getStdout() {
|
|
28
|
+
return stdoutSpy.mock.calls.map((c) => c[0]).join("");
|
|
29
|
+
}
|
|
30
|
+
export function registerHooks() {
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
vi.clearAllMocks();
|
|
33
|
+
process.exitCode = undefined;
|
|
34
|
+
stdoutSpy = vi.spyOn(process.stdout, "write").mockImplementation(() => true);
|
|
35
|
+
stderrSpy = vi.spyOn(process.stderr, "write").mockImplementation(() => true);
|
|
36
|
+
});
|
|
37
|
+
afterEach(() => {
|
|
38
|
+
process.exitCode = undefined;
|
|
39
|
+
stdoutSpy.mockRestore();
|
|
40
|
+
stderrSpy.mockRestore();
|
|
41
|
+
});
|
|
42
|
+
}
|
|
43
|
+
export { getStdout, main, mockRunLogFile, mockRunResolveFetch, mockRunResolveMutate, readFileSync, runLogFile, runResolveFetch, runResolveMutate, stderrSpy, stdoutSpy, };
|
package/bin/commands/check.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { fetchPrBatch } from "../github/batch.mjs";
|
|
2
|
-
import { getRepoInfo, getCurrentPrNumber
|
|
2
|
+
import { getRepoInfo, getCurrentPrNumber } from "../github/client.mjs";
|
|
3
3
|
import { classifyChecks, getCiVerdict } from "../checks/classify.mjs";
|
|
4
4
|
import { mergeStartupFailureChecks } from "../checks/startup-failures.mjs";
|
|
5
5
|
import { fetchStartupFailureChecks, triageFailingChecks } from "../checks/triage.mjs";
|
|
@@ -10,6 +10,7 @@ import { loadConfig } from "../config/load.mjs";
|
|
|
10
10
|
import { classifyVisibleComments } from "../comments/visible-comments.mjs";
|
|
11
11
|
import { computeStatus } from "./check-status.mjs";
|
|
12
12
|
import { buildTerminalReport } from "./check-terminal-report.mjs";
|
|
13
|
+
import { isBlockedByFilteredCheck, refreshReadyMergeability, refreshUnknownMergeability, } from "./ready-mergeability.mjs";
|
|
13
14
|
import { loadSeenMap, markSeen, classifyItem } from "../state/seen-comments.mjs";
|
|
14
15
|
export async function runCheck(opts) {
|
|
15
16
|
const repo = await getRepoInfo();
|
|
@@ -21,17 +22,10 @@ export async function runCheck(opts) {
|
|
|
21
22
|
const paginateApprovedReviews = config.iterate.minimizeApprovals;
|
|
22
23
|
const result = await fetchPrBatch(prNumber, repo, { paginateApprovedReviews });
|
|
23
24
|
let batchData = result.data;
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
batchData = {
|
|
29
|
-
...batchData,
|
|
30
|
-
mergeable: restState.mergeable ?? batchData.mergeable,
|
|
31
|
-
mergeStateStatus: restState.mergeStateStatus ?? batchData.mergeStateStatus,
|
|
32
|
-
};
|
|
33
|
-
}
|
|
34
|
-
const mergeStatus = deriveMergeStatus(batchData);
|
|
25
|
+
const unknownRefresh = await refreshUnknownMergeability(prNumber, repo, batchData);
|
|
26
|
+
batchData = unknownRefresh.batchData;
|
|
27
|
+
const didRefreshMergeability = unknownRefresh.didRefresh;
|
|
28
|
+
let mergeStatus = deriveMergeStatus(batchData);
|
|
35
29
|
if (mergeStatus.state === "MERGED" || mergeStatus.state === "CLOSED") {
|
|
36
30
|
return buildTerminalReport(prNumber, repo, batchData, mergeStatus, mergeStatus.state);
|
|
37
31
|
}
|
|
@@ -116,11 +110,14 @@ export async function runCheck(opts) {
|
|
|
116
110
|
...[...firstLookSummaries, ...editedSummaries].map((r) => markSeen(stateKey, r.id, r.body)),
|
|
117
111
|
]);
|
|
118
112
|
const resolutionOnlyThreads = unresolvedThreads.filter((t) => !autoResolvedIds.has(t.id) && (t.isOutdated || t.isMinimized));
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
113
|
+
let status = computeStatus(verdict, activeThreads.length + resolutionOnlyThreads.length, visibleCommentClassification.actionable.length, mergeStatus, batchData.changesRequestedReviews.length);
|
|
114
|
+
if (status === "READY" && !didRefreshMergeability) {
|
|
115
|
+
const refreshed = await refreshReadyMergeability(prNumber, repo, batchData, verdict, activeThreads.length + resolutionOnlyThreads.length, visibleCommentClassification.actionable.length);
|
|
116
|
+
batchData = refreshed.batchData;
|
|
117
|
+
mergeStatus = refreshed.mergeStatus;
|
|
118
|
+
status = refreshed.status;
|
|
119
|
+
}
|
|
120
|
+
const blockedByFilteredCheck = isBlockedByFilteredCheck(mergeStatus, verdict);
|
|
124
121
|
return {
|
|
125
122
|
pr: prNumber,
|
|
126
123
|
nodeId: batchData.nodeId,
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
// @ts-nocheck
|
|
2
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
3
|
+
vi.mock("../github/batch.mts", () => ({ fetchPrBatch: vi.fn() }));
|
|
4
|
+
vi.mock("../github/client.mts", () => ({
|
|
5
|
+
getRepoInfo: vi.fn().mockResolvedValue({ owner: "owner", name: "repo" }),
|
|
6
|
+
getCurrentPrNumber: vi.fn().mockResolvedValue(42),
|
|
7
|
+
getMergeableState: vi.fn(),
|
|
8
|
+
}));
|
|
9
|
+
vi.mock("../checks/triage.mts", () => ({
|
|
10
|
+
triageFailingChecks: vi.fn((checks) => Promise.resolve(checks)),
|
|
11
|
+
fetchStartupFailureChecks: vi.fn().mockResolvedValue([]),
|
|
12
|
+
}));
|
|
13
|
+
vi.mock("../comments/resolve.mts", () => ({
|
|
14
|
+
autoResolveOutdated: vi.fn().mockResolvedValue({ resolved: [], errors: [] }),
|
|
15
|
+
}));
|
|
16
|
+
vi.mock("../state/seen-comments.mts", async (importOriginal) => {
|
|
17
|
+
const actual = await importOriginal();
|
|
18
|
+
return {
|
|
19
|
+
...actual,
|
|
20
|
+
loadSeenMap: vi.fn().mockResolvedValue(new Map()),
|
|
21
|
+
markSeen: vi.fn().mockResolvedValue(undefined),
|
|
22
|
+
};
|
|
23
|
+
});
|
|
24
|
+
const { mockLoadConfig } = vi.hoisted(() => ({ mockLoadConfig: vi.fn() }));
|
|
25
|
+
vi.mock("../config/load.mts", () => ({ loadConfig: mockLoadConfig }));
|
|
26
|
+
import { runCheck } from "./check.mjs";
|
|
27
|
+
import { fetchPrBatch } from "../github/batch.mjs";
|
|
28
|
+
import { getCurrentPrNumber, getMergeableState } from "../github/client.mjs";
|
|
29
|
+
import { fetchStartupFailureChecks, triageFailingChecks } from "../checks/triage.mjs";
|
|
30
|
+
import { loadSeenMap, markSeen, hashBody } from "../state/seen-comments.mjs";
|
|
31
|
+
import { autoResolveOutdated } from "../comments/resolve.mjs";
|
|
32
|
+
const mockFetchPrBatch = vi.mocked(fetchPrBatch);
|
|
33
|
+
const mockGetCurrentPrNumber = vi.mocked(getCurrentPrNumber);
|
|
34
|
+
const mockGetMergeableState = vi.mocked(getMergeableState);
|
|
35
|
+
const mockTriageFailingChecks = vi.mocked(triageFailingChecks);
|
|
36
|
+
const mockFetchStartupFailureChecks = vi.mocked(fetchStartupFailureChecks);
|
|
37
|
+
const mockLoadSeenMap = vi.mocked(loadSeenMap);
|
|
38
|
+
const mockMarkSeen = vi.mocked(markSeen);
|
|
39
|
+
const mockAutoResolveOutdated = vi.mocked(autoResolveOutdated);
|
|
40
|
+
const BASE_OPTS = { format: "text" };
|
|
41
|
+
function defaultConfig() {
|
|
42
|
+
return {
|
|
43
|
+
iterate: {
|
|
44
|
+
fixAttemptsPerThread: 3,
|
|
45
|
+
stallTimeoutMinutes: 30,
|
|
46
|
+
minimizeApprovals: false,
|
|
47
|
+
minimizeComments: "all",
|
|
48
|
+
},
|
|
49
|
+
watch: { readyDelayMinutes: 10 },
|
|
50
|
+
resolve: {
|
|
51
|
+
shaPoll: { intervalMs: 2000, maxAttempts: 10 },
|
|
52
|
+
fetchReviewSummaries: true,
|
|
53
|
+
},
|
|
54
|
+
checks: {
|
|
55
|
+
ciTriggerEvents: ["pull_request", "pull_request_target"],
|
|
56
|
+
},
|
|
57
|
+
mergeStatus: { blockingReviewerLogins: ["copilot"] },
|
|
58
|
+
actions: {
|
|
59
|
+
autoResolveOutdated: true,
|
|
60
|
+
autoMarkReady: true,
|
|
61
|
+
commitSuggestions: true,
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
function makeCheck(overrides = {}) {
|
|
66
|
+
return {
|
|
67
|
+
name: "tests",
|
|
68
|
+
status: "COMPLETED",
|
|
69
|
+
conclusion: "SUCCESS",
|
|
70
|
+
detailsUrl: "",
|
|
71
|
+
event: "pull_request",
|
|
72
|
+
runId: null,
|
|
73
|
+
category: "passed",
|
|
74
|
+
...overrides,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function makeBatchData(overrides = {}) {
|
|
78
|
+
return {
|
|
79
|
+
nodeId: "PR_kgDOAAA",
|
|
80
|
+
number: 42,
|
|
81
|
+
state: "OPEN",
|
|
82
|
+
isDraft: false,
|
|
83
|
+
mergeable: "MERGEABLE",
|
|
84
|
+
mergeStateStatus: "CLEAN",
|
|
85
|
+
reviewDecision: "APPROVED",
|
|
86
|
+
headRefOid: "abc123",
|
|
87
|
+
headRefName: "feature",
|
|
88
|
+
headRepoWithOwner: "owner/repo",
|
|
89
|
+
baseRefName: "main",
|
|
90
|
+
reviewRequests: [],
|
|
91
|
+
latestReviews: [],
|
|
92
|
+
reviewThreads: [],
|
|
93
|
+
comments: [],
|
|
94
|
+
changesRequestedReviews: [],
|
|
95
|
+
reviewSummaries: [],
|
|
96
|
+
approvedReviews: [],
|
|
97
|
+
checks: [makeCheck()],
|
|
98
|
+
...overrides,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
function makeThread(overrides = {}) {
|
|
102
|
+
return {
|
|
103
|
+
id: "t1",
|
|
104
|
+
isResolved: false,
|
|
105
|
+
isOutdated: false,
|
|
106
|
+
isMinimized: false,
|
|
107
|
+
path: "src/foo.mts",
|
|
108
|
+
line: 10,
|
|
109
|
+
startLine: null,
|
|
110
|
+
author: "reviewer",
|
|
111
|
+
authorType: "Unknown",
|
|
112
|
+
body: "fix this",
|
|
113
|
+
url: "",
|
|
114
|
+
createdAtUnix: 0,
|
|
115
|
+
...overrides,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
function makeComment(overrides = {}) {
|
|
119
|
+
return {
|
|
120
|
+
id: "c1",
|
|
121
|
+
author: "commenter",
|
|
122
|
+
authorType: "Unknown",
|
|
123
|
+
body: "comment body",
|
|
124
|
+
url: "",
|
|
125
|
+
createdAtUnix: 0,
|
|
126
|
+
isMinimized: false,
|
|
127
|
+
...overrides,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
// No PR found
|
|
131
|
+
export function registerHooks() {
|
|
132
|
+
beforeEach(() => {
|
|
133
|
+
vi.clearAllMocks();
|
|
134
|
+
mockLoadConfig.mockReturnValue(defaultConfig());
|
|
135
|
+
mockFetchPrBatch.mockResolvedValue({ data: makeBatchData() });
|
|
136
|
+
mockGetMergeableState.mockResolvedValue({ mergeable: "MERGEABLE", mergeStateStatus: "CLEAN" });
|
|
137
|
+
mockFetchStartupFailureChecks.mockResolvedValue([]);
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
export { BASE_OPTS, autoResolveOutdated, defaultConfig, fetchPrBatch, fetchStartupFailureChecks, getCurrentPrNumber, getMergeableState, hashBody, loadSeenMap, makeBatchData, makeCheck, makeComment, makeThread, markSeen, mockAutoResolveOutdated, mockFetchPrBatch, mockFetchStartupFailureChecks, mockGetCurrentPrNumber, mockGetMergeableState, mockLoadConfig, mockLoadSeenMap, mockMarkSeen, mockTriageFailingChecks, runCheck, triageFailingChecks, };
|
|
@@ -0,0 +1,87 @@
|
|
|
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
|
+
import { runCommitSuggestion } from "./commit-suggestion.mjs";
|
|
27
|
+
import { getCurrentBranch } from "../github/client.mjs";
|
|
28
|
+
import { fetchPrBatch } from "../github/batch.mjs";
|
|
29
|
+
import { readFile } from "node:fs/promises";
|
|
30
|
+
const mockGetCurrentBranch = vi.mocked(getCurrentBranch);
|
|
31
|
+
const mockFetchBatch = vi.mocked(fetchPrBatch);
|
|
32
|
+
const mockReadFile = vi.mocked(readFile);
|
|
33
|
+
function makeThread(overrides = {}) {
|
|
34
|
+
return {
|
|
35
|
+
id: "PRRT_x",
|
|
36
|
+
isResolved: false,
|
|
37
|
+
isOutdated: false,
|
|
38
|
+
isMinimized: false,
|
|
39
|
+
path: "src/foo.ts",
|
|
40
|
+
line: 5,
|
|
41
|
+
startLine: null,
|
|
42
|
+
author: "alice",
|
|
43
|
+
authorType: "Unknown",
|
|
44
|
+
body: "Use a const here.\n\n```suggestion\nconst x = 10;\n```",
|
|
45
|
+
url: "",
|
|
46
|
+
createdAtUnix: 0,
|
|
47
|
+
...overrides,
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function makeBatch(threads) {
|
|
51
|
+
return {
|
|
52
|
+
nodeId: "PR_kgDOAAA",
|
|
53
|
+
number: 42,
|
|
54
|
+
state: "OPEN",
|
|
55
|
+
isDraft: false,
|
|
56
|
+
mergeable: "MERGEABLE",
|
|
57
|
+
mergeStateStatus: "CLEAN",
|
|
58
|
+
reviewDecision: "APPROVED",
|
|
59
|
+
headRefOid: "headsha",
|
|
60
|
+
headRefName: "feature/foo",
|
|
61
|
+
headRepoWithOwner: "owner/repo",
|
|
62
|
+
baseRefName: "main",
|
|
63
|
+
reviewRequests: [],
|
|
64
|
+
latestReviews: [],
|
|
65
|
+
reviewThreads: threads,
|
|
66
|
+
checks: [],
|
|
67
|
+
comments: [],
|
|
68
|
+
changesRequestedReviews: [],
|
|
69
|
+
reviewSummaries: [],
|
|
70
|
+
approvedReviews: [],
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
const FILE_CONTENT = "line1\n" +
|
|
74
|
+
"line2\n" +
|
|
75
|
+
"line3\n" +
|
|
76
|
+
"line4\n" +
|
|
77
|
+
"const x = 1;\n" + // line 5 — matches the suggestion anchor
|
|
78
|
+
"line6\n" +
|
|
79
|
+
"line7\n";
|
|
80
|
+
const GLOBAL_OPTS = { format: "text" };
|
|
81
|
+
function makeGitSuccess(stdout = "") {
|
|
82
|
+
return Promise.resolve({ stdout, stderr: "" });
|
|
83
|
+
}
|
|
84
|
+
// ---------------------------------------------------------------------------
|
|
85
|
+
// Output shape and instruction content
|
|
86
|
+
// ---------------------------------------------------------------------------
|
|
87
|
+
export { FILE_CONTENT, GLOBAL_OPTS, fetchPrBatch, getCurrentBranch, makeBatch, makeGitSuccess, makeThread, mockExecFile, mockFetchBatch, mockGetCurrentBranch, mockReadFile, readFile, runCommitSuggestion, };
|