Files
nisse/specs/loop_scenarios.qnt
T
dongarandClaude Opus 5.5 a921b73d34
CI / checks (push) Skipped
CI / checks (pull_request) Successful in 1m43s
CI / e2e (pull_request) Successful in 3m53s
M4.4: an unreachable build VM blocks its projects until it's back (#137)
M4.3's block, with new triggers and a way out. A VM out of reach at the
check before a run, in a run (which ends there: the driver, a toolchain
step and a requested step), or while preparing the workspace blocks every
project on it; the item stays ready, waiting on no one. On the intake round
it is tried again every tuning.build_vm.retry_s (300), and once it answers
it must still answer settle_s (180) later; then the work a lost run left
there is parked (pushed), the VM unblocked, and that said once.

A changed host key, at the check or on a retry, blocks the VM too, until
the operator's `vm ready`, which trusts the new key: `trust new host key`
is retired, and the message says it's a key problem that may be an
impersonation. Questions are answered while a VM is blocked, without it.
Starting the agent with a VM down no longer fails.

The walk breaks the VM's SSH, now or partway through a run, and checks that
no model works on a VM that is down; loop.qnt models the VM going and
coming back (unblocked_only_when_up). Together they found: a lost run's
work set aside by the next item there; a question left behind a block; a
lost VM swallowed while the toolchain step checked or recorded it; and a
remark waiting to be answered that outlived its PR's merge. All fixed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01J5Utyfzw7gF7UhjRDEtjTk
2026-10-08 21:06:05 +00:00

101 lines
4.4 KiB
Plaintext

// Counterexamples found in loop.qnt, written out as short traces. Each was a
// gap in docs/Architecture.md; the model now has the proposed fix (marked
// FIX n in loop.qnt), and each trace checks the fixed behaviour.
//
// Run: quint test specs/loop_scenarios.qnt (runs every *Test)
module loop_scenarios {
import loop.* from "./loop"
// An assigned issue, restated, a go-ahead, milestone 1 under way.
action toWorking = init
.then(assignIssue(1)).then(intake(1, "A", false))
.then(activate(1)).then(restate(false))
.then(say(1, "go")).then(handleMsg)
.then(startMilestone)
// ...and published: its PR is open and the item waits for the merge.
action toPrOpen = toWorking
.then(finishMilestone)
.then(publishStep(false)).then(publishStep(false)).then(publishStep(false))
// 1. Abandoning an item left its open PR open: L2 closed the record and
// aborted intents, but said nothing about PRs. Fixed: abandon closes
// the item's open PRs with a note.
run abandonClosesTheOpenPrTest =
toPrOpen
.then(park)
.then(say(1, "abandon")).then(handleMsg)
.expect(closed_items_leave_nothing and s.prs.get(prKey(1, 1, 0)) == "closed")
// 2. Gitea opened the PR but the response was lost, so the agent never
// knew. The operator closed that PR; the agent's retry only adopted
// *open* PRs, so it opened a second one over the operator's close.
// Fixed: a closed PR on the branch is an answer: report it and ask.
run retryNeverReopensAClosedPrTest =
toWorking
.then(finishMilestone)
.then(publishStep(false))
.then(publishStep(true)) // opened, response lost
.then(prAct(1, "closed")) // the operator closes it
.then(finishMilestone) // the agent retries
.then(publishStep(false)).then(publishStep(false))
.expect(one_pr_per_milestone and itm(s, 1).waitFor == "answer")
// 3. A review comment made the item ready; the operator merged the PR
// before the fix was pushed, so the fix went to a merged branch and
// never reached main. Fixed: a fix for a PR that merged meanwhile goes
// out as a new PR (and one for a closed PR is reported).
run reviewFixAfterMergeGetsANewPrTest =
toPrOpen
.then(prAct(1, "comment")).then(observe(1))
.then(prAct(1, "merged"))
.then(answerReview)
.expect(review_fixes_land and itm(s, 1).stage == "working" and itm(s, 1).attempt == 1)
// 4. After a PR closed unmerged and the operator answered, the next
// milestone could start with the closed one never merged. Fixed: a
// redo is the same milestone on a new branch and PR.
run closedPrRedoesTheSameMilestoneTest =
toPrOpen
.then(prAct(1, "closed")).then(observe(1))
.then(say(1, "answer")).then(handleMsg)
.then(startMilestone)
.expect(milestone_after_merge and itm(s, 1).ms == 1 and itm(s, 1).attempt == 1)
// 5. Work begun in conversation gets a record issue, assigned to the
// agent. Opening it and storing the link were two steps, and intake
// could take the assigned, unlinked issue for new work in between.
// Fixed: the issue carries its work item's ID from creation.
run recordIssueIsNeverTakenForNewWorkTest =
init
.then(converse("A")).then(activate(1)).then(restate(false))
.then(say(1, "go")).then(handleMsg)
.then(createRecord)
.expect(s.issues.get(99).linked == 1)
.then(linkRecord)
.expect(one_item_per_issue and itm(s, 1).issue == 99)
// M4.3: a build VM the driver finds broken blocks its project; nothing is
// taken up on it until the operator says `vm ready`, then work goes on.
run aBrokenVmBlocksUntilReadyTest =
init
.then(assignIssue(1)).then(intake(1, "A", false))
.then(activate(1)).then(requestReimage)
.expect(s.vmBlocked.contains("A") and s.active == 0)
.then(vmReady("A"))
.then(activate(1))
.expect(blocked_vm_runs_nothing and s.active == 1)
// #137: an out-of-reach VM found at the check blocks its project; a probe
// can't unblock it while it's down, and does once it's back.
run anUnreachableVmBlocksUntilItAnswersTest =
init
.then(assignIssue(1)).then(intake(1, "A", false))
.then(goDown("A"))
.then(activate(1))
.expect(s.vmUnreachable.contains("A") and s.active == 0)
.then(comeBack("A")).then(probe("A"))
.then(activate(1))
.expect(all_properties and s.active == 1)
}