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.
Files changed (31) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/bin/checks/triage.test-support.mjs +61 -0
  3. package/bin/cli/iterate-lean.test-support.mjs +5 -0
  4. package/bin/cli-parser.commit-suggestion.test-support.mjs +66 -0
  5. package/bin/cli-parser.iterate-fix.test-support.mjs +48 -0
  6. package/bin/cli-parser.iterate.test-support.mjs +49 -0
  7. package/bin/cli-parser.test-support.mjs +43 -0
  8. package/bin/commands/check.mjs +14 -17
  9. package/bin/commands/check.test-support.mjs +140 -0
  10. package/bin/commands/commit-suggestion.apply.test-support.mjs +87 -0
  11. package/bin/commands/commit-suggestion.test-support.mjs +112 -0
  12. package/bin/commands/iterate/index.mjs +20 -16
  13. package/bin/commands/iterate-stall.test-support.mjs +25 -0
  14. package/bin/commands/iterate-test-support.mjs +149 -0
  15. package/bin/commands/iterate.fix-code-in-progress.test-support.mjs +118 -0
  16. package/bin/commands/ready-mergeability.mjs +30 -0
  17. package/bin/commands/resolve.test-support.mjs +114 -0
  18. package/bin/commands/shepherd-journal.test-support.mjs +9 -0
  19. package/bin/comments/resolve.test-support.mjs +40 -0
  20. package/bin/github/batch-parsers.test-support.mjs +67 -0
  21. package/bin/github/batch.test-support.mjs +67 -0
  22. package/bin/github/graphql-http.mjs +73 -0
  23. package/bin/github/http-auth.mjs +48 -0
  24. package/bin/github/http-request.mjs +15 -0
  25. package/bin/github/http-utils.mjs +34 -0
  26. package/bin/github/http.mjs +4 -319
  27. package/bin/github/http.test-support.mjs +52 -0
  28. package/bin/github/rest-http.mjs +131 -0
  29. package/bin/suggestions/patch.test-support.mjs +4 -0
  30. package/package.json +2 -2
  31. 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 isReady = report.status === "READY";
54
- const readyState = await updateReadyDelay(report.pr, isReady, readyDelaySeconds, repoOwner, repoName);
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, };