pstack-claude-mirror 0.9.133 → 0.9.135
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/marketplace.json +1 -1
- package/CHANGES.md +16 -0
- package/README.md +23 -7
- package/UPSTREAM-README.md +7 -1
- package/bin/pstack-claude.mjs +278 -130
- package/docs/reference.md +4 -4
- package/package.json +27 -9
- package/plugins/pstack/.antigravity-plugin/plugin.json +1 -1
- package/plugins/pstack/.claude-plugin/plugin.json +1 -1
- package/plugins/pstack/.codex-plugin/plugin.json +1 -1
- package/plugins/pstack/.github/plugin/plugin.json +1 -1
- package/plugins/pstack/.grok-plugin/plugin.json +1 -1
- package/plugins/pstack/.muse-plugin/plugin.json +1 -1
- package/plugins/pstack/LICENSE +22 -0
- package/plugins/pstack/hooks/copilot-hooks.json +4 -2
- package/plugins/pstack/hooks/session-start.ps1 +76 -12
- package/plugins/pstack/hooks/session-start.sh +6 -0
- package/plugins/pstack/models.json +1 -1
- package/plugins/pstack/package.json +1 -1
- package/plugins/pstack/pi/slash.ts +19 -5
- package/plugins/pstack/skills/poteto-mode/SKILL.md +1 -0
- package/plugins/pstack/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/plugins/pstack/skills/poteto-mode/references/agy-tools.md +5 -4
- package/plugins/pstack/skills/poteto-mode/references/codex-tools.md +3 -2
- package/plugins/pstack/skills/poteto-mode/references/copilot-tools.md +2 -1
- package/plugins/pstack/skills/poteto-mode/references/pi-tools.md +1 -1
- package/plugins/pstack/skills/poteto-mode/scripts/.prettierignore +1 -1
- package/plugins/pstack/skills/poteto-mode/scripts/codex-session-effort.mjs +35 -7
- package/plugins/pstack/skills/poteto-mode/scripts/watch-pr/github.port.test.ts +427 -0
- package/plugins/pstack/skills/poteto-mode/scripts/watch-pr/github.test.ts +3 -399
- package/plugins/pstack/skills/poteto-mode/scripts/watch-pr/policy.port.test.ts +708 -0
- package/plugins/pstack/skills/poteto-mode/scripts/watch-pr/policy.test.ts +7 -666
- package/plugins/pstack/skills/poteto-mode/scripts/worktree-audit.mjs +42 -1
- package/plugins/pstack/skills/reflect/scripts/find-transcript.mjs +2 -2
- package/plugins/pstack/skills/setup-pstack/SKILL.md +6 -4
- package/plugins/pstack/skills/setup-pstack/references/runtimes.md +1 -1
- package/tools/forks.json +5 -5
|
@@ -3,22 +3,18 @@ import { spawnSync } from "node:child_process";
|
|
|
3
3
|
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
4
4
|
import { tmpdir } from "node:os";
|
|
5
5
|
import { join } from "node:path";
|
|
6
|
-
import { WatchDeadline } from "./deadline.ts";
|
|
7
6
|
import {
|
|
8
7
|
ChecksUnavailable,
|
|
9
8
|
WatcherQueryError,
|
|
10
|
-
discoverStack,
|
|
11
9
|
buildReviewThreads,
|
|
12
10
|
mapRollupNode,
|
|
13
11
|
orderStack,
|
|
14
12
|
parseFastCheck,
|
|
15
13
|
parsePullRequest,
|
|
16
14
|
parseReviewThreadPage,
|
|
17
|
-
PR_FACTS_QUERY,
|
|
18
15
|
resolveChecks,
|
|
19
16
|
resolveContext,
|
|
20
17
|
resolveReviewThreads,
|
|
21
|
-
runJson,
|
|
22
18
|
} from "./github.ts";
|
|
23
19
|
import {
|
|
24
20
|
fakeReader,
|
|
@@ -26,7 +22,7 @@ import {
|
|
|
26
22
|
passingCheck,
|
|
27
23
|
pendingCheck,
|
|
28
24
|
} from "./fakes.test-helper.ts";
|
|
29
|
-
import type { CheckRead,
|
|
25
|
+
import type { CheckRead, ReportedChecks } from "./types.ts";
|
|
30
26
|
import { parsePrNumber } from "./types.ts";
|
|
31
27
|
|
|
32
28
|
const context = {
|
|
@@ -41,9 +37,6 @@ function reported(read: CheckRead): ReportedChecks {
|
|
|
41
37
|
return read;
|
|
42
38
|
}
|
|
43
39
|
|
|
44
|
-
const numbers = (stack: readonly PrContext[]): number[] =>
|
|
45
|
-
stack.map((pr) => Number(pr.number));
|
|
46
|
-
|
|
47
40
|
describe("checks fallback chain", () => {
|
|
48
41
|
it("uses a non-empty fast-path result without a rollup query", async () => {
|
|
49
42
|
const reader = fakeReader({
|
|
@@ -59,11 +52,7 @@ describe("checks fallback chain", () => {
|
|
|
59
52
|
const reader = fakeReader({
|
|
60
53
|
fastPath: { kind: "unusable", exitCode: 8, stderr: "" },
|
|
61
54
|
rollupPages: [
|
|
62
|
-
{
|
|
63
|
-
kind: "contexts",
|
|
64
|
-
checks: [passingCheck("first")],
|
|
65
|
-
endCursor: "next",
|
|
66
|
-
},
|
|
55
|
+
{ kind: "contexts", checks: [passingCheck("first")], endCursor: "next" },
|
|
67
56
|
{ kind: "contexts", checks: [failedCheck("second")], endCursor: null },
|
|
68
57
|
],
|
|
69
58
|
});
|
|
@@ -80,13 +69,7 @@ describe("checks fallback chain", () => {
|
|
|
80
69
|
it("falls back when valid fast-path JSON represented an empty list", async () => {
|
|
81
70
|
const reader = fakeReader({
|
|
82
71
|
fastPath: { kind: "checks", checks: [] },
|
|
83
|
-
rollupPages: [
|
|
84
|
-
{
|
|
85
|
-
kind: "contexts",
|
|
86
|
-
checks: [pendingCheck("fallback")],
|
|
87
|
-
endCursor: null,
|
|
88
|
-
},
|
|
89
|
-
],
|
|
72
|
+
rollupPages: [{ kind: "contexts", checks: [pendingCheck("fallback")], endCursor: null }],
|
|
90
73
|
});
|
|
91
74
|
expect(reported(await resolveChecks(reader, context)).checks[0].name).toBe(
|
|
92
75
|
"fallback"
|
|
@@ -107,113 +90,6 @@ describe("checks fallback chain", () => {
|
|
|
107
90
|
);
|
|
108
91
|
expect(reader.calls).toEqual(["checksFastPath", "checkRollupPage:null"]);
|
|
109
92
|
});
|
|
110
|
-
|
|
111
|
-
it("reads no checks when gh reports none and the head commit has no rollup", async () => {
|
|
112
|
-
const reader = fakeReader({
|
|
113
|
-
fastPath: { kind: "none-reported" },
|
|
114
|
-
rollupPages: [{ kind: "no-rollup" }],
|
|
115
|
-
});
|
|
116
|
-
expect(await resolveChecks(reader, context)).toEqual({
|
|
117
|
-
kind: "no-checks",
|
|
118
|
-
checks: [],
|
|
119
|
-
});
|
|
120
|
-
expect(reader.calls).toEqual(["checksFastPath", "checkRollupPage:null"]);
|
|
121
|
-
});
|
|
122
|
-
|
|
123
|
-
it("fails closed when only one read says there are no checks", async () => {
|
|
124
|
-
const oneSided = [
|
|
125
|
-
{
|
|
126
|
-
fastPath: { kind: "unusable", exitCode: 1, stderr: "HTTP 401" },
|
|
127
|
-
rollupPages: [{ kind: "no-rollup" }],
|
|
128
|
-
},
|
|
129
|
-
{
|
|
130
|
-
fastPath: { kind: "checks", checks: [] },
|
|
131
|
-
rollupPages: [{ kind: "no-rollup" }],
|
|
132
|
-
},
|
|
133
|
-
{
|
|
134
|
-
fastPath: { kind: "none-reported" },
|
|
135
|
-
rollupPages: [{ kind: "contexts", checks: [], endCursor: null }],
|
|
136
|
-
},
|
|
137
|
-
] as const;
|
|
138
|
-
for (const options of oneSided)
|
|
139
|
-
await expect(
|
|
140
|
-
resolveChecks(fakeReader(options), context)
|
|
141
|
-
).rejects.toBeInstanceOf(ChecksUnavailable);
|
|
142
|
-
});
|
|
143
|
-
|
|
144
|
-
it("rejects a rollup cursor that does not advance instead of paging on", async () => {
|
|
145
|
-
const stuck = { kind: "contexts", checks: [], endCursor: "same" } as const;
|
|
146
|
-
const reader = fakeReader({
|
|
147
|
-
fastPath: { kind: "checks", checks: [] },
|
|
148
|
-
rollupPages: [
|
|
149
|
-
stuck,
|
|
150
|
-
stuck,
|
|
151
|
-
{ kind: "contexts", checks: [], endCursor: null },
|
|
152
|
-
],
|
|
153
|
-
});
|
|
154
|
-
await expect(resolveChecks(reader, context)).rejects.toMatchObject({
|
|
155
|
-
failure: {
|
|
156
|
-
kind: "missing-field",
|
|
157
|
-
retryable: true,
|
|
158
|
-
detail: expect.stringContaining("must advance"),
|
|
159
|
-
},
|
|
160
|
-
});
|
|
161
|
-
expect(reader.calls).toEqual([
|
|
162
|
-
"checksFastPath",
|
|
163
|
-
"checkRollupPage:null",
|
|
164
|
-
"checkRollupPage:same",
|
|
165
|
-
]);
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
it("rejects a rollup cursor that returns to an earlier page after moving on", async () => {
|
|
169
|
-
const page = (endCursor: string | null) =>
|
|
170
|
-
({ kind: "contexts", checks: [], endCursor }) as const;
|
|
171
|
-
const reader = fakeReader({
|
|
172
|
-
fastPath: { kind: "checks", checks: [] },
|
|
173
|
-
rollupPages: [page("a"), page("b"), page("a"), page(null)],
|
|
174
|
-
});
|
|
175
|
-
await expect(resolveChecks(reader, context)).rejects.toMatchObject({
|
|
176
|
-
failure: {
|
|
177
|
-
kind: "missing-field",
|
|
178
|
-
detail: expect.stringContaining("must advance"),
|
|
179
|
-
},
|
|
180
|
-
});
|
|
181
|
-
expect(reader.calls).toEqual([
|
|
182
|
-
"checksFastPath",
|
|
183
|
-
"checkRollupPage:null",
|
|
184
|
-
"checkRollupPage:a",
|
|
185
|
-
"checkRollupPage:b",
|
|
186
|
-
]);
|
|
187
|
-
});
|
|
188
|
-
|
|
189
|
-
it("propagates a failed rollup query instead of reading it as no checks", async () => {
|
|
190
|
-
const reader = fakeReader({ fastPath: { kind: "none-reported" } });
|
|
191
|
-
const failure = new WatcherQueryError({
|
|
192
|
-
kind: "command-exit",
|
|
193
|
-
retryable: true,
|
|
194
|
-
code: 1,
|
|
195
|
-
detail: "HTTP 502",
|
|
196
|
-
});
|
|
197
|
-
reader.checkRollupPage = async () => {
|
|
198
|
-
throw failure;
|
|
199
|
-
};
|
|
200
|
-
await expect(resolveChecks(reader, context)).rejects.toBe(failure);
|
|
201
|
-
});
|
|
202
|
-
});
|
|
203
|
-
|
|
204
|
-
it("reports a binary that cannot be spawned as a query failure that does not retry", async () => {
|
|
205
|
-
await expect(
|
|
206
|
-
runJson(
|
|
207
|
-
["/nonexistent/watch-pr-missing-gh", "pr", "view"],
|
|
208
|
-
new WatchDeadline(0, () => 0)
|
|
209
|
-
)
|
|
210
|
-
).rejects.toMatchObject({
|
|
211
|
-
failure: {
|
|
212
|
-
kind: "spawn-failed",
|
|
213
|
-
retryable: false,
|
|
214
|
-
detail: expect.stringContaining("ENOENT"),
|
|
215
|
-
},
|
|
216
|
-
});
|
|
217
93
|
});
|
|
218
94
|
|
|
219
95
|
describe("rollup node mapping", () => {
|
|
@@ -307,33 +183,6 @@ describe("rollup node mapping", () => {
|
|
|
307
183
|
});
|
|
308
184
|
});
|
|
309
185
|
|
|
310
|
-
describe("fast-path check mapping", () => {
|
|
311
|
-
// gh's aggregate.go files every state it does not name into the pending
|
|
312
|
-
// bucket, including completed conclusions.
|
|
313
|
-
it("fails completed conclusions that gh buckets as pending", () => {
|
|
314
|
-
for (const state of ["STARTUP_FAILURE", "STALE"]) {
|
|
315
|
-
expect(
|
|
316
|
-
parseFastCheck({ name: "ci", state, bucket: "pending" })
|
|
317
|
-
).toMatchObject({ kind: "failed", reportedState: state });
|
|
318
|
-
}
|
|
319
|
-
});
|
|
320
|
-
|
|
321
|
-
it("keeps in-flight states pending", () => {
|
|
322
|
-
for (const state of [
|
|
323
|
-
"EXPECTED",
|
|
324
|
-
"REQUESTED",
|
|
325
|
-
"WAITING",
|
|
326
|
-
"QUEUED",
|
|
327
|
-
"PENDING",
|
|
328
|
-
"IN_PROGRESS",
|
|
329
|
-
]) {
|
|
330
|
-
expect(
|
|
331
|
-
parseFastCheck({ name: "ci", state, bucket: "pending" })
|
|
332
|
-
).toMatchObject({ kind: "pending", reportedState: state });
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
});
|
|
336
|
-
|
|
337
186
|
describe("closed enum parsing", () => {
|
|
338
187
|
const rawPullRequest = {
|
|
339
188
|
mergeable: "MERGEABLE",
|
|
@@ -369,32 +218,6 @@ describe("closed enum parsing", () => {
|
|
|
369
218
|
).toThrow(WatcherQueryError);
|
|
370
219
|
});
|
|
371
220
|
|
|
372
|
-
it("uses the current base target when the scalar base OID is stale", () => {
|
|
373
|
-
expect(
|
|
374
|
-
parsePullRequest(
|
|
375
|
-
{
|
|
376
|
-
...rawPullRequest,
|
|
377
|
-
baseRefOid: "stale-base",
|
|
378
|
-
baseRef: { target: { oid: "current-base" } },
|
|
379
|
-
},
|
|
380
|
-
context
|
|
381
|
-
).baseRefOid
|
|
382
|
-
).toBe("current-base");
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
it("queries the current base target with the other PR facts", () => {
|
|
386
|
-
expect(PR_FACTS_QUERY).toContain("baseRef { target { oid } }");
|
|
387
|
-
expect(PR_FACTS_QUERY).not.toContain("baseRefOid");
|
|
388
|
-
});
|
|
389
|
-
|
|
390
|
-
it("fails closed when the current base ref is null or missing", () => {
|
|
391
|
-
const missingBaseRef = Object.fromEntries(
|
|
392
|
-
Object.entries(rawPullRequest).filter(([key]) => key !== "baseRef")
|
|
393
|
-
);
|
|
394
|
-
for (const value of [{ ...rawPullRequest, baseRef: null }, missingBaseRef])
|
|
395
|
-
expect(() => parsePullRequest(value, context)).toThrow(WatcherQueryError);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
221
|
it("rejects unknown enum values as retryable errors carrying the raw value", () => {
|
|
399
222
|
try {
|
|
400
223
|
parsePullRequest(
|
|
@@ -572,44 +395,6 @@ describe("review thread pagination", () => {
|
|
|
572
395
|
};
|
|
573
396
|
expect(() => parseReviewThreadPage(response)).toThrow(WatcherQueryError);
|
|
574
397
|
});
|
|
575
|
-
|
|
576
|
-
it("rejects a page that promises more pages without a cursor", () => {
|
|
577
|
-
for (const endCursor of [null, ""]) {
|
|
578
|
-
const response = {
|
|
579
|
-
data: {
|
|
580
|
-
repository: {
|
|
581
|
-
pullRequest: {
|
|
582
|
-
reviewThreads: {
|
|
583
|
-
pageInfo: { hasNextPage: true, endCursor },
|
|
584
|
-
nodes: [],
|
|
585
|
-
},
|
|
586
|
-
},
|
|
587
|
-
},
|
|
588
|
-
},
|
|
589
|
-
};
|
|
590
|
-
expect(() => parseReviewThreadPage(response)).toThrow(WatcherQueryError);
|
|
591
|
-
}
|
|
592
|
-
});
|
|
593
|
-
|
|
594
|
-
it("rejects a thread cursor that does not advance instead of paging on", async () => {
|
|
595
|
-
const stuck = { threads: [], endCursor: "same" };
|
|
596
|
-
const reader = fakeReader({
|
|
597
|
-
threadPages: [stuck, stuck, { threads: [], endCursor: null }],
|
|
598
|
-
});
|
|
599
|
-
await expect(resolveReviewThreads(reader, context)).rejects.toMatchObject({
|
|
600
|
-
failure: {
|
|
601
|
-
kind: "missing-field",
|
|
602
|
-
retryable: true,
|
|
603
|
-
detail: expect.stringContaining(
|
|
604
|
-
"reviewThreads.pageInfo.endCursor must advance"
|
|
605
|
-
),
|
|
606
|
-
},
|
|
607
|
-
});
|
|
608
|
-
expect(reader.calls).toEqual([
|
|
609
|
-
"reviewThreadPage:null",
|
|
610
|
-
"reviewThreadPage:same",
|
|
611
|
-
]);
|
|
612
|
-
});
|
|
613
398
|
});
|
|
614
399
|
|
|
615
400
|
describe("context and stack discovery", () => {
|
|
@@ -639,31 +424,6 @@ describe("context and stack discovery", () => {
|
|
|
639
424
|
expect(reader.calls).toEqual(["originRepo"]);
|
|
640
425
|
});
|
|
641
426
|
|
|
642
|
-
it("refuses to pair the checkout's PR number with a different explicit repository", async () => {
|
|
643
|
-
const reader = fakeReader({
|
|
644
|
-
current: { owner: "acme", repo: "web", number: parsePrNumber(57) },
|
|
645
|
-
});
|
|
646
|
-
const resolved = resolveContext({
|
|
647
|
-
reader,
|
|
648
|
-
owner: "acme",
|
|
649
|
-
repo: "api",
|
|
650
|
-
pr: null,
|
|
651
|
-
});
|
|
652
|
-
await expect(resolved).rejects.toBeInstanceOf(WatcherQueryError);
|
|
653
|
-
await expect(resolved).rejects.toMatchObject({
|
|
654
|
-
failure: { retryable: false },
|
|
655
|
-
});
|
|
656
|
-
});
|
|
657
|
-
|
|
658
|
-
it("accepts an explicit repository that matches the checkout's PR", async () => {
|
|
659
|
-
const reader = fakeReader({
|
|
660
|
-
current: { owner: "acme", repo: "web", number: parsePrNumber(57) },
|
|
661
|
-
});
|
|
662
|
-
expect(
|
|
663
|
-
await resolveContext({ reader, owner: "ACME", repo: "Web", pr: null })
|
|
664
|
-
).toEqual({ owner: "ACME", repo: "Web", number: parsePrNumber(57) });
|
|
665
|
-
});
|
|
666
|
-
|
|
667
427
|
it("orders the connected stack bottom-to-top", () => {
|
|
668
428
|
const ordered = orderStack(context, "main", [
|
|
669
429
|
{
|
|
@@ -687,162 +447,6 @@ describe("context and stack discovery", () => {
|
|
|
687
447
|
]);
|
|
688
448
|
expect(ordered.map((item) => Number(item.number))).toEqual([41, 42, 43]);
|
|
689
449
|
});
|
|
690
|
-
|
|
691
|
-
it("keeps a PR whose head is the default branch out of every stack", () => {
|
|
692
|
-
const repo = { owner: "owner", repo: "repo" };
|
|
693
|
-
const backport = parsePrNumber(2);
|
|
694
|
-
const open = [
|
|
695
|
-
{
|
|
696
|
-
number: backport,
|
|
697
|
-
headRepository: repo,
|
|
698
|
-
headRefName: "main",
|
|
699
|
-
baseRefName: "release",
|
|
700
|
-
},
|
|
701
|
-
{
|
|
702
|
-
number: context.number,
|
|
703
|
-
headRepository: repo,
|
|
704
|
-
headRefName: "feature",
|
|
705
|
-
baseRefName: "main",
|
|
706
|
-
},
|
|
707
|
-
{
|
|
708
|
-
number: parsePrNumber(43),
|
|
709
|
-
headRepository: repo,
|
|
710
|
-
headRefName: "upstack",
|
|
711
|
-
baseRefName: "feature",
|
|
712
|
-
},
|
|
713
|
-
];
|
|
714
|
-
expect(numbers(orderStack(context, "main", open))).toEqual([42, 43]);
|
|
715
|
-
expect(
|
|
716
|
-
numbers(orderStack({ ...context, number: backport }, "main", open))
|
|
717
|
-
).toEqual([2]);
|
|
718
|
-
});
|
|
719
|
-
|
|
720
|
-
it("keeps a PR that brings the default branch into a feature branch out of that feature's stack", () => {
|
|
721
|
-
const repo = { owner: "owner", repo: "repo" };
|
|
722
|
-
const sync = parsePrNumber(7);
|
|
723
|
-
const open = [
|
|
724
|
-
{
|
|
725
|
-
number: context.number,
|
|
726
|
-
headRepository: repo,
|
|
727
|
-
headRefName: "feature",
|
|
728
|
-
baseRefName: "main",
|
|
729
|
-
},
|
|
730
|
-
{
|
|
731
|
-
number: sync,
|
|
732
|
-
headRepository: repo,
|
|
733
|
-
headRefName: "main",
|
|
734
|
-
baseRefName: "feature",
|
|
735
|
-
},
|
|
736
|
-
];
|
|
737
|
-
expect(numbers(orderStack(context, "main", open))).toEqual([42]);
|
|
738
|
-
expect(
|
|
739
|
-
numbers(orderStack({ ...context, number: sync }, "main", open))
|
|
740
|
-
).toEqual([7]);
|
|
741
|
-
});
|
|
742
|
-
|
|
743
|
-
it("still stacks a fork PR whose head branch has the default branch's name", () => {
|
|
744
|
-
const fork = parsePrNumber(50);
|
|
745
|
-
const open = [
|
|
746
|
-
{
|
|
747
|
-
number: context.number,
|
|
748
|
-
headRepository: { owner: "owner", repo: "repo" },
|
|
749
|
-
headRefName: "feature",
|
|
750
|
-
baseRefName: "main",
|
|
751
|
-
},
|
|
752
|
-
{
|
|
753
|
-
number: fork,
|
|
754
|
-
headRepository: { owner: "contributor", repo: "repo" },
|
|
755
|
-
headRefName: "main",
|
|
756
|
-
baseRefName: "feature",
|
|
757
|
-
},
|
|
758
|
-
];
|
|
759
|
-
expect(numbers(orderStack(context, "main", open))).toEqual([42, 50]);
|
|
760
|
-
expect(
|
|
761
|
-
numbers(orderStack({ ...context, number: fork }, "main", open))
|
|
762
|
-
).toEqual([42, 50]);
|
|
763
|
-
});
|
|
764
|
-
|
|
765
|
-
it("stops at the default branch when it is an integration branch with a release PR", () => {
|
|
766
|
-
const repo = { owner: "owner", repo: "repo" };
|
|
767
|
-
const open = [
|
|
768
|
-
{
|
|
769
|
-
number: parsePrNumber(500),
|
|
770
|
-
headRepository: repo,
|
|
771
|
-
headRefName: "develop",
|
|
772
|
-
baseRefName: "main",
|
|
773
|
-
},
|
|
774
|
-
{
|
|
775
|
-
number: parsePrNumber(612),
|
|
776
|
-
headRepository: repo,
|
|
777
|
-
headRefName: "feature/login",
|
|
778
|
-
baseRefName: "develop",
|
|
779
|
-
},
|
|
780
|
-
{
|
|
781
|
-
number: parsePrNumber(613),
|
|
782
|
-
headRepository: repo,
|
|
783
|
-
headRefName: "fix/typo",
|
|
784
|
-
baseRefName: "develop",
|
|
785
|
-
},
|
|
786
|
-
];
|
|
787
|
-
expect(
|
|
788
|
-
numbers(
|
|
789
|
-
orderStack({ ...repo, number: parsePrNumber(612) }, "develop", open)
|
|
790
|
-
)
|
|
791
|
-
).toEqual([612]);
|
|
792
|
-
expect(
|
|
793
|
-
numbers(
|
|
794
|
-
orderStack({ ...repo, number: parsePrNumber(500) }, "develop", open)
|
|
795
|
-
)
|
|
796
|
-
).toEqual([500]);
|
|
797
|
-
});
|
|
798
|
-
|
|
799
|
-
it("reads the default branch once while discovering a stack", async () => {
|
|
800
|
-
const reader = fakeReader({ defaultBranch: "trunk" });
|
|
801
|
-
expect(await discoverStack(reader, context)).toEqual([context]);
|
|
802
|
-
expect(reader.calls).toEqual(["openPullRequests", "defaultBranch"]);
|
|
803
|
-
});
|
|
804
|
-
|
|
805
|
-
it("orders a discovered stack around the default branch the repository reports", async () => {
|
|
806
|
-
const repo = { owner: "owner", repo: "repo" };
|
|
807
|
-
const reader = fakeReader({
|
|
808
|
-
defaultBranch: "develop",
|
|
809
|
-
openPullRequests: [
|
|
810
|
-
{
|
|
811
|
-
number: parsePrNumber(500),
|
|
812
|
-
headRepository: repo,
|
|
813
|
-
headRefName: "develop",
|
|
814
|
-
baseRefName: "main",
|
|
815
|
-
},
|
|
816
|
-
{
|
|
817
|
-
number: context.number,
|
|
818
|
-
headRepository: repo,
|
|
819
|
-
headRefName: "feature",
|
|
820
|
-
baseRefName: "develop",
|
|
821
|
-
},
|
|
822
|
-
],
|
|
823
|
-
});
|
|
824
|
-
expect(numbers(await discoverStack(reader, context))).toEqual([42]);
|
|
825
|
-
});
|
|
826
|
-
|
|
827
|
-
it("refuses a full open-PR page, which may have cut the stack", async () => {
|
|
828
|
-
const openPrs = (count: number) =>
|
|
829
|
-
Array.from({ length: count }, (_, index) => ({
|
|
830
|
-
number: parsePrNumber(index + 1),
|
|
831
|
-
headRepository: { owner: "owner", repo: "repo" },
|
|
832
|
-
headRefName: `branch-${index + 1}`,
|
|
833
|
-
baseRefName: "main",
|
|
834
|
-
}));
|
|
835
|
-
await expect(
|
|
836
|
-
discoverStack(fakeReader({ openPullRequests: openPrs(300) }), context)
|
|
837
|
-
).rejects.toMatchObject({ failure: { kind: "invalid-stack" } });
|
|
838
|
-
expect(
|
|
839
|
-
await discoverStack(
|
|
840
|
-
fakeReader({ openPullRequests: openPrs(299) }),
|
|
841
|
-
context
|
|
842
|
-
)
|
|
843
|
-
).toEqual([context]);
|
|
844
|
-
});
|
|
845
|
-
|
|
846
450
|
it("does not use a fork's same-named head as a stack parent", () => {
|
|
847
451
|
const ordered = orderStack(context, "main", [
|
|
848
452
|
{ number: parsePrNumber(41), headRefName: "base-feature", baseRefName: "main", headRepository: { owner: "contributor", repo: "repo" } },
|