feat: Phase 3 recipes file, spotify_play, and locked-screen refusal
Phase 3 of AGENTS.md section 11.
Recipes file (FR-5)
- recipes/ holds one hand-editable file per task; recipes/spotify_play.recipe
is the first one.
- internal/recipe parses directives, guards, parameters and steps, and runs
them with a per-step and a whole-run timeout (FR-11).
- Steps name text ("tap the element whose text is Search"), never coordinates.
- guards name screens Artemis must never tap on, so a signed-out or
first-run Spotify fails with its own reason (FR-8).
- Every run appends task, params, steps, result and duration to
logs/runs.jsonl (FR-10).
spotify_play
- launch, search, play, then wait for the transport button to say "Pause".
- internal/ui/rows.go adds tap_row: a result row is a clickable container with
a title and a subtitle, so a one-line search suggestion is not a result, and
the tap lands on the row's centre rather than on its "More options" button.
This fixes a real wrong tap: a plain text tap hit a 126x12 pixel
"More options for song Honky Tonk Women - Live" button.
Locked screen (AGENTS.md section 13 Q2, decided)
- Artemis fails loudly on a secure keyguard and never types a PIN. No secret is
stored. A non-secure keyguard is still dismissed.
- internal/ui/keyguard.go reads the KeyguardServiceDelegate block plus
cmd lock_settings is-user-secure.
- Every acting command wakes the screen before it checks the keyguard: a dozing
phone reports showing=false, because the lock screen is not drawn yet.
- AGENTS.md records the decision and that storing a PIN for auto-unlock is
rejected for now.
Verified live: the phone locked itself mid-session, and keyguard, tap and
run spotify_play all refused with "phone is locked; unlock it and retry".
The live end-to-end playback run still needs an unlocked phone.
This commit is contained in:
81
internal/ui/keyguard_test.go
Normal file
81
internal/ui/keyguard_test.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package ui
|
||||
|
||||
import "testing"
|
||||
|
||||
// A real (trimmed) KeyguardServiceDelegate block from `dumpsys window`.
|
||||
const keyguardLockedDump = `WINDOW MANAGER POLICY STATE (dumpsys window policy)
|
||||
mSafeMode=false mSystemReady=true
|
||||
mKeyguardOccluded=false mPendingKeyguardOccluded=false
|
||||
KeyguardServiceDelegate
|
||||
showing=true
|
||||
inputRestricted=true
|
||||
occluded=false
|
||||
trusted=false
|
||||
simSecure=false
|
||||
dreaming=false
|
||||
systemReady=true
|
||||
deviceHasKeyguard=true
|
||||
enabled=true
|
||||
userId=0
|
||||
bootCompleted=true
|
||||
screenState=SCREEN_STATE_ON
|
||||
interactiveState=INTERACTIVE_STATE_AWAKE
|
||||
Looper state:
|
||||
Looper (android.ui, tid 22) {4c5c23e}
|
||||
`
|
||||
|
||||
const keyguardUnlockedDump = `WINDOW MANAGER POLICY STATE (dumpsys window policy)
|
||||
mKeyguardOccluded=false mPendingKeyguardOccluded=false
|
||||
KeyguardServiceDelegate
|
||||
showing=false
|
||||
inputRestricted=false
|
||||
occluded=false
|
||||
trusted=false
|
||||
simSecure=false
|
||||
dreaming=false
|
||||
systemReady=true
|
||||
deviceHasKeyguard=true
|
||||
enabled=true
|
||||
userId=0
|
||||
bootCompleted=true
|
||||
screenState=SCREEN_STATE_ON
|
||||
interactiveState=INTERACTIVE_STATE_AWAKE
|
||||
Looper state:
|
||||
Looper (android.ui, tid 22) {4c5c23e}
|
||||
`
|
||||
|
||||
func TestKeyguardShowingParsesTheDelegateBlock(t *testing.T) {
|
||||
showing, err := keyguardShowing(keyguardLockedDump)
|
||||
if err != nil {
|
||||
t.Fatalf("keyguardShowing: %v", err)
|
||||
}
|
||||
if !showing {
|
||||
t.Fatal("want showing=true")
|
||||
}
|
||||
|
||||
showing, err = keyguardShowing(keyguardUnlockedDump)
|
||||
if err != nil {
|
||||
t.Fatalf("keyguardShowing: %v", err)
|
||||
}
|
||||
if showing {
|
||||
t.Fatal("want showing=false")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyguardShowingRejectsAnUnparsableDump(t *testing.T) {
|
||||
// Acting on a phone whose lock state is unknown is not allowed.
|
||||
if _, err := keyguardShowing("dumpsys window: service not found\n"); err == nil {
|
||||
t.Fatal("expected an error when the delegate block is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestKeyguardShowingReadsIsKeyguardShowing(t *testing.T) {
|
||||
dump := " KeyguardServiceDelegate\n showing=false\n mDisableSecureWindows=false\n isKeyguardShowing=true\n"
|
||||
showing, err := keyguardShowing(dump)
|
||||
if err != nil {
|
||||
t.Fatalf("keyguardShowing: %v", err)
|
||||
}
|
||||
if !showing {
|
||||
t.Fatal("want showing=true from isKeyguardShowing")
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user