Final QA: end-to-end operational dry run #7
Labels
No labels
area/ai
area/backend
area/frontend
area/infra
area/scheduler
area/wled
good-first-issue
priority/high
priority/low
priority/medium
type/bug
type/chore
type/ci-cd
type/docs
type/feature
type/qa
v1.0.0
v1.1.0
v1.2.0
v2.0.0
No project
No assignees
1 participant
Notifications
Due date
No due date set.
Dependencies
No dependencies set
Reference
rbrooks/Iris-WLED#7
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Goal
Exercise the full operational loop end-to-end against a real (or mock) WLED device before tagging v1.0.0.
Flow to validate
Acceptance criteria
Part of the M10 final QA pass.
Dev host is deployed and ready — but needs configuring before this dry run can start
Carried over from #65, which delivered the deploy mechanism but stopped at configuration.
Current state of
claude@10.1.1.14Running the CI-published image (
sha256:f4d5c6cb…), checkout current withmain, database migrated forward from May (0006 → 0008, no errors) with 97 events and 169 schemes intact. Pre-redeploy backup atdata/iris.db.pre-redeploy-20260831-053637,md5-verified identical.Deploy/redeploy from here is:
What must be set before the dry run is meaningful
The instance is up but inert — no scheduler jobs are registered and no controller is configured:
Row counts confirm it was never configured past event/scheme generation:
So before this issue's steps can run:
LATITUDE/LONGITUDEin.env(required forSCHEDULE_MODE=solar; without them no on/dim/off jobs are ever scheduled)WLED_HOSTpointing at the controllerTZis correct (currently set)SESSION_SECRETwas generated during the #65 deploy, so that no longer blocks startup.These are values only you can supply — I would be guessing at coordinates and a controller address. Once they are in, the deploy command above picks them up on restart and this dry run can proceed against real hardware.
Note for whoever runs it
This is also the first time the priority resolver, default-scheme, fallback-policy and quick-push-revert fixes (#43/#44/#45/#47) will be exercised against a real controller — they were unit-tested but never run end to end on hardware. Worth watching those paths specifically rather than only the happy path.
Dev deployment is now configured against real hardware.
Values were supplied privately and written to the dev host's
.env(gitignored); they are deliberately not repeated here, since this repo is public and the coordinates are a home location. A timestamped.envbackup was taken first.Controller —
GET /api/v1/wled/status:The device ("Niji", ESP32, WLED 16.0.0) is a 36-LED RGBW strip presenting a single segment
0–36. It's a bench unit rather than a full install, which is fine for exercising the loop.Segment config — there were none, so schemes had nothing to target. Created one themed segment
strip→wled_index 0. The name is arbitrary (the generator uses whatever the configured segments are called), so rename it when the real layout exists — note that renaming triggers scheme regeneration for the affected segments.Solar scheduling now resolves —
GET /api/v1/schedule/status:That checks out for the configured latitude/longitude: sunset ≈ 19:33 CDT with the
-30offset, sunrise ≈ 06:32 CDT.One thing worth flagging: the dev host was running a
:mainimage built 2026-08-31T05:29 — roughly 21 hours behind, predating #93./events/resolvedreturned{"detail": "Event not found"}, i.e. the route-shadowing bug #93 fixed. That was a stale deploy, not a regression:docs/setup.md§6 already prescribespullbeforeup -d, and my first recreate simply skipped the pull. After pulling (image now built 01:06 today, post-#97) the endpoint returns resolved days correctly.Remaining before the acceptance criteria can be ticked:
Correction to my previous comment: I described the controller as "a bench unit rather than a full install". That's wrong — it is the live roofline. I inferred "bench" from the 36-LED count, which was an assumption, not an observation. The dry run below was run against live lights and treated as such: state was snapshotted first and restored at the end.
Dry run results
Leg 1 — configure → review → approve ✅
Approved
labor_day_2026_mid. The scheme carries thestripsegment created earlier, confirming the generator picks up configured segments.One observation:
sourcestayedbuiltinafter approval rather than flipping touser. Promotion touserappears to apply to community/AI-sourced schemes only. Flagging in case §3.3's "promoted to user-saved on approval" was meant to cover builtins too — not treating it as a defect here.Leg 2 — push ✅
POST /schedule/push-today{"pushed": "off", "reason": "no_event"}→ lights off. Correct: 2026-09-01 has no event anddefault_schemewas unset.POST /wled/quick-push {scheme_id}fx 9,col [[210,30,40],[255,255,255],[25,70,160]]— the approved Labor Day scheme on the roof.POST /wled/quick-push {color:[0,255,0]}fx 0,pal 0,col [[0,255,0],[0,0,0],[0,0,0]]— #97's arbitrary-colour path, verified against real hardware.DELETE /wled/quick-pushWorth noting:
labor_day_2026_midcarries four colours (…,[255,180,40]gold) but WLED takes three slots, andscheme_to_wled_statedoescolors[:3], so the gold is silently dropped. Correct per the WLED protocol, but the generator can emit schemes whose 4th colour can never render. Possibly worth either capping generation at 3 or surfacing it in the UI.Leg 3 — scheduler on / dim / off ✅
Driven directly rather than waiting for real sunset, with
default_schemetemporarily set todim_whiteso the transitions were observable (restored toNoneafterwards):run_on_jobon,bri 102, warm white[255,180,107]— 40% perlate_night_brightness, matchingdim_white's documented behaviourrun_dim_jobbri 40— 40% of 102, correctrun_off_jobon: falseMethodology note: my first attempt called these via
docker exec … python -cand gotWLED controller not configured. That was my harness, not a defect —_controlleris a process-global set by the app's lifespan, and a fresh process has none. Re-running withinit_controller()first gave the results above.Leg 4 — failure paths ✅
Against unroutable
192.0.2.1(TEST-NET-1):silentsuccess=False attempts=1, logged, no follow-uplast_known_goodsuccess=False attempts=1, reconnect watch requestedretrysuccess=False attempts=1, retry chain requestedThen the same failure driven through the live app (so a real APScheduler was present):
POST /schedule/push-todayreturnedtimed out, andwled_push_retryappeared in the persistentapscheduler_jobstable scheduled for +5 minutes — the spaced-retry leg confirmed end to end, surviving in the jobstore as §5.3 describes. Host restored immediately;reachable: true.Acceptance criteria
Open items
default_scheme = None(restored to its original value), every no-event evening pushes off at sunset−30. 2026 has 83 schemes but 0 approved besides Labor Day, so most nights currently resolve to "off". That's a real behavioural decision — setdefault_scheme, or approve schemes for current dates.useron approval.communitytouser; AI and hand-edited schemes keep their original source #99The unattended overnight cycle ran last night and did not pass. Found a real bug: #106.
What happened on the dev deployment between 2026-08-31 evening and 2026-09-01 morning:
on_job(push today's scheme)dim_job23:00 CDTreschedule_daily00:01 CDTdaily_backup04:00 CDTiris_daily_20260901T090000Z.dboff_job06:31 CDTschedule-logrowThe lights were off when I checked, which nearly read as success. They had been switched off by a Home Assistant automation the user had forgotten was still enabled. Without that coincidence the roofline would have stayed lit all day, and the schedule log would still have looked healthy — the missing job leaves no row, and every job that did run reported success.
Root cause in #106:
reschedule_dailyruns at 00:01 local and rewrites the singleoff_jobslot to the next morning's sunrise, cancelling the one pending ~6.5 hours away.off_jobis the only one of the three whose correct time falls on a different calendar day, which is why only it collides.Acceptance criteria
This issue should stay open until #106 lands and one real night has been observed end to end with the Home Assistant automation disabled, so nothing external can mask the result again.
Worth recording why this was nearly missed: I had already confirmed
run_off_job()works when invoked directly, so the leg looked proven. Driving a job by hand tests the job; it does not test that the scheduler will ever call it.Moved out of v1.0.0 to v1.1.0 — this is an observation to run tomorrow morning, not a blocker on cutting the release.
That is a reasonable call because the mechanism is already verified; what remains is confirmation under ordinary conditions:
dim_jobfired unattended on the night of 2026-08-31off_joblogged aschedule_logrow and switched the controller off, with the pending-off condition genuinely reproduced rather than simulatedSo the one unticked criterion — "the loop runs unattended" — is now about watching an ordinary night end to end, not about discovering whether it works. The residual risk that a full 19:01 → 23:00 → 00:01 → 06:32 cycle behaves differently from the reproduced one is small.
What to check tomorrow
Tonight's setup is already in place:
late_summer_moon_2026extended through 2026-09-01, its approved scheme resolving for tonight,on_job19:01 →dim_job23:00 →reschedule_daily00:01 →off_job06:32. The Home Assistant automation that masked the original failure is disabled, so anoff_jobrow tomorrow is attributable to Iris alone.One caveat if this slips. The confirming observation is only meaningful while the dev deployment keeps running an image with #106 in it and the HA automation stays off. If either changes before the check happens, the result stops being evidence — worth re-establishing both rather than trusting a stale setup.
Confirmed — three consecutive unattended cycles
Checked on 2026-09-04. The dev container has been up since the #106 deploy (
started 2026-09-01T17:07Z, image built16:51Z,get_active_timespresent), so all three nights ran on the fixed build with no intervention.Every
off_jobcrossed a 00:01reschedule_dailyand survived — the failure in #106 reproduced three more times over, and didn't.The solar drift is right too, which is a useful independent check that the times are being recomputed rather than cached: sunset−30 moves earlier each evening (19:01 → 19:00 → 18:58) and sunrise later each morning (06:32 → 06:33 → 06:34).
Which night actually satisfies the criterion
Being precise, because it matters: only the first cycle had the lights genuinely lit. 2026-09-01 was covered by
late_summer_moon_2026, soon_jobpushed the approved scheme (global_brightness: 128),dim_jobtook it to 51, andoff_jobswitched it off at sunrise. That is the full configure→approve→push→schedule→sunrise-off loop, unattended, end to end.2026-09-02 and 09-03 are no-event days with
default_schemeunset, soon_jobcorrectly pushed off and the lights never came on. Those two nights confirm the scheduler keeps firing and rescheduling correctly across days, but they are not lit cycles.The Home Assistant automation stayed disabled throughout, so nothing external produced these results.
Acceptance criteria
Flow
One defect found while checking
Brightness has been decaying across the quiet nights: 128 → 51 → 20 → 8.
run_dim_jobdims relative to the live value with no check that the lights are on, so on consecutive no-event days each night's dim compounds on the last. Filed as #110 — not a scheduler fault, and invisible while the lights are off, but it would surprise anyone who turned them on by hand mid-week.Closing.