Repository navigation
test: validate recorded engine runs against the TLA+ model - #36
Merged
Merged
Conversation
rgamba
added this pull request to stack #37
September 25, 2026 01:37
sndre
approved these changes
Sep 25, 2026
rgamba
marked this pull request as ready for review
September 25, 2026 23:10
Base automatically changed from
formal-verification-workflow-liveness
to
main
September 25, 2026 23:12
…rogress) The model check proves properties of the spec; this checks that the spec describes the code. testutils records every store and scheduler write of each test runtime (plus the three reads the model treats as steps) when -PskipperTraceDir is set, with a snapshot of the modelled state after each one. formal/tla/trace turns each workflow's run into a TLC model of SkipperTrace, which accepts it only if the spec can take exactly those steps, and a mutation self-test checks the checker rejects corrupted runs. A new CI job records traces from the end-to-end suites and checks them. Running it corrected the spec where it did not describe the code: workflows are created RUNNING, timer updates follow Timer.Status.canTransitionTo, retries can be due at once, a terminal workflow re-run is persisted again, signals can take the in-memory path, callers call again, in-memory copies run on the instance they were queued with, and the handler's two reads are separate steps. The four open findings keep their verdicts. Still to do: bound long traces, re-run the full self-test, check traces from a deliberately changed engine, and bring the trace README up to date.
A per-trace time budget (TRACE_BUDGET, default 300 s) reports a trace TLC cannot decide as inconclusive, flagged as a warning on GitHub Actions, instead of stalling the job. Only rejections fail it. The longest trace (104 events) now checks in 23 seconds and 241 states instead of not finishing: between events, time only moves the run_after of a row the next event needs. With that the self-test mutates every accepted trace, not only short ones. Also fixed: a run whose persist the trace never reached was left with no allowed outcome and rejected; the runner no longer hits macOS xargs -I's 255-byte command limit; direct writes by a test are reported as such rather than as an unmapped write. The trace README covers the logged reads, the search-size measures, the budget, and an engine change it catches (TimerTaskHandler scheduling the workflow before expiring its timer: end-to-end suites green, 8 runs rejected).
rgamba
force-pushed
the
formal-trace-validation
branch
from
September 25, 2026 23:12
3d17d23 to
b6f5a9d
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
#35 model-checks a TLA+ spec of how the engine drives a workflow. That shows properties hold in the model, but nothing tied the model to the code. This PR adds trace validation:
That catches spec drift, and it catches engine changes that break the protocol even while the tests stay green.
Design
Recording (
testutils/.../trace).-PskipperTraceDir=<dir>makesTestRuntimeandWorkflowTestwrap the store and scheduler in tracing decorators. Without the property, nothing changes.SkipperRuntimebackend checks pass wrapping factories.Conversion (
formal/tla/trace/trace_to_tla.py). Splits each trace by workflow, maps each write to its model action by operation and call site, and reduces state to model values.Checking (
SkipperTrace.tla). A step either consumes the next event (takes its action and must land exactly on its snapshot) or is silent (changes no durable state).waitUntilallows, so the check covers the engine's protocol for any workflow code.Bounded checks. Each trace gets a time budget (
TRACE_BUDGET, default 300 s). A trace TLC can't decide in time is reported as inconclusive, with a CI warning, instead of stalling the job. Only rejections fail it.Keeping the search small. TLC has to guess whatever the trace doesn't pin down. Several measures cut that down, and each can only make the checker reject more, never accept a wrong run:
The details are in
formal/tla/trace/README.md.Self-test (
mutants.py). A checker loose enough to accept anything would also be green.--self-testcorrupts every accepted trace in five ways (versions, statuses, dropped writes, swapped writes), and every mutant must be rejected.CI. A new
trace-validationjob informal.ymlrecords traces from the end-to-end suites (record_traces.sh) and runscheck_traces.sh --self-test.formaljobs now also trigger onsrc/main/**andtestutils/**, since engine changes are exactly what trace validation is for.What it has found so far
The spec was wrong in these places, and they're now fixed:
createWorkflowinsertsRUNNINGCREATED. This also corrects finding 1 in #35's README: the stuck workflow isRUNNING.Timer.Status.canTransitionToallowsFORCE_SIGNAL_WORKFLOW_EXEC_IN_SCHEDULERis offAll of #35's model configs still give their expected verdicts: the four open bugs are still found, and #12 still reproduces with its gate off.
Testing
record_traces.shruns these with tracing on, and they all pass: the SQLite integration suite, the coroutine, factory, service, scheduler-manager, SQLite-scheduler, lease-renewal and timer-handler tests, andskipper-testutils. That writes 121 trace files. Tracing initially broke two tests; both are fixed:SqliteSchedulerTestfailed because my decorator rejected thenullpartition callers pass to mean no filtering.LostSignalOnHonoredLeaseTestfailed because the lock stalled its blocking scheduler wrapper; custom backends are no longer traced.TimerTaskHandlerto schedule the workflow before expiring its timer:formal/tla/check.shgives all 10 verdicts as expected.spotlessCheckis clean, andactionlintpasses on the workflow.