feat: Phase 4 — spotify_radio, and a bound on logs/runs.jsonl

spotify_radio opens a track's own context menu and starts its song radio.
The labels came from a fresh uiautomator dump on the phone, not from a guess:
the entry is "Go to song radio" and it sits below the fold, so the recipe
scrolls before it matches.

Recipe format:
- menu_row "{query}" finds the row like tap_row, then presses the action
  button inside that row's own subtree (ui.Row.Action), so the More options
  button of any other row is never a candidate. It records the row as {row}.
- a step can name its own failure: wait ... else "reason". The named reason
  comes first and the screen's labels still follow (FR-8).
- scroll up|down moves the current list.
- playing with no title waits for the media session to be PLAYING. A radio
  changes track every few minutes, so a title match would fail on a working
  radio; spotify_play still passes the title it wants.

ui:
- pickBestMatch ignores a match with no usable bounds. An element scrolled out
  of its list reports [0,0][0,0], and tapping its "centre" would be a blind tap
  in the corner. A wait now only succeeds once the element is on screen.

Run log (FR-10): appendRunLog rotates runs.jsonl when a write would pass
ARTEMIS_LOG_MAX_BYTES (default 1 MiB), keeping ARTEMIS_LOG_KEEP (default 5)
files. History is bounded, never deleted wholesale, and the per-entry cap of
6 screen labels of 60 characters is unchanged.

Live evidence in docs/phase-4-song-radio.md: ok:true in 56.6s, the menu that
opened belonged to the pressed row, the station name "Honky Tonk Women Radio"
on the phone, and the radio playing past its seed track (Layla) per the media
session and dumpsys audio. Both new failure modes were demonstrated failing
with distinct reasons. The mini-bar's transport label stays "Play" while
playing on this build, which is why playback is confirmed from the media
session.

Co-Authored-By: Paperclip <noreply@paperclip.ing>
This commit is contained in:
2026-10-16 15:25:50 +11:00
parent 40a8a51c9e
commit 36b4dd2391
11 changed files with 1116 additions and 34 deletions

View File

@@ -0,0 +1,97 @@
# spotify_radio — start a song radio in Spotify from a search result.
#
# Edit this file by hand. Steps name the text Artemis must find on screen; it
# taps the centre of the element it matched. There are no coordinates here on
# purpose, so this recipe survives a Spotify redesign (FR-4).
#
# Run it with:
# artemis run spotify_radio query="Rolling Stones Honky Tonk"
task: spotify_radio
description: Start a song radio in Spotify from a search result.
app: com.spotify.music
# The caller must supply these.
param: query
# Spotify's list rows: title + subtitle inside one clickable container. Naming
# the container keeps the row itself the target, not the row's buttons.
row_id: com.spotify.music:id/row_root
step_timeout: 20s
total_timeout: 420s
# Screens Artemis must never tap on. Each one fails the run with the reason, so
# a wrong tap is impossible (FR-8).
guard "Log in" => Spotify is signed out; sign in on the phone and retry
guard "Sign up free" => Spotify is signed out; sign in on the phone and retry
guard "Log in to Spotify" => Spotify is signed out; sign in on the phone and retry
guard "Continue with Facebook" => Spotify is showing its welcome screen; sign in on the phone and retry
guard "Continue with Google" => Spotify is showing its welcome screen; sign in on the phone and retry
guard "Update Spotify" => Spotify wants an update; update it on the phone and retry
guard "What's new" => Spotify is showing an announcement screen; dismiss it on the phone and retry
steps:
# 1. Make sure Spotify is up and really in front before any text is trusted.
- launch
- foreground 30s
- wait "Search" 30s retry 2
# 2. Search for the track. Identical to spotify_play: Spotify remembers the
# last tab, so a search screen with the old query can come back.
- tap "Search"
- tap_if "Clear search query"
- wait "What do you want to listen to?" 30s retry 3
- tap "What do you want to listen to?"
- type "{query}"
- wait "Clear search query" 30s
# 3. Wait for the results list to replace "Recents", then let it settle.
- wait_gone "Recents" 30s
- sleep 1s
# 4. Open the More options menu of the row that matches the query. menu_row
# looks for the button INSIDE the row it chose, so the More options button
# of any other row is never a candidate. It records the row's title as
# {row} for the two steps below.
- menu_row "{query}" 40s
# 5. The context menu is a new screen. When it is open, the dump holds only
# the menu, and the menu's header names the track it belongs to. So this
# step is the wrong-row check: if the menu belongs to a different track,
# its header is a different name and the run fails with a reason that names
# the real problem. It must match exactly — "Honky Tonk Women" is not a
# match for the "Honky Tonk Women - Mono Version" row.
- wait exact "{row}" 20s else "the context menu opened on the wrong row"
# 6. "Go to song radio" sits below the fold. Spotify reports an option it has
# not drawn yet with bounds [0,0][0,0], and a wait only accepts an element
# that is really on screen, so this needs a scroll first. "retry 3" runs
# the scroll again up to three times if one swipe was not enough.
#
# A row with no radio never shows the label at all, and fails with its own
# reason, which names the screen's visible labels (FR-8).
- scroll down
- wait exact "Go to song radio" 15s retry 3 else "this track offers no Song radio"
- tap exact "Go to song radio"
# 7. The radio page names the station ("Honky Tonk Women Radio"). That name,
# and not the track, is what proves a radio opened rather than the single
# song. The page is still paused at this point.
- wait "Radio" 30s else "the song radio page did not open"
# 8. Start it. The button reads "Play playlist" only while the radio is not
# playing, so tap_if is right here: on a radio that is already playing it
# is absent, and the step is a no-op.
- tap_if "Play playlist"
# 9. Confirm playback by asking Android, not by reading the screen. Spotify's
# transport button keeps the label "Play" while music is playing, so an
# on-screen label check fails on a phone that is playing perfectly. The
# media session is the authoritative answer, and it carries the title.
#
# No title is given here on purpose: a radio moves on to a different track
# every few minutes, so "playing <seed track>" would fail on a radio that
# is working exactly as it should. The station name in step 7 is what
# proves the radio, and this step proves it is playing.
- playing 30s retry 3