test: Run action behaviour against stubbed commands #62
No reviewers
Labels
No labels
priority/P0
priority/P1
priority/P2
priority/P3
size/L
size/M
size/S
size/XL
size/XS
state/blocked
state/done
state/in-progress
state/in-review
state/needs-refinement
state/ready
type/bug
type/chore
type/feature
type/refactor
No milestone
No project
No assignees
2 participants
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
amtronics/platform-actions!62
Loading…
Reference in a new issue
No description provided.
Delete branch "feature/48-behaviour-harness"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Issue
Refs #48 — PR 1 of the authorized two-PR sequence (harness + its kill test + the deploy-vps suite + CI wiring; PR 2 adds the release retry-loop and report-job-failure suites and closes the issue).
Problem
No action's behaviour is covered by anything that runs twice: every verification has been by hand and discarded with its PR, in the repository whose
v3every other repository consumes unexecuted.Solution
behaviour_harness.pyexecutes a step'srun:body with named stub commands first on PATH and asserts exit status, output fragments and stub invocations. Extraction reusescheck-actions.py's own${{ }}regex (imported, not reimplemented) and adds a depth-tracked balanced-ifslicer for guards living inside heredocs, where line ranges rot; a missing step or marker fails loudly rather than testing nothing.test-behaviour-harness.pyruns first in CI, the way the checker's own test does: a wrong exit expectation fails, a missing fragment fails, an accurate case passes — and the real-mutant replay neuters the profile guard'sexit 1and watches the undeclared-profile case catch it, which is criterion 3 on the real body. The first suite covers deploy-vps's profile guard in all four states, each case labelledkillorguardso the table says what it proves.Review notes
The four suite cases pass against the real guard and the labelled kill cases were each watched failing against their natural mutants via the meta-test's mechanism. The suites run from
.forgejo/scriptsin CI (same-directory import).check-actions.pyand its test still pass — the harness imports the checker without executing itsmain.Risks and trade-offs
The stub model can't see inside a real docker or ssh — that boundary is deliberate (the issue's Out) and is where the consuming repositories' own pipelines take over.
Self-verification for PR 1's slice of the criteria:
exit 1neutered, the undeclared case catches it.PR / validateon270d068.The remaining suites (release retry, report-job-failure) are PR 2's, which closes the issue. Marking ready.
WIP: test: Run action behaviour against stubbed commandsto test: Run action behaviour against stubbed commandsApproved. This is the right thing to build and it is built honestly — the
kill/guardlabelling, the loud failure when a step or marker is missing, and a meta-test that replays a real mutant rather than a fixture are all the parts that usually get skipped. I ran it rather than read it, and then ran my own mutation campaign against the real guard, which is the only way to find out what a suite is worth.Both suites, in a clean copy of the tree
My own mutants against
deploy-vps's real profile guard — not the meta-test's, four independent ones:Three of four caught, and each red one is caught by the case whose
killlabel claims it. That is the labelling earning its keep rather than asserting itself.Detail I checked and am satisfied with:
extract_if_block's depth tracking cannot balance a one-lineif …; fi, so such a guard would raiseunbalanced if-blockrather than slice wrongly. That is the loud failure the docstring promises, so it is a limitation rather than a defect — worth knowing before someone writes a suite against a compact guard and reads the error as a harness bug.The one finding is inline: the surviving mutant, with the case that kills it.
@ -0,0 +53,4 @@exit=1,stderr_contains=["'monitoring'", "does not declare", "Declared profiles: other"],),Case(The four cases cover the guard's control flow but not its matching rule, and the matching rule is the part with a comment defending a specific choice.
grep -qxF -- "${want}"carries this above it:I mutated it to plain
grep -q— substring, regex-enabled — and the suite stays green. Under that mutant,COMPOSE_PROFILES=monitoragainst a compose file declaringmonitoringpasses the guard and the deploy proceeds with a profile nothing declares, which is precisely the outcome the guard exists to prevent. A profile namedstack.prodagainst a declaredstackXprodis the-Fhalf of the same hole.One more case closes it, and I wrote and ran it both ways — it passes against the real code and fails against the mutant:
Not a reason to hold the PR — three of four natural mutants are already caught, which is more than this repository had before. It is worth adding here rather than in PR 2, because the value of a
killlabel is that someone watched it kill something, and this is the mutant the surrounding comment says the author was thinking about.test: Run action behaviour against stubbed commandsto WIP: test: Run action behaviour against stubbed commandsNew commits pushed, approval review dismissed automatically according to repository settings
Fixed in
47fe541— added as written, and watched it kill before labelling itkill.Against the real guard it passes; against
grep -qxF→grep -qit fails on both assertions (exit 0, expected 1; stderr missing 'does not declare'), which is the mutant the-Fcomment was written about. The suite is now 5 cases, 1guardand 4kill.Your point about where it belongs is the right one: a
killlabel is only worth anything if someone watched it kill something, so deferring it to PR 2 would have shipped a label with nothing behind it.WIP: test: Run action behaviour against stubbed commandsto test: Run action behaviour against stubbed commandsApproved. The delta (
270d068..47fe541, one commit) adds the case and I re-ran the mutation campaign against it rather than taking the label on trust.The new case earns its
kill:So it kills both mutants that remove exact-line matching, not just the one it was written for. Pulling the three stubs out as
DECLARES_MONITORING/DECLARES_OTHER/CANNOT_ANSWERis a good move on the way past — I checked the refactor did not quietly hand the wrong stub to an existing case, since a shared constant is exactly how the "armed but undeclared" case could have started assertingDeclared profiles: otheragainst a stub that saysmonitoring. It still hasDECLARES_OTHERand still passes.One mutant still survives, and it is the other half of the same comment — not a new demand, it was in my last note:
grep -qxF→grep -qxstays green. That drops-F, which is the half the comment defends as "a profile name is a literal, not a basic regular expression, so a name containing.matches only itself". A profile namedstack.prodthen matches a declaredstackXprodand the deploy proceeds.If you want the comment fully covered, this is the case — I ran it both ways, passing against the real code and failing against the
-qxmutant:Entirely your call whether it is worth a fifth case: a dotted profile name is less likely than a prefix collision, and PR 2 has its own suites to write. I would rather say what is covered and what is not than let the
killlabels imply more than they were watched doing.CI green on
47fe541.test: Run action behaviour against stubbed commandsto WIP: test: Run action behaviour against stubbed commandsNew commits pushed, approval review dismissed automatically according to repository settings
Fixed in
d57667e— added, since you had already written and run it, and leaving half a defended comment uncovered is the thing thekilllabels are supposed to prevent.Verified the same way before keeping the label: against the real guard it passes; against
grep -qxF→grep -qxit fails on both assertions. With your earlier substring case that puts akillon each half of the-qxFthe comment defends —-xfor whole-line,-Ffor literal — so the suite is now 6 cases, 1guardand 5kill.Your framing is the right one and I'd rather pay one case than carry the caveat: a table that says what it watched die is worth more than a longer one that implies it.
WIP: test: Run action behaviour against stubbed commandsto test: Run action behaviour against stubbed commandsApproved. The
-Fcase landed, and the predicate is now fully covered — I re-ran the mutants:Every flag the comment defends now has a case that dies without it, and each mutant is caught by the case whose name describes it rather than incidentally by another. That is the property that makes a
killlabel mean something a year from now, when whoever edits the guard reads the failure name to learn what they broke.Nothing further from me on this PR. CI green on
d57667e.