@zenspc/pi-pstack 0.1.0
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/LICENSE +21 -0
- package/README.md +49 -0
- package/agents/comment-sicko.md +34 -0
- package/agents/poteto-agent.md +13 -0
- package/extensions/pstack/config.ts +228 -0
- package/extensions/pstack/index.ts +211 -0
- package/package.json +57 -0
- package/skills/architect/SKILL.md +82 -0
- package/skills/architect/references/design-red-flags.md +33 -0
- package/skills/architect/references/rationale-template.md +35 -0
- package/skills/architect/references/runner-prompt.md +20 -0
- package/skills/arena/SKILL.md +70 -0
- package/skills/automate-me/SKILL.md +114 -0
- package/skills/blast-radius/SKILL.md +49 -0
- package/skills/bro/SKILL.md +6 -0
- package/skills/create-verification-skill/SKILL.md +43 -0
- package/skills/create-verification-skill/references/feature-map-example/README.md +47 -0
- package/skills/create-verification-skill/references/feature-map-example/create-note.md +39 -0
- package/skills/create-verification-skill/references/feature-map-example/search.md +45 -0
- package/skills/figure-it-out/SKILL.md +54 -0
- package/skills/how/SKILL.md +134 -0
- package/skills/how/references/critic-prompt.md +59 -0
- package/skills/how/references/critique-rubric.md +58 -0
- package/skills/how/references/explainer-prompt.md +55 -0
- package/skills/how/references/explorer-prompt.md +52 -0
- package/skills/interrogate/SKILL.md +109 -0
- package/skills/interrogate/references/code-quality-review.md +47 -0
- package/skills/interrogate/references/lead-judgment.md +58 -0
- package/skills/interrogate/references/reviewer-prompt.md +72 -0
- package/skills/interrogate/references/rubric.md +77 -0
- package/skills/maintain-verification-skill/SKILL.md +38 -0
- package/skills/no-comments/SKILL.md +23 -0
- package/skills/poteto-mode/SKILL.md +140 -0
- package/skills/poteto-mode/playbooks/authoring-a-skill.md +12 -0
- package/skills/poteto-mode/playbooks/autonomous-run.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-full.md +13 -0
- package/skills/poteto-mode/playbooks/autopilot-stack.md +31 -0
- package/skills/poteto-mode/playbooks/babysit.md +27 -0
- package/skills/poteto-mode/playbooks/bug-fix.md +17 -0
- package/skills/poteto-mode/playbooks/eval.md +27 -0
- package/skills/poteto-mode/playbooks/feature.md +21 -0
- package/skills/poteto-mode/playbooks/hillclimb.md +21 -0
- package/skills/poteto-mode/playbooks/investigation.md +14 -0
- package/skills/poteto-mode/playbooks/multi-phase-plan.md +3 -0
- package/skills/poteto-mode/playbooks/opening-a-pr.md +35 -0
- package/skills/poteto-mode/playbooks/orchestrate.md +113 -0
- package/skills/poteto-mode/playbooks/pause-safely.md +10 -0
- package/skills/poteto-mode/playbooks/perf-issue.md +24 -0
- package/skills/poteto-mode/playbooks/prototype.md +14 -0
- package/skills/poteto-mode/playbooks/refactoring.md +16 -0
- package/skills/poteto-mode/playbooks/runtime-forensics.md +11 -0
- package/skills/poteto-mode/playbooks/session-pickup.md +13 -0
- package/skills/poteto-mode/playbooks/shipping.md +40 -0
- package/skills/poteto-mode/playbooks/trace-forensics.md +14 -0
- package/skills/poteto-mode/playbooks/visual-parity.md +11 -0
- package/skills/poteto-mode/playbooks/worktree-cleanup.md +14 -0
- package/skills/poteto-mode/references/bugbot-triage.md +142 -0
- package/skills/poteto-mode/references/plan.md +105 -0
- package/skills/poteto-mode/scripts/bootstrap.ts +62 -0
- package/skills/poteto-mode/scripts/bun.lock +67 -0
- package/skills/poteto-mode/scripts/orch/orch.test.ts +634 -0
- package/skills/poteto-mode/scripts/orch/orch.ts +578 -0
- package/skills/poteto-mode/scripts/orch/store.ts +1607 -0
- package/skills/poteto-mode/scripts/package.json +16 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.test.ts +224 -0
- package/skills/poteto-mode/scripts/watch-pr/cli.ts +223 -0
- package/skills/poteto-mode/scripts/watch-pr/fakes.test-helper.ts +118 -0
- package/skills/poteto-mode/scripts/watch-pr/github.test.ts +306 -0
- package/skills/poteto-mode/scripts/watch-pr/github.ts +699 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.test.ts +420 -0
- package/skills/poteto-mode/scripts/watch-pr/policy.ts +832 -0
- package/skills/poteto-mode/scripts/watch-pr/render.ts +169 -0
- package/skills/poteto-mode/scripts/watch-pr/tsconfig.json +13 -0
- package/skills/poteto-mode/scripts/watch-pr/types.compile.ts +93 -0
- package/skills/poteto-mode/scripts/watch-pr/types.ts +401 -0
- package/skills/poteto-mode/scripts/watch-pr/watch-pr +6 -0
- package/skills/poteto-mode/scripts/worktree-audit.sh +85 -0
- package/skills/principle-boundary-discipline/SKILL.md +33 -0
- package/skills/principle-build-the-lever/SKILL.md +22 -0
- package/skills/principle-encode-lessons-in-structure/SKILL.md +30 -0
- package/skills/principle-exhaust-the-design-space/SKILL.md +20 -0
- package/skills/principle-experience-first/SKILL.md +18 -0
- package/skills/principle-fix-root-causes/SKILL.md +22 -0
- package/skills/principle-foundational-thinking/SKILL.md +20 -0
- package/skills/principle-guard-the-context-window/SKILL.md +16 -0
- package/skills/principle-laziness-protocol/SKILL.md +17 -0
- package/skills/principle-make-operations-idempotent/SKILL.md +23 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis/SKILL.md +21 -0
- package/skills/principle-minimize-reader-load/SKILL.md +22 -0
- package/skills/principle-model-the-domain/SKILL.md +25 -0
- package/skills/principle-never-block-on-the-human/SKILL.md +22 -0
- package/skills/principle-outcome-oriented-execution/SKILL.md +21 -0
- package/skills/principle-prove-it-works/SKILL.md +32 -0
- package/skills/principle-redesign-from-first-principles/SKILL.md +15 -0
- package/skills/principle-separate-before-serializing-shared-state/SKILL.md +15 -0
- package/skills/principle-sequence-verifiable-units/SKILL.md +21 -0
- package/skills/principle-subtract-before-you-add/SKILL.md +21 -0
- package/skills/principle-type-system-discipline/SKILL.md +30 -0
- package/skills/recall/SKILL.md +40 -0
- package/skills/reflect/SKILL.md +77 -0
- package/skills/reflect/references/divergent-reviewer.md +43 -0
- package/skills/reflect/references/judgment-reviewer.md +42 -0
- package/skills/reflect/references/synthesizer.md +56 -0
- package/skills/reflect/references/tooling-reviewer.md +57 -0
- package/skills/setup-pstack/SKILL.md +28 -0
- package/skills/show-me-your-work/SKILL.md +89 -0
- package/skills/show-me-your-work/references/decision-log-template.tsv +1 -0
- package/skills/show-me-your-work/scripts/log.sh +40 -0
- package/skills/swarm/SKILL.md +45 -0
- package/skills/tdd/SKILL.md +43 -0
- package/skills/teach/SKILL.md +20 -0
- package/skills/technical-writing/SKILL.md +129 -0
- package/skills/typescript-best-practices/SKILL.md +28 -0
- package/skills/typescript-best-practices/references/patterns.md +292 -0
- package/skills/unslop/SKILL.md +80 -0
- package/skills/why/SKILL.md +229 -0
- package/skills/why/references/epistemics.md +144 -0
- package/skills/why/references/investigator-prompt.md +103 -0
- package/skills/why/references/source-playbook.md +17 -0
- package/skills/why/references/sources/code-archaeology.md +88 -0
- package/skills/why/references/sources/databricks.md +70 -0
- package/skills/why/references/sources/datadog.md +99 -0
- package/skills/why/references/sources/incident-postmortem.md +15 -0
- package/skills/why/references/sources/linear.md +48 -0
- package/skills/why/references/sources/notion.md +55 -0
- package/skills/why/references/sources/sentry.md +100 -0
- package/skills/why/references/sources/slack.md +54 -0
- package/skills/why/references/synthesizer-prompt.md +135 -0
|
@@ -0,0 +1,420 @@
|
|
|
1
|
+
import { describe, expect, it } from "bun:test";
|
|
2
|
+
import { WatcherQueryError } from "./github.ts";
|
|
3
|
+
import {
|
|
4
|
+
applyQueueSnapshot,
|
|
5
|
+
assessGitHubMerge,
|
|
6
|
+
classifyPr,
|
|
7
|
+
createQueueState,
|
|
8
|
+
evaluateQueue,
|
|
9
|
+
planQueue,
|
|
10
|
+
queryBackoffSeconds,
|
|
11
|
+
readSnapshot,
|
|
12
|
+
runQueued,
|
|
13
|
+
selectTierMajorStackDecision,
|
|
14
|
+
} from "./policy.ts";
|
|
15
|
+
import {
|
|
16
|
+
fakeReader,
|
|
17
|
+
failedCheck,
|
|
18
|
+
passingCheck,
|
|
19
|
+
pendingCheck,
|
|
20
|
+
} from "./fakes.test-helper.ts";
|
|
21
|
+
import type {
|
|
22
|
+
GitHubReader,
|
|
23
|
+
NonEmpty,
|
|
24
|
+
PollingOptions,
|
|
25
|
+
PrContext,
|
|
26
|
+
ProgressVerdict,
|
|
27
|
+
PullRequestFacts,
|
|
28
|
+
RollupState,
|
|
29
|
+
} from "./types.ts";
|
|
30
|
+
import { parsePrNumber } from "./types.ts";
|
|
31
|
+
|
|
32
|
+
const context = (number: number): PrContext => ({
|
|
33
|
+
owner: "owner",
|
|
34
|
+
repo: "repo",
|
|
35
|
+
number: parsePrNumber(number),
|
|
36
|
+
});
|
|
37
|
+
const options = {
|
|
38
|
+
interval: 10,
|
|
39
|
+
sweepInterval: 300,
|
|
40
|
+
timeout: 0,
|
|
41
|
+
maxQueryErrors: 5,
|
|
42
|
+
allowDraft: false,
|
|
43
|
+
} satisfies PollingOptions;
|
|
44
|
+
|
|
45
|
+
describe("readiness truth table", () => {
|
|
46
|
+
it("covers every specified row and every UNKNOWN rollup value", () => {
|
|
47
|
+
const cases: readonly [
|
|
48
|
+
PullRequestFacts["mergeStateStatus"],
|
|
49
|
+
RollupState,
|
|
50
|
+
"allowed" | "refused",
|
|
51
|
+
][] = [
|
|
52
|
+
["BLOCKED", "FAILURE", "refused"],
|
|
53
|
+
["BLOCKED", "ERROR", "refused"],
|
|
54
|
+
["BLOCKED", "PENDING", "allowed"],
|
|
55
|
+
["UNSTABLE", "FAILURE", "allowed"],
|
|
56
|
+
["UNKNOWN", "ERROR", "allowed"],
|
|
57
|
+
["UNKNOWN", "EXPECTED", "allowed"],
|
|
58
|
+
["UNKNOWN", "FAILURE", "allowed"],
|
|
59
|
+
["UNKNOWN", "PENDING", "allowed"],
|
|
60
|
+
["UNKNOWN", "SUCCESS", "allowed"],
|
|
61
|
+
["UNKNOWN", null, "allowed"],
|
|
62
|
+
["CLEAN", "SUCCESS", "allowed"],
|
|
63
|
+
];
|
|
64
|
+
for (const [mergeStateStatus, headRollupState, expected] of cases) {
|
|
65
|
+
expect(
|
|
66
|
+
assessGitHubMerge({ mergeStateStatus, headRollupState }).kind
|
|
67
|
+
).toBe(expected);
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("turns a clean visible list plus GitHub refusal into an explicit CI blocker", async () => {
|
|
72
|
+
const reader = fakeReader({
|
|
73
|
+
facts: { mergeStateStatus: "BLOCKED" },
|
|
74
|
+
fastPath: { kind: "checks", checks: [passingCheck()] },
|
|
75
|
+
commitRollups: [{ oid: "head", state: "FAILURE" }],
|
|
76
|
+
});
|
|
77
|
+
const snapshot = await readSnapshot({
|
|
78
|
+
reader,
|
|
79
|
+
context: context(1),
|
|
80
|
+
pendingHistory: "include",
|
|
81
|
+
allowDraft: false,
|
|
82
|
+
});
|
|
83
|
+
expect(snapshot.kind).toBe("open");
|
|
84
|
+
if (snapshot.kind !== "open") throw new Error("expected open snapshot");
|
|
85
|
+
expect(snapshot.ci.kind).toBe("ci-github-rejected");
|
|
86
|
+
expect(classifyPr(snapshot)).toMatchObject({
|
|
87
|
+
kind: "blocker",
|
|
88
|
+
blocker: { kind: "failing-checks" },
|
|
89
|
+
});
|
|
90
|
+
});
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
describe("snapshot query planning", () => {
|
|
94
|
+
it("does not query commit rollups while queued checks are pending", async () => {
|
|
95
|
+
const reader = fakeReader({
|
|
96
|
+
fastPath: { kind: "checks", checks: [pendingCheck()] },
|
|
97
|
+
});
|
|
98
|
+
const snapshot = await readSnapshot({
|
|
99
|
+
reader,
|
|
100
|
+
context: context(2),
|
|
101
|
+
pendingHistory: "omit",
|
|
102
|
+
allowDraft: false,
|
|
103
|
+
});
|
|
104
|
+
expect(snapshot.kind).toBe("open");
|
|
105
|
+
if (snapshot.kind !== "open") throw new Error("expected open snapshot");
|
|
106
|
+
expect(snapshot.ci.kind).toBe("ci-pending");
|
|
107
|
+
expect(reader.calls).toEqual([
|
|
108
|
+
"pullRequest",
|
|
109
|
+
"reviewThreads",
|
|
110
|
+
"checksFastPath",
|
|
111
|
+
]);
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
it("queries rollups for settled and failed lists", async () => {
|
|
115
|
+
const settled = fakeReader();
|
|
116
|
+
await readSnapshot({
|
|
117
|
+
reader: settled,
|
|
118
|
+
context: context(3),
|
|
119
|
+
pendingHistory: "omit",
|
|
120
|
+
allowDraft: false,
|
|
121
|
+
});
|
|
122
|
+
expect(settled.calls).toContain("commitRollups");
|
|
123
|
+
|
|
124
|
+
const failed = fakeReader({
|
|
125
|
+
fastPath: { kind: "checks", checks: [failedCheck()] },
|
|
126
|
+
});
|
|
127
|
+
await readSnapshot({
|
|
128
|
+
reader: failed,
|
|
129
|
+
context: context(4),
|
|
130
|
+
pendingHistory: "omit",
|
|
131
|
+
allowDraft: false,
|
|
132
|
+
});
|
|
133
|
+
expect(failed.calls).toContain("commitRollups");
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
it("short-circuits merged rows before threads and checks", async () => {
|
|
137
|
+
const reader = fakeReader({
|
|
138
|
+
facts: { state: "MERGED", mergedAt: "2026-07-26T00:00:00Z" },
|
|
139
|
+
});
|
|
140
|
+
expect(
|
|
141
|
+
(
|
|
142
|
+
await readSnapshot({
|
|
143
|
+
reader,
|
|
144
|
+
context: context(5),
|
|
145
|
+
pendingHistory: "include",
|
|
146
|
+
allowDraft: false,
|
|
147
|
+
})
|
|
148
|
+
).kind
|
|
149
|
+
).toBe("merged");
|
|
150
|
+
expect(reader.calls).toEqual(["pullRequest"]);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("scans stacks tier-major so an upstack conflict outranks frontier CI", async () => {
|
|
155
|
+
const frontier = await readSnapshot({
|
|
156
|
+
reader: fakeReader({
|
|
157
|
+
fastPath: { kind: "checks", checks: [failedCheck()] },
|
|
158
|
+
commitRollups: [{ oid: "head", state: "FAILURE" }],
|
|
159
|
+
}),
|
|
160
|
+
context: context(10),
|
|
161
|
+
pendingHistory: "omit",
|
|
162
|
+
allowDraft: false,
|
|
163
|
+
});
|
|
164
|
+
const upstack = await readSnapshot({
|
|
165
|
+
reader: fakeReader({ facts: { mergeable: "CONFLICTING" } }),
|
|
166
|
+
context: context(11),
|
|
167
|
+
pendingHistory: "omit",
|
|
168
|
+
allowDraft: false,
|
|
169
|
+
});
|
|
170
|
+
const decision = selectTierMajorStackDecision([frontier, upstack]);
|
|
171
|
+
expect(decision).toMatchObject({
|
|
172
|
+
kind: "blocker",
|
|
173
|
+
blocker: { kind: "merge-conflicts", pr: { number: 11 } },
|
|
174
|
+
});
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it("attributes a stack wait to the PR whose checks are pending, not the bottom", async () => {
|
|
178
|
+
const readyBottom = await readSnapshot({
|
|
179
|
+
reader: fakeReader(),
|
|
180
|
+
context: context(20),
|
|
181
|
+
pendingHistory: "omit",
|
|
182
|
+
allowDraft: false,
|
|
183
|
+
});
|
|
184
|
+
const pendingUpstack = await readSnapshot({
|
|
185
|
+
reader: fakeReader({
|
|
186
|
+
fastPath: { kind: "checks", checks: [pendingCheck("upstack-build")] },
|
|
187
|
+
}),
|
|
188
|
+
context: context(21),
|
|
189
|
+
pendingHistory: "omit",
|
|
190
|
+
allowDraft: false,
|
|
191
|
+
});
|
|
192
|
+
const decision = selectTierMajorStackDecision([readyBottom, pendingUpstack]);
|
|
193
|
+
expect(decision).toMatchObject({
|
|
194
|
+
kind: "waiting",
|
|
195
|
+
frontier: { number: 21 },
|
|
196
|
+
pending: [{ name: "upstack-build" }],
|
|
197
|
+
});
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
it("waits on a draft while checks are pending, then reports the draft gate", async () => {
|
|
201
|
+
const pending = await readSnapshot({
|
|
202
|
+
reader: fakeReader({
|
|
203
|
+
facts: { isDraft: true },
|
|
204
|
+
fastPath: { kind: "checks", checks: [pendingCheck()] },
|
|
205
|
+
}),
|
|
206
|
+
context: context(12),
|
|
207
|
+
pendingHistory: "omit",
|
|
208
|
+
allowDraft: false,
|
|
209
|
+
});
|
|
210
|
+
expect(classifyPr(pending).kind).toBe("waiting");
|
|
211
|
+
|
|
212
|
+
const settled = await readSnapshot({
|
|
213
|
+
reader: fakeReader({ facts: { isDraft: true } }),
|
|
214
|
+
context: context(12),
|
|
215
|
+
pendingHistory: "omit",
|
|
216
|
+
allowDraft: false,
|
|
217
|
+
});
|
|
218
|
+
expect(classifyPr(settled)).toMatchObject({
|
|
219
|
+
kind: "blocker",
|
|
220
|
+
blocker: { kind: "merge-gate", reason: "draft-pr" },
|
|
221
|
+
});
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
describe("queued-stack cadence", () => {
|
|
225
|
+
async function openSnapshot(pr: PrContext) {
|
|
226
|
+
return readSnapshot({
|
|
227
|
+
reader: fakeReader(),
|
|
228
|
+
context: pr,
|
|
229
|
+
pendingHistory: "omit",
|
|
230
|
+
allowDraft: false,
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
it("drops a sweep head only after its snapshot succeeds", async () => {
|
|
235
|
+
const queue = [
|
|
236
|
+
context(20),
|
|
237
|
+
context(21),
|
|
238
|
+
context(22),
|
|
239
|
+
] satisfies NonEmpty<PrContext>;
|
|
240
|
+
let state = createQueueState(queue, 0);
|
|
241
|
+
const first = await openSnapshot(queue[0]);
|
|
242
|
+
state = applyQueueSnapshot(state, first, 0, options).state;
|
|
243
|
+
expect(state.work).toMatchObject({
|
|
244
|
+
kind: "whole-stack-sweep",
|
|
245
|
+
remaining: [{ number: 21 }, { number: 22 }],
|
|
246
|
+
});
|
|
247
|
+
const second = await openSnapshot(queue[1]);
|
|
248
|
+
state = applyQueueSnapshot(state, second, 60, options).state;
|
|
249
|
+
expect(state.work).toMatchObject({
|
|
250
|
+
kind: "whole-stack-sweep",
|
|
251
|
+
remaining: [{ number: 22 }],
|
|
252
|
+
});
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("resumes the sweep at the PR whose read failed", async () => {
|
|
256
|
+
const middle = context(21);
|
|
257
|
+
const base = fakeReader();
|
|
258
|
+
let failNext = true;
|
|
259
|
+
const timeline: string[] = [];
|
|
260
|
+
const reader = {
|
|
261
|
+
...base,
|
|
262
|
+
async pullRequest(pr: PrContext) {
|
|
263
|
+
if (pr.number === middle.number && failNext) {
|
|
264
|
+
failNext = false;
|
|
265
|
+
timeline.push(`fail:${pr.number}`);
|
|
266
|
+
throw new WatcherQueryError({
|
|
267
|
+
kind: "command-exit",
|
|
268
|
+
retryable: true,
|
|
269
|
+
detail: "rate limited",
|
|
270
|
+
code: 1,
|
|
271
|
+
});
|
|
272
|
+
}
|
|
273
|
+
timeline.push(`read:${pr.number}`);
|
|
274
|
+
return base.pullRequest(pr);
|
|
275
|
+
},
|
|
276
|
+
} satisfies GitHubReader;
|
|
277
|
+
let now = 0;
|
|
278
|
+
let sleeps = 0;
|
|
279
|
+
const running = runQueued({
|
|
280
|
+
dependencies: {
|
|
281
|
+
reader,
|
|
282
|
+
clock: {
|
|
283
|
+
now: () => now,
|
|
284
|
+
observedAt: () => "2026-07-26T00:00:00.000Z",
|
|
285
|
+
async sleep(seconds) {
|
|
286
|
+
timeline.push("sleep");
|
|
287
|
+
now += seconds;
|
|
288
|
+
sleeps += 1;
|
|
289
|
+
if (sleeps === 2) throw new Error("stop after resume proof");
|
|
290
|
+
},
|
|
291
|
+
},
|
|
292
|
+
emit(verdict) {
|
|
293
|
+
timeline.push(`emit:${verdict.kind}`);
|
|
294
|
+
},
|
|
295
|
+
},
|
|
296
|
+
contexts: [context(20), middle, context(22)],
|
|
297
|
+
options,
|
|
298
|
+
});
|
|
299
|
+
await expect(running).rejects.toThrow("stop after resume proof");
|
|
300
|
+
expect(timeline).toEqual([
|
|
301
|
+
"emit:QUEUE",
|
|
302
|
+
"read:20",
|
|
303
|
+
"fail:21",
|
|
304
|
+
"emit:RETRY",
|
|
305
|
+
"sleep",
|
|
306
|
+
"read:21",
|
|
307
|
+
"read:22",
|
|
308
|
+
"emit:STATUS",
|
|
309
|
+
"emit:WAITING",
|
|
310
|
+
"sleep",
|
|
311
|
+
]);
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("emits a completed sweep only after its final successful snapshot", async () => {
|
|
315
|
+
const queue = [context(30), context(31)] satisfies NonEmpty<PrContext>;
|
|
316
|
+
let state = createQueueState(queue, 0);
|
|
317
|
+
const first = applyQueueSnapshot(
|
|
318
|
+
state,
|
|
319
|
+
await openSnapshot(queue[0]),
|
|
320
|
+
0,
|
|
321
|
+
options
|
|
322
|
+
);
|
|
323
|
+
expect(first.completedSweepRows).toBeNull();
|
|
324
|
+
state = first.state;
|
|
325
|
+
const second = applyQueueSnapshot(
|
|
326
|
+
state,
|
|
327
|
+
await openSnapshot(queue[1]),
|
|
328
|
+
5,
|
|
329
|
+
options
|
|
330
|
+
);
|
|
331
|
+
expect(
|
|
332
|
+
second.completedSweepRows?.map((row) => Number(row.context.number))
|
|
333
|
+
).toEqual([30, 31]);
|
|
334
|
+
expect(second.state.nextSweepAt).toBe(305);
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
it("ADVANCE continues directly to the new frontier without sleeping", async () => {
|
|
338
|
+
const one = context(40);
|
|
339
|
+
const two = context(41);
|
|
340
|
+
const base = fakeReader();
|
|
341
|
+
const reads = new Map<number, number>();
|
|
342
|
+
const timeline: string[] = [];
|
|
343
|
+
const reader = {
|
|
344
|
+
...base,
|
|
345
|
+
async pullRequest(pr: PrContext) {
|
|
346
|
+
timeline.push(`read:${pr.number}`);
|
|
347
|
+
const facts = await base.pullRequest(pr);
|
|
348
|
+
const count = (reads.get(pr.number) ?? 0) + 1;
|
|
349
|
+
reads.set(pr.number, count);
|
|
350
|
+
return pr.number === one.number && count > 1
|
|
351
|
+
? {
|
|
352
|
+
...facts,
|
|
353
|
+
state: "MERGED" as const,
|
|
354
|
+
mergedAt: "2026-07-26T00:00:00Z",
|
|
355
|
+
}
|
|
356
|
+
: facts;
|
|
357
|
+
},
|
|
358
|
+
} satisfies GitHubReader;
|
|
359
|
+
let now = 0;
|
|
360
|
+
let sleeps = 0;
|
|
361
|
+
const emitted: ProgressVerdict[] = [];
|
|
362
|
+
const running = runQueued({
|
|
363
|
+
dependencies: {
|
|
364
|
+
reader,
|
|
365
|
+
clock: {
|
|
366
|
+
now: () => now,
|
|
367
|
+
observedAt: () => "2026-07-26T00:00:00.000Z",
|
|
368
|
+
async sleep(seconds) {
|
|
369
|
+
timeline.push("sleep");
|
|
370
|
+
now += seconds;
|
|
371
|
+
sleeps += 1;
|
|
372
|
+
if (sleeps === 2) throw new Error("stop after advance proof");
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
emit(verdict) {
|
|
376
|
+
emitted.push(verdict);
|
|
377
|
+
timeline.push(`emit:${verdict.kind}`);
|
|
378
|
+
},
|
|
379
|
+
},
|
|
380
|
+
contexts: [one, two],
|
|
381
|
+
options,
|
|
382
|
+
});
|
|
383
|
+
await expect(running).rejects.toThrow("stop after advance proof");
|
|
384
|
+
expect(emitted.some((event) => event.kind === "ADVANCE")).toBe(true);
|
|
385
|
+
const firstSleep = timeline.indexOf("sleep");
|
|
386
|
+
expect(timeline.slice(firstSleep, firstSleep + 5)).toEqual([
|
|
387
|
+
"sleep",
|
|
388
|
+
"read:40",
|
|
389
|
+
"emit:ADVANCE",
|
|
390
|
+
"read:41",
|
|
391
|
+
"emit:WAITING",
|
|
392
|
+
]);
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
it("deduplicates identical waits and schedules the next due sweep", async () => {
|
|
396
|
+
const queue = [context(50)] satisfies NonEmpty<PrContext>;
|
|
397
|
+
let state = createQueueState(queue, 0);
|
|
398
|
+
state = applyQueueSnapshot(
|
|
399
|
+
state,
|
|
400
|
+
await openSnapshot(queue[0]),
|
|
401
|
+
0,
|
|
402
|
+
options
|
|
403
|
+
).state;
|
|
404
|
+
const first = evaluateQueue(state, 0, options);
|
|
405
|
+
expect(first.kind).toBe("waiting");
|
|
406
|
+
if (first.kind !== "waiting") throw new Error("expected waiting");
|
|
407
|
+
expect(first.emit).toBe(true);
|
|
408
|
+
const second = evaluateQueue(first.state, 10, options);
|
|
409
|
+
expect(second.kind).toBe("waiting");
|
|
410
|
+
if (second.kind !== "waiting") throw new Error("expected waiting");
|
|
411
|
+
expect(second.emit).toBe(false);
|
|
412
|
+
expect(planQueue(second.state, 300).work?.kind).toBe("whole-stack-sweep");
|
|
413
|
+
});
|
|
414
|
+
});
|
|
415
|
+
|
|
416
|
+
it("uses the specified retry floor and cap", () => {
|
|
417
|
+
expect(queryBackoffSeconds(1, 1)).toBe(60);
|
|
418
|
+
expect(queryBackoffSeconds(1, 2)).toBe(120);
|
|
419
|
+
expect(queryBackoffSeconds(60, 4)).toBe(300);
|
|
420
|
+
});
|