A scheduled recording now makes sure the computer is awake for it - #1400
Merged
Conversation
…ke for it Reported by a listener, and invisible from inside the app: he scheduled a football pregame for 11:00 and Quill Radio announced the recording at 11:03. Nothing failed. Nothing was logged. He lost the first three minutes of the show and had no way to find out why. `RecordingScheduler` polls every twenty seconds, so twenty seconds is the whole budget -- minutes mean the machine was asleep. A schedule is a thread inside a running app, and a sleeping computer runs no threads: nobody asks the schedule anything until it wakes, and the window model then fires the entry immediately, which from the outside is indistinguishable from the app losing track of time. Keep-awake was scoped to `(playing or recording)`, covering every moment except the quiet stretch before a scheduled recording -- which is exactly when a computer with nothing to do decides to sleep. Three defences, weakest to strongest, meant to work together: **It says so.** The scheduling window states the requirement in one line before anything is set: Quill Radio must be running, and the tray counts. A requirement you discover by losing the start of a game is not a requirement, it is a trap. **It holds standby off as the moment approaches.** `schedule_wake.is_imminent` answers the question the sleep inhibitor now asks alongside playback and recording, five minutes ahead. Re-evaluated on the existing one-minute tick, because "a recording is now five minutes away" is not a playback state change and would otherwise never reach the inhibitor. **And it can wake the machine.** Nothing inside a sleeping process can rouse it; only the OS can. `recording_wake_task` registers a one-shot per-user Task Scheduler entry with `WakeToRun` two minutes ahead, which then starts Quill Radio if it is not already running. Registered from XML because `schtasks /Create` has no switch for that flag, with `DisallowStartIfOnBatteries` off so a laptop still records. Re-registered whenever the schedule changes and at launch (a one-shot task is spent once it fires), removed when nothing is scheduled, so Task Scheduler never fills with dead entries. Two `RadioHistory` flags, both default true and separate on purpose: inhibiting standby is local and permission-free, while waking the machine changes how the computer behaves, and one switch could not express wanting only the first. Everything is best effort and never raises -- a machine whose policy blocks `schtasks` keeps the other two defences and sees no error. The pure arithmetic is tested against an injected clock, including the exact shape of the report: at 10:58 an 11:00 recording is imminent, at 10:50 it is not. GATE-11 forced two extractions rather than a rebaseline: `apps/radio_preferences.py` (the whole Preferences dialog, out of the at-ceiling app shell) and this logic into its own module rather than the at-ceiling `recording_schedule.py`. Already true and now documented: a late start does not run late at the other end -- `remaining_minutes` records the time *left* in the window -- which is why a late start costs the beginning, and why a cushion means starting earlier *and* lengthening by the same amount. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…file
`test_radio_favorites_manual_order` isolates itself by patching
`quill.core.paths.app_data_dir`. `main_frame_radio` does
`from quill.core.paths import app_data_dir` at module scope, so it holds its own
binding that the patch never touches -- and `_save_radio_favorites` resolves the
name from *there*. The test therefore wrote its three fixture stations ("Zeta",
"Alpha", "Mu", at example.com) straight into a real `radio_favorites.json`,
replacing seven actual stations. The suite passed every time it did so; backups
show it went unnoticed for about two weeks, and it surfaced only because
somebody looked at their favorites and asked what "Zeta" was.
Two fixes, because the test is the symptom and the missing guard is the cause.
The test now patches the binding that is actually read.
And `tests/conftest.py` refuses the real profile outright: any write beneath the
directory the app would use raises, naming the path and pointing at the
`quill_data_dir` fixture. `_DEV_BUILD` plus `QUILL_DATA_DIR` already isolates a
test that remembers to ask; this catches the test that *thinks* it did, which is
the only kind that can lose somebody's data quietly. Verified by probing a write
to the real profile and watching it fail with no file created, then by a full
suite run to confirm no legitimate test was relying on writing there.
Also here: `meeting*.md` join `john.md` in the scratch-file ignore block, so the
repo-layout gate stops failing on working notes at the repository root.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
A listener scheduled a football pregame for 11:00 and Quill Radio announced the recording at 11:03. Nothing failed, nothing was logged, and he lost the first three minutes with no way to find out why.
RecordingSchedulerpolls every 20 seconds, so 20 seconds is the entire budget — minutes mean the machine slept. A schedule is a thread inside a running app, and a sleeping computer runs no threads. Keep-awake was scoped to(playing or recording), covering every moment except the quiet stretch before a scheduled recording — exactly when an idle computer decides to sleep.Three defences, weakest to strongest
schedule_wake.is_imminent, five minutes ahead, consulted by the sleep inhibitor alongside playback and recording — re-evaluated on the existing one-minute tick, because "a recording is now five minutes away" is not a playback state change and would never otherwise reach it.recording_wake_taskregisters a one-shot per-user Task Scheduler entry withWakeToRuntwo minutes ahead, then starts Quill Radio if it is not running. From XML, becauseschtasks /Createhas no switch for that flag;DisallowStartIfOnBatteriesoff so a laptop still records; re-registered on every schedule change and at launch, removed when nothing is scheduled.Two
RadioHistoryflags, both default true and deliberately separate: inhibiting standby is local and permission-free, waking the machine changes how the computer behaves. Best effort throughout — a machine whose policy blocksschtaskskeeps the other two defences and sees no error.GATE-11 forced two extractions rather than a rebaseline:
apps/radio_preferences.py(the whole Preferences dialog out of the at-ceiling app shell) and the new logic into its own module rather than the at-ceilingrecording_schedule.py.Second commit: a test was writing into a real user profile
test_radio_favorites_manual_orderpatchesquill.core.paths.app_data_dir.main_frame_radioimported the name at module scope, so it holds its own binding the patch never touches — and_save_radio_favoritesreads that one. The test wrote its fixture stations ("Zeta", "Alpha", "Mu" at example.com) into a realradio_favorites.json, replacing seven actual stations. The suite passed every time; backups show ~two weeks of this, found only because somebody looked at their favorites and asked what "Zeta" was.The test now patches the binding that is read — and
tests/conftest.pyrefuses the real profile outright, so any write beneath it raises and names the path._DEV_BUILD+QUILL_DATA_DIRisolates a test that remembers to ask; this catches the test that thinks it did, which is the only kind that loses data quietly. Verified by probing a real-profile write (blocked, no file created) and by a full suite run.Verification
-n 4 --dist=loadfile) green; the two failures it surfaced are fixed here.🤖 Generated with Claude Code