Files
sanderling/examples/sample-app/spec.ts
T
pj 69002b07b1 fix(runner,sample-app): surface silent errors and demo a failing property (#15)
* fix(runner): surface non-deadline WaitForIdle errors

Previously the WaitForIdle return value was discarded entirely, hiding
real driver failures (gRPC transport errors, sidecar crashes) behind
the expected deadline-exceeded case. Log non-deadline errors so they
are visible without changing control flow.

* chore(sample-app): drop unused uptime_millis extractor

Registered in SampleApplication but never consumed by spec.ts.

* fix(sample-app): drop trivial appIsRunning property

app_state was hardcoded to 'running' so the property was a tautology
that could never fail. Removing both the extractor and the property
is the simplest fix; demo-grade properties that can fail land next.

* feat(sample-app): add Reset button that zeroes clickCount

Pairs with the next commit's tap-reset action so the fuzzer can
violate clickCountNeverDecreases and demonstrate uatu actually
finding a property violation.

* feat(sample-app): add tap-reset action to exercise Reset button

Weighted at 10/122, fuzzer reaches it within a short run. Pairs with
the Reset button to demonstrate uatu detecting the
clickCountNeverDecreases violation.

* fix(runner): filter WaitForIdle errors via context state, not errors.Is

errors.Is(err, context.DeadlineExceeded) misses gRPC's wrapped
status.DeadlineExceeded, so every step under the maestro driver
logged a spurious warning. Check idleCtx.Err() instead — captures
both deadline-fired and parent-canceled cases regardless of how the
driver wraps them.

* chore(sample-app): tune action weights so demo violates in ~30s

Prior weights left tap-reset rare enough that short demo runs missed
the violation by chance. Bumped to 30/107, with typeUsername reduced
since username noise doesn't help exercise clickCount.

* refactor(runner): route warnings through slog

Adds Options.Logger (defaults to slog.Default()) and converts the
three warning sites that were using fmt.Printf. Progress line stays
on Printf since it's user-facing UI, not a log. Makes the warnings
testable via a capturing handler.

* test(runner): assert WaitForIdle driver errors are logged

Captures slog output via TextHandler into a buffer and asserts the
warning message + injected error text appear when the mock driver
returns a non-context error from WaitForIdle. Guards against a
regression of the silent-error swallow.
2026-04-18 18:48:25 +07:00

64 lines
2.2 KiB
TypeScript

import {
extract,
always,
actions,
weighted,
Tap,
InputText,
taps,
swipes,
} from "@uatu/spec";
// ── Snapshot extractors (fed by SampleApplication.kt) ──────────
// See ./android/src/main/kotlin/dev/uatu/sample/SampleApplication.kt
const clickCount = extract<number>(
(state) => (state.snapshots.click_count as number) ?? 0,
);
const username = extract<string>(
(state) => (state.snapshots.username as string) ?? "",
);
// ── UI elements ────────────────────────────────────────────────
const clickButton = extract((state) => state.ax.find("text:Click me"));
const resetButton = extract((state) => state.ax.find("text:Reset"));
const usernameField = extract((state) => state.ax.find("desc:username_field"));
// ── Properties ─────────────────────────────────────────────────
export const properties = {
clickCountNonNegative: always(() => clickCount.current >= 0),
clickCountNeverDecreases: always(() => {
const previous = clickCount.previous;
return previous === undefined || clickCount.current >= previous;
}),
usernameNeverShrinks: always(() => {
const previous = username.previous;
return previous === undefined || username.current.length >= previous.length;
}),
};
// ── Actions ────────────────────────────────────────────────────
const tapClickMe = actions(() => {
return clickButton.current ? [Tap({ on: clickButton.current })] : [];
});
const typeUsername = actions(() => {
return usernameField.current
? [InputText({ into: usernameField.current, text: "alice" })]
: [];
});
const tapReset = actions(() => {
return resetButton.current ? [Tap({ on: resetButton.current })] : [];
});
export const actionsRoot = weighted(
[50, tapClickMe],
[20, typeUsername],
[30, tapReset],
[5, taps],
[2, swipes],
);
(globalThis as { actions?: unknown; properties?: unknown }).actions = actionsRoot;
(globalThis as { properties?: unknown }).properties = properties;