Skip to content

Commit 75aef71

Browse files
fix(server): a PR watch still owes its final wake when a pending check turns required
Co-Authored-By: Claude Opus 5.5 <[email protected]>
1 parent c762335 commit 75aef71

5 files changed

Lines changed: 31 additions & 10 deletions

File tree

‎apps/server/src/orchestration-v2/Orchestrator.ts‎

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3400,6 +3400,9 @@ const makeOrchestrator = Effect.fn("orchestrationV2.Orchestrator.layer")(functio
34003400
passedChecks: existing.endedWatch.passedChecks,
34013401
conflicting: existing.endedWatch.conflicting,
34023402
}),
3403+
...(existing.endedWatch?.owedChecks !== undefined && {
3404+
owedChecks: existing.endedWatch.owedChecks,
3405+
}),
34033406
...(existing.endedWatch?.checksAwaitedSince !== undefined && {
34043407
checksAwaitedSince: existing.endedWatch.checksAwaitedSince,
34053408
}),

‎apps/server/src/orchestration-v2/PullRequestWatchReactor.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -171,6 +171,7 @@ function watchesEqual(left: ThreadPullRequestWatch, right: ThreadPullRequestWatc
171171
left.failedChecks.join("\n") === right.failedChecks.join("\n") &&
172172
left.passed === right.passed &&
173173
left.passedChecks.join("\n") === right.passedChecks.join("\n") &&
174+
(left.owedChecks ?? []).join("\n") === (right.owedChecks ?? []).join("\n") &&
174175
left.remarksThrough === right.remarksThrough &&
175176
left.remarkIds.join("\n") === right.remarkIds.join("\n") &&
176177
left.conflicting === right.conflicting &&

‎apps/server/src/orchestration-v2/pullRequestWatch.test.ts‎

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -162,6 +162,14 @@ describe("evaluatePullRequestWatch", () => {
162162
const botDone = detail({ checks: [...done.checks, check("bot", "success")] });
163163
assert.deepEqual(evaluatePullRequestWatch(churn.next, botDone, noRemarks).changes, []);
164164

165+
// A pending check that turns required on the next read still finishes with one wake.
166+
const promoted = detail({
167+
checks: [required("test", "success"), required("e2e", "success")],
168+
});
169+
const gated = evaluatePullRequestWatch(first.next, promoted, noRemarks);
170+
assert.deepEqual(gated.changes, [{ kind: "checks-passed", count: 2, required: true }]);
171+
assert.deepEqual(evaluatePullRequestWatch(gated.next, promoted, noRemarks).changes, []);
172+
165173
// Nothing else was pending when the required ones passed, so nothing more is owed.
166174
const green = evaluatePullRequestWatch(watch(), done, noRemarks);
167175
assert.deepEqual(green.changes, [{ kind: "checks-passed", count: 1, required: true }]);

‎apps/server/src/orchestration-v2/pullRequestWatch.ts‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,6 +51,7 @@ export function evaluatePullRequestWatch(
5151
let failedChecks = headMoved ? [] : watch.failedChecks;
5252
let passed = headMoved ? false : watch.passed;
5353
let passedChecks = headMoved ? [] : watch.passedChecks;
54+
let owedChecks = headMoved ? [] : (watch.owedChecks ?? []);
5455
if (detail.checks.length > 0) {
5556
const failed = detail.checks.filter(isFailedCheck);
5657
const newlyFailed = failed.filter((check) => !failedChecks.includes(check.name));
@@ -66,25 +67,30 @@ export function evaluatePullRequestWatch(
6667
// required checks, any check counts, and advisory bots keep adding passed ones: no wake.
6768
const gateGrew = required.length > 0 && gateNames.some((name) => !told.includes(name));
6869
// Other checks still pending when required ones passed are owed one wake when all settle.
69-
// They are kept with the told names until then, so later advisory churn never wakes.
70+
// Only those are owed, so later advisory churn never wakes. One turned required is gate news.
7071
const owed =
71-
required.length > 0
72-
? detail.checks.filter((check) => check.required !== true && told.includes(check.name))
72+
passedNow && passed
73+
? detail.checks
74+
.filter((check) => check.required !== true && owedChecks.includes(check.name))
75+
.map((check) => check.name)
7376
: [];
7477
const settled =
7578
failed.length === 0 && detail.checks.every((check) => check.status !== "pending");
76-
let owedNow = passedNow && passed ? owed : [];
79+
owedChecks = owed;
7780
if (passedNow && (!passed || gateGrew)) {
7881
changes.push({ kind: "checks-passed", count: gate.length, required: required.length > 0 });
79-
owedNow = detail.checks.filter(
80-
(check) => check.required !== true && check.status === "pending",
81-
);
82-
} else if (settled && owedNow.length > 0) {
82+
owedChecks =
83+
required.length > 0
84+
? detail.checks
85+
.filter((check) => check.required !== true && check.status === "pending")
86+
.map((check) => check.name)
87+
: [];
88+
} else if (settled && owed.length > 0) {
8389
changes.push({ kind: "checks-passed", count: detail.checks.length, required: false });
84-
owedNow = [];
90+
owedChecks = [];
8591
}
8692
passed = passedNow;
87-
passedChecks = passedNow ? [...gateNames, ...owedNow.map((check) => check.name)] : [];
93+
passedChecks = passedNow ? gateNames : [];
8894
}
8995

9096
// Only a head never read with checks waits for them: an empty read after checks were seen is
@@ -142,6 +148,7 @@ export function evaluatePullRequestWatch(
142148
failedChecks,
143149
passed,
144150
passedChecks,
151+
owedChecks,
145152
remarksThrough,
146153
remarkIds,
147154
conflicting,

‎packages/contracts/src/threadPullRequest.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,8 @@ export const ThreadPullRequestWatch = Schema.Struct({
8989
passedChecks: Schema.Array(TrimmedNonEmptyString).pipe(
9090
Schema.withDecodingDefault(Effect.succeed([])),
9191
),
92+
/** Checks still pending when required ones passed early, owed one wake once all settle. */
93+
owedChecks: Schema.optional(Schema.Array(TrimmedNonEmptyString)),
9294
remarksThrough: IsoDateTime,
9395
/** Remarks created exactly at `remarksThrough` that were reported, so a late one still counts. */
9496
remarkIds: Schema.Array(TrimmedNonEmptyString),

0 commit comments

Comments
 (0)