Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

62 changes: 62 additions & 0 deletions pstack/skills/poteto-mode/scripts/watch-pr/policy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {
queryBackoffSeconds,
readSnapshot,
runQueued,
runSimple,
selectTierMajorStackDecision,
} from "./policy.ts";
import {
Expand Down Expand Up @@ -418,3 +419,64 @@ it("uses the specified retry floor and cap", () => {
expect(queryBackoffSeconds(1, 2)).toBe(120);
expect(queryBackoffSeconds(60, 4)).toBe(300);
});


describe("polling deadline sleeps", () => {
for (const mode of ["single", "queued-stack", "query-retry"] as const) {
it(`caps ${mode} sleeps at the configured deadline`, async () => {
let now = 0;
const sleeps: number[] = [];
const pending = fakeReader({ fastPath: { kind: "checks", checks: [pendingCheck()] } });
const reader = mode === "query-retry"
? { ...pending, async pullRequest(): Promise<PullRequestFacts> {
throw new WatcherQueryError({ kind: "command-exit", retryable: true, code: 1, detail: "temporary failure" });
} }
: pending;
const dependencies = {
reader,
clock: {
now: () => now,
observedAt: () => "2026-07-26T00:00:00.000Z",
async sleep(seconds: number) { sleeps.push(seconds); now += seconds; },
},
emit() {},
};
const polling = { ...options, interval: 60, timeout: 5 };
const result = mode === "queued-stack"
? await runQueued({ dependencies, contexts: [context(1)], options: polling })
: await runSimple({ dependencies, contexts: [context(1)], mode: "single", statusOnly: false, options: polling });
expect(result.kind).toBe("TIMEOUT");
expect(sleeps).toEqual([5]);
expect(now).toBe(5);
});
}
it("preserves the polling interval when the deadline is disabled", async () => {
let now = 0;
let reads = 0;
const sleeps: number[] = [];
const pending = fakeReader({ fastPath: { kind: "checks", checks: [pendingCheck()] } });
const passed = fakeReader({ fastPath: { kind: "checks", checks: [passingCheck()] } });
const result = await runSimple({
dependencies: {
reader: {
...pending,
async checksFastPath(pr) {
reads += 1;
return reads === 1 ? pending.checksFastPath(pr) : passed.checksFastPath(pr);
},
},
clock: {
now: () => now,
observedAt: () => "2026-07-26T00:00:00.000Z",
async sleep(seconds) { sleeps.push(seconds); now += seconds; },
},
emit() {},
},
contexts: [context(1)], mode: "single", statusOnly: false,
options: { ...options, interval: 60, timeout: 0 },
});
expect(result.kind).toBe("READY");
expect(sleeps).toEqual([60]);
});

});
10 changes: 8 additions & 2 deletions pstack/skills/poteto-mode/scripts/watch-pr/policy.ts
Original file line number Diff line number Diff line change
Expand Up @@ -370,6 +370,12 @@ async function pollUntilTerminal<V>(args: {
}): Promise<V | T.BlockerVerdict | T.TimeoutVerdict> {
let failures = 0;
const started = args.dependencies.clock.now();
const sleep = async (seconds: number): Promise<void> => {
const remaining = args.options.timeout > 0
? Math.max(0, args.options.timeout - (args.dependencies.clock.now() - started))
: seconds;
await args.dependencies.clock.sleep(Math.min(seconds, remaining));
};
while (true) {
let result: StepResult<V>;
try {
Expand Down Expand Up @@ -400,7 +406,7 @@ async function pollUntilTerminal<V>(args: {
exitCode: 5,
reason: { kind: "status-unavailable", failure: error.failure },
});
await args.dependencies.clock.sleep(retryInSeconds);
await sleep(retryInSeconds);
continue;
}
if (result.kind === "terminal") return result.verdict;
Expand All @@ -410,7 +416,7 @@ async function pollUntilTerminal<V>(args: {
deadlinePassed(started, args.options, args.dependencies.clock.now())
)
return result.onDeadline();
await args.dependencies.clock.sleep(result.seconds);
await sleep(result.seconds);
}
}
}
Expand Down