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,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, };
|
|
@@ -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, };
|
|
@@ -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, };
|
|
@@ -51,8 +51,25 @@ export async function runIterate(opts) {
|
|
|
51
51
|
log: `CANCEL: PR #${report.pr} is ${state} — stopping`,
|
|
52
52
|
};
|
|
53
53
|
}
|
|
54
|
-
const
|
|
55
|
-
|
|
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);
|
|
56
73
|
const base = {
|
|
57
74
|
pr: report.pr,
|
|
58
75
|
repo: report.repo,
|
|
@@ -88,20 +105,6 @@ export async function runIterate(opts) {
|
|
|
88
105
|
};
|
|
89
106
|
}
|
|
90
107
|
const headSha = (await getCurrentHeadSha()) ?? "unknown";
|
|
91
|
-
const { minimizeIds: reviewSummaryIds, firstLookSummaries, editedSummaries, surfacedApprovals, } = classifyReviewSummaries({
|
|
92
|
-
firstLook: report.firstLookSummaries,
|
|
93
|
-
seen: report.reviewSummaries,
|
|
94
|
-
edited: report.editedSummaries,
|
|
95
|
-
}, report.approvedReviews, config.iterate.minimizeApprovals, config.iterate.minimizeComments);
|
|
96
|
-
const hasActionableWork = report.threads.actionable.length > 0 ||
|
|
97
|
-
report.threads.resolutionOnly.length > 0 ||
|
|
98
|
-
report.comments.actionable.length > 0 ||
|
|
99
|
-
report.changesRequestedReviews.length > 0 ||
|
|
100
|
-
report.checks.failing.length > 0 ||
|
|
101
|
-
report.mergeStatus.status === "CONFLICTS" ||
|
|
102
|
-
reviewSummaryIds.length > 0 ||
|
|
103
|
-
firstLookSummaries.length > 0 ||
|
|
104
|
-
editedSummaries.length > 0;
|
|
105
108
|
if (hasActionableWork) {
|
|
106
109
|
return handleFixCode({
|
|
107
110
|
base,
|