Redeliver an interrupted review comment, and close the issue on completion
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m16s
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m16s
Fixes counterexamples 6 and 7 from specs/orchestrator.qnt, the last two. 6. A crash while the executor answered a review comment lost it: its cursor is saved before the work (at most once), and a restart in PR_OPEN resumed quietly. Now pr.reviewing marks the comment until the executor returns (every way it returns, but not a crash), and boot resumes the watch just before it, with a notice. The mark stays until it is answered, so a second crash cannot lose it either. 7. 'project complete' left the plan issue open. It now closes it with a note, after IDLE is saved, as abandon does (shared helper). The model marks and redelivers, and closes the issue; scenarios 6 and 7 now expect their properties to hold. A mutant without redelivery fails scenario 6. All eight properties held together over 300,000 traces. No xfail reproductions remain. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PPQ14L3LikyefCMFBqEbKE
This commit is contained in:
1 parent
83a4bd72b5
commit
848bd9f122
4 files changed
+153
-52
No files matched your search
+20
-11
@@ -60,6 +60,7 @@ module orchestrator {
|
||||
ms: int,
|
||||
pending: Pending,
|
||||
prOn: bool, prKey: int, prLast: int, // _data["pr"]
|
||||
reviewing: int, // pr["reviewing"]: the comment being answered, 0 if none
|
||||
boardOn: bool, boardPosted: bool, boardLast: int, // _data["board"]
|
||||
intents: Set[int], // pending open_pr intents, by PR key
|
||||
// -- Gitea --
|
||||
@@ -127,7 +128,7 @@ module orchestrator {
|
||||
startPlanning(
|
||||
{ ...s, tid: t, task: true, gate: NO_GATE,
|
||||
revs: Map("arch" -> 0, "ms1" -> 0, "ms2" -> 0), lastRev: 0, ms: 1, pending: NO_PENDING,
|
||||
prOn: false, prKey: 0, prLast: 0,
|
||||
prOn: false, prKey: 0, prLast: 0, reviewing: 0,
|
||||
boardOn: giteaUp, boardPosted: false, boardLast: 0,
|
||||
openIssues: if (giteaUp) s.openIssues.union(Set(t)) else s.openIssues,
|
||||
planWatch: OFF, prWatch: OFF },
|
||||
@@ -182,15 +183,17 @@ module orchestrator {
|
||||
// _abandon: IDLE first, then close the issue and the PR, abort intents.
|
||||
pure def abandon(s: Sys): Sys =
|
||||
{ ...s, st: "IDLE", task: false, gate: NO_GATE, pending: NO_PENDING,
|
||||
prOn: false, boardOn: false, boardPosted: false, intents: Set(),
|
||||
prOn: false, reviewing: 0, boardOn: false, boardPosted: false, intents: Set(),
|
||||
openIssues: s.openIssues.exclude(Set(s.tid)),
|
||||
giteaPr: if (s.prOn) s.giteaPr.put(s.prKey, "closed") else s.giteaPr,
|
||||
planWatch: OFF, prWatch: OFF }
|
||||
|
||||
// _handle_pr_open: 'project complete'
|
||||
// _handle_pr_open: 'project complete' closes the plan issue; the PR is
|
||||
// the operator's to merge.
|
||||
pure def complete(s: Sys): Sys =
|
||||
{ ...s, st: "IDLE", task: false, gate: NO_GATE, pending: NO_PENDING,
|
||||
prOn: false, boardOn: false, boardPosted: false, planWatch: OFF, prWatch: OFF }
|
||||
prOn: false, reviewing: 0, boardOn: false, boardPosted: false,
|
||||
openIssues: s.openIssues.exclude(Set(s.tid)), planWatch: OFF, prWatch: OFF }
|
||||
|
||||
// _resume_after_restart
|
||||
pure def resume(s: Sys, p: Pending, j: Job): Sys =
|
||||
@@ -245,11 +248,12 @@ module orchestrator {
|
||||
if (not(SIGNOFF.contains(s.st))) s // not taken
|
||||
else applyGate({ ...s, boardLast: max(s.boardLast, j.cid) }, j)
|
||||
|
||||
// _handle_review_comment_now, first half: the executor starts on it.
|
||||
// _handle_review_comment_now, first half: the executor starts on it,
|
||||
// with the comment marked (durably) until it returns.
|
||||
pure def handleReviewComment(s: Sys, j: Job): Sys =
|
||||
if (s.st != "PR_OPEN") s // not taken
|
||||
else { ...s, prLast: max(s.prLast, j.cid), taken: s.taken.union(Set(j.cid)),
|
||||
work: "review", workCid: j.cid }
|
||||
reviewing: j.cid, work: "review", workCid: j.cid }
|
||||
|
||||
pure def handleJob(s: Sys, j: Job, giteaUp: bool): Sys =
|
||||
if (j.src == "matrix") handleMatrix(s, j, giteaUp)
|
||||
@@ -271,7 +275,10 @@ module orchestrator {
|
||||
{ ...s0, st: "PR_OPEN", prOn: true, prKey: prKeyOf(s), prLast: 0,
|
||||
prWatch: { on: true, floor: 0 }, planWatch: OFF }
|
||||
else if (s.st == "PR_OPEN")
|
||||
{ ...s0, prWatch: if (s.prOn) { on: true, floor: s.prLast } else OFF }
|
||||
// A comment whose answer never finished is delivered again.
|
||||
{ ...s0, prWatch: if (s.prOn)
|
||||
{ on: true, floor: if (s.reviewing > 0) s.reviewing - 1 else s.prLast }
|
||||
else OFF }
|
||||
else if (s.st == "IDLE" or (SIGNOFF.contains(s.st) and s.gate.on))
|
||||
if (s.gate.on and s.boardPosted) { ...s0, planWatch: { on: true, floor: s.boardLast } } else s0
|
||||
else
|
||||
@@ -284,7 +291,7 @@ module orchestrator {
|
||||
action init = s' = {
|
||||
st: "IDLE", tid: 0, task: false, gate: NO_GATE,
|
||||
revs: Map("arch" -> 0, "ms1" -> 0, "ms2" -> 0), lastRev: 0, ms: 1, pending: NO_PENDING,
|
||||
prOn: false, prKey: 0, prLast: 0,
|
||||
prOn: false, prKey: 0, prLast: 0, reviewing: 0,
|
||||
boardOn: false, boardPosted: false, boardLast: 0,
|
||||
intents: Set(),
|
||||
planThread: List(), prThread: List(), nextId: 1,
|
||||
@@ -418,7 +425,7 @@ module orchestrator {
|
||||
action finishReview(outcome: str): bool = all {
|
||||
s.work == "review", outcome != "stopped" or s.cancel,
|
||||
s' =
|
||||
val t = endWork({ ...s, answered: s.answered.union(Set(s.workCid)) })
|
||||
val t = endWork({ ...s, answered: s.answered.union(Set(s.workCid)), reviewing: 0 })
|
||||
if (outcome == "stopped" or outcome == "escalate")
|
||||
awaitInput(t, { kind: "review_stopped", g: "", interrupted: "" })
|
||||
else t,
|
||||
@@ -464,9 +471,11 @@ module orchestrator {
|
||||
// Never more than one PR opened for a milestone.
|
||||
val one_pr_per_milestone = s.opened.keys().forall(k => s.opened.get(k) <= 1)
|
||||
|
||||
// A review comment the agent took up is answered, or is being worked on.
|
||||
// A review comment the agent took up is answered, being worked on, or
|
||||
// marked to be delivered again: never silently dropped.
|
||||
val review_comments_answered =
|
||||
s.taken.forall(id => s.answered.contains(id) or (s.work == "review" and s.workCid == id))
|
||||
s.taken.forall(id => s.answered.contains(id) or (s.work == "review" and s.workCid == id)
|
||||
or s.reviewing == id)
|
||||
|
||||
// With no task, no plan issue is left open.
|
||||
val no_open_issue_without_a_task = not(s.task) implies s.openIssues.size() == 0
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
// Counterexamples found in orchestrator.qnt, written out as short traces.
|
||||
// Each ends by expecting the property it breaks to be false; once the design
|
||||
// is fixed, the expectation is flipped and the trace kept as a regression.
|
||||
// Fixed: 1-4 (replies bind to the revision up when they arrived), 5 (boot
|
||||
// and publishing adopt an open PR).
|
||||
// All fixed: 1-4 (replies bind to the revision up when they arrived), 5 (boot
|
||||
// and publishing adopt an open PR), 6 (redelivered), 7 (issue closed).
|
||||
//
|
||||
// Run: quint test specs/orchestrator_scenarios.qnt (runs every *Test)
|
||||
module orchestrator_scenarios {
|
||||
@@ -107,18 +107,19 @@ module orchestrator_scenarios {
|
||||
|
||||
// 6. A crash while the executor answers a review comment. The comment's
|
||||
// cursor was saved before the work started (at most once), and a
|
||||
// restart in PR_OPEN resumes quietly, so the comment is never answered
|
||||
// and nobody is told.
|
||||
// restart in PR_OPEN resumed quietly, so it was never answered. Fixed:
|
||||
// the comment is marked while answered, and delivered again.
|
||||
run crashDuringAReviewLosesTheCommentTest =
|
||||
toPrOpen
|
||||
.then(commentPr).then(pollPr).then(runJob(true)) // the executor starts on it
|
||||
.then(crash(true))
|
||||
.expect(s.st == "PR_OPEN" and not(review_comments_answered))
|
||||
.expect(s.st == "PR_OPEN" and review_comments_answered)
|
||||
.then(pollPr).then(runJob(true)).then(finishReview("completed"))
|
||||
.expect(s.answered.size() == 1 and s.reviewing == 0)
|
||||
|
||||
// 7. A completed task leaves its plan issue open: 'project complete'
|
||||
// closes nothing, unlike 'abandon'.
|
||||
// 7. 'project complete' left the plan issue open. Fixed: it closes it.
|
||||
run projectCompleteLeavesThePlanIssueOpenTest =
|
||||
toPrOpen
|
||||
.then(send("complete")).then(runJob(true))
|
||||
.expect(s.st == "IDLE" and not(no_open_issue_without_a_task))
|
||||
.expect(s.st == "IDLE" and no_open_issue_without_a_task)
|
||||
}
|
||||
Reference in new issue
Block a user