feat(runner): re-fetch on transitional hierarchy capture

Some actions trigger async work (DB write, ViewModel coroutine) whose
navigation transition begins after the sidecar settle poll has already
exited. Without intervention, the next iteration's hierarchy fetch
lands mid cross-fade and the verifier observes a partial extractor
state which then surfaces as a false-positive violation at the step
where the transition completes.

fetchSyncedState pairs hierarchy + screenshot in one goroutine and
retries the pair (up to 4 times, 200ms apart) while the captured tree
contains more than one route-level *Screen tag. Steps that observe
no transition get no added cost; steps that catch a transition pay
up to ~600ms extra wall time but record a tree that matches the
post-transition state the property language expects to compare.
This commit is contained in:
pj committed 2026-05-31 12:11:29 +05:30
1 parent 8deef1a425
commit ddec95a2c5
2 files changed
+118 -20

No files matched your search

+29
View File
@@ -17,6 +17,7 @@ import (
"github.com/priyanshujain/sanderling/internal/driver"
mockdriver "github.com/priyanshujain/sanderling/internal/driver/mock"
"github.com/priyanshujain/sanderling/internal/hierarchy"
"github.com/priyanshujain/sanderling/internal/trace"
"github.com/priyanshujain/sanderling/internal/verifier"
)
@@ -493,6 +494,34 @@ func TestRunner_OneScreenshotPerStep(t *testing.T) {
}
}
// TestIsTransitionalHierarchy_DetectsMultipleScreens covers the runner-side
// guard that re-fetches when the hierarchy still carries two route-level
// *Screen ids - the NavHost cross-fade signature.
func TestIsTransitionalHierarchy_DetectsMultipleScreens(t *testing.T) {
multi, err := hierarchy.Parse(`{"attributes":{"resource-id":"root"},"children":[
{"attributes":{"resource-id":"AddAccountScreen"},"children":[]},
{"attributes":{"resource-id":"HomeScreen"},"children":[]}
]}`)
if err != nil {
t.Fatal(err)
}
if !isTransitionalHierarchy(multi) {
t.Error("expected multi-screen tree to be flagged as transitional")
}
single, err := hierarchy.Parse(`{"attributes":{"resource-id":"HomeScreen"},"children":[]}`)
if err != nil {
t.Fatal(err)
}
if isTransitionalHierarchy(single) {
t.Error("single-screen tree must not be flagged as transitional")
}
if isTransitionalHierarchy(nil) {
t.Error("nil tree must not be flagged as transitional")
}
}
// TestRunner_WaitActionSkipsIdle ensures the runner does not call WaitForIdle
// after a Wait action - the action already provides settling time.
func TestRunner_WaitActionSkipsIdle(t *testing.T) {