Milestone 2 Offline Validation¶
Milestone 2 (Hybrid Local AI Core) is not complete until the main user flows run with no cloud provider, no API key, and no backend service configured.
This document is the offline validation checklist for Milestone 2 and the
companion to the automated unit suite in
app/src/test/java/dev/touchpilot/app/agent/OfflineMilestone2Test.kt.
Scope¶
These flows must all succeed with the device in airplane mode and no provider secret configured:
Hello— conversational gate greeting.Help— conversational gate help reply.Open Settings— deterministic local router routes toopen_app.Go back— deterministic local router routes topress_back.- One ambiguous request — local-model-needed path routes through
LocalModelCommandProvider, falling through to the deterministic router when no local model asset is bundled. - One blocked secret-entry path —
DefaultActionPolicyblockstype_textwhen the text contains a recognized secret, and the agent emits apolicy_blockedevent.
Prerequisites¶
- Build with
./gradlew assembleDebug(no network required after the first build). - For unit verification:
./gradlew testruns the offline suite on the JVM with no Android device, no emulator, and no provider secret. - For live verification on an emulator or device:
- Put the device in airplane mode.
- Confirm
Settings -> Apps -> TouchPilot -> Permissionsshows no cleared provider secret. - Confirm the runtime mode is
Local RouterorLocal Modelin the agent panel.
Checklist¶
Automated (JVM unit suite)¶
Every test drives the real DefaultLocalReasoningCore and asserts the event
contract it actually emits, rather than synthesising events. For flows that
reach AgentRunner, the tests inject a fake AgentRunInvocation that returns
events built with the same AgentEvent factory methods the production runner
uses (see AgentRunner.kt).
-
./gradlew :app:testDebugUnitTest --tests "dev.touchpilot.app.agent.OfflineMilestone2Test"passes. -
Helloandhelpeach produce exactlyuser_messagethenfinal_answerthrough the conversational short-circuit inDefaultLocalReasoningCore, with no tool events and no invocation. -
Open Settingsgoes throughIntentGateand reaches the runner withexactCommand = open_app(target=settings)andproviderMode = LOCAL_ROUTER; the resulting event sequence isuser_message -> tool_requested -> approval_required -> tool_running -> tool_succeeded -> final_answer. -
Go backfollows the same shape, producing anapproval_requiredevent forpress_backand atool_succeededfor the same tool. - An unsafe phrase such as
change my password pleaseis short-circuited byIntentGate.UnsafeRequestand emitsuser_message -> policy_blocked -> final_answerwithout invoking the runner at all. - An ambiguous reference such as
do the thingis classified asIntentDecision.ClarificationNeededand emitsuser_message -> assistant_message(clarification prompt) without invoking the runner. - A non-deterministic request such as
show me something usefulis classified asIntentDecision.LocalModelNeeded; the core hands off to invocation preserving the session'sproviderModeand never setsexactCommand. No cloud provider is required. -
type_textwithtext=my password is hunter2is rejected byDefaultActionPolicyas a second line of defence and the agent emits apolicy_blockedevent with the matching wire shape.
Live (emulator or device, optional but recommended)¶
- Device is in airplane mode for the duration of the run.
-
Hellotyped in the chat returns the conversational reply. -
Helptyped in the chat returns the conversational reply. -
Open Settingsopens the Settings app via the local router. -
Go backissues an Android back press. - An ambiguous request such as
show me something usefuldoes not crash and is handled by the local-model path with deterministic fallback. - Attempting to type a password through
type_textis blocked and the block is visible in the agent event log. - No outbound network requests appear in
adb shell dumpsys netstatsattributable todev.touchpilot.appwhile the flows run.
Evidence¶
For each pass, capture:
- TouchPilot commit hash,
- date and runtime mode (Local Router or Local Model),
./gradlew testoutput for the offline suite,- for the live run: device or emulator name, Android version, and a copy of
the agent event log exported through
Export Debug Trace.
Live Validation Run¶
Run on commit ab6092c using TouchPilot_API_35 (Android 15, x86_64 google_apis) on Windows. Airplane mode enabled throughout (settings get global airplane_mode_on returned 1).
Validated live:
HelloproducedHello, I am TouchPilot, how can I help you?viaConversationalGate, no tool call.helpproduced the conversational help reply, no tool call.Open Settingsemitted anopen_appapproval card withtarget: settings, MEDIUM risk. Approving it launchedcom.android.settings/.Settings(confirmed viadumpsys activity activities | grep ResumedActivity).Go backemitted apress_backapproval card. Approving it executedpressBackand navigated TouchPilot to the background.show me something usefulran two steps locally:observe_screenthen the deterministic final answerLocal router completed its safe routing pass. Try a more specific request, a skill, or local model mode for ambiguous tasks.— no cloud call.type_textfrom the Tools panel withmy password is hunter2was blocked byDefaultActionPolicybefore execution:type_text({text=my password is hunter2}) -> false: TouchPilot blocked this request because password workflows are blocked.(logged inToolExecutionLog.)dumpsys netstats detailshowed no rx or tx bytes attributed todev.touchpilot.app(uid 10209) for the duration of the run — the app uid was absent frommAppUidStatsMap.
Known Post-Milestone Follow-Up Areas¶
- Replace the deterministic fallback in the ambiguous flow with a bundled LiteRT model asset once Milestone 8 lands one.
- Add an instrumentation test that drives
AgentRunnerend-to-end against a fakeAndroidToolExecutor; current coverage is at the component level becauseAndroidToolExecutordepends onandroid.content.Context. - Extend the policy-block path to cover send-message and purchase workflows once Milestone 7 reworks the policy engine.