fix(qa): harness reliability — 4 gate-fidelity + #623 recovery fixes - #966
Conversation
…ry fixes The QA measurement harness was the meta-blocker: noisy/flaky signals that conflate harness failures with product quality. Four root-caused, TARGETED fixes (each preserves its integrity gate — risk-mitigations enforced + tested): 1. party_traveled false-cap (assert_behavioral.py): a COMPLETE single-location drama (clock advanced AND a quest resolved AND >=8 beats) no longer RED-caps every lens <=2.5 like a frozen stall. Conjunctive in-place-progression exception; a frozen opening (day==1/morning, no quest) still fails >=2 conjuncts and stays RED. Additive — can only let a run PASS. 2. persona rc!=0 mis-counted as a quality fail (sweep_v2.sh + ui_playtest_app.sh + release_readiness.py): a NON-quota player-process CRASH (player_rc!=0) is now retried once, then stamped part_b.harness_error → surfaced as a cross_persona_sat EVIDENCE GAP (harness_contaminated, still blocks release as "re-measure"), NOT a score_pass quality fail. Discriminator is the process exit, never the score — a played low-score run exits rc=0 and still fails the quality gate. 3. Mac-handoff PART_A_DEADLINE drift (ui_playtest_app.sh): the flat 420s mint deadline had drifted BELOW the DM cold-open's own 500/550s timeout → healthy-but-slow mints abandoned (flaky scripted/codex legs). Now derived from the cold-open tier + 90s margin, with a liveness-bounded grace (extends only while dm.combined.jsonl streams / the DM proc is alive; a dead cold-open fails promptly). can_act:true+minted_run PASS condition unchanged. 4. #623 silent-drop recycle + hang escalation (dm_beat_mark.py + lib_beat_driver.sh + play*.sh): (a) an empty pre-beat mark on a CONTINUING beat (first==0, a mark-write bug) no longer fails-open and stamps recycled prose "genuine" (masked dead beat); a true first-prose cold open (first==1) is unaffected. (b) a deadline-killed (rc=124) ROUTINE beat no longer escalates the retry to the cold-open tier (the ~14-15min single-beat hang) — it routes to the visible failure path; cold opens + non-124 transient errors still retry. Tests: assert_behavioral 38, release_readiness 47, dm_beat_mark 8 (new file), fast_gate 222 — all green.
|
Caution Review failedThe pull request is closed. ℹ️ Recent review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Plus Run ID: 📒 Files selected for processing (2)
📝 WalkthroughWalkthroughThe PR hardens the QA automation pipeline across four areas: (1) adds a ChangesQA Harness Hardening
Sequence Diagram(s)sequenceDiagram
participant sweep as sweep_v2.sh run_persona
participant playtest as ui_playtest_app.sh
participant release as release_readiness.py
sweep->>playtest: run Part A + Part B
playtest->>playtest: coldopen_is_live() liveness gate (Part A)
playtest->>playtest: player_rc != 0 AND no quota/429 text?
alt harness error (non-quota)
playtest->>playtest: PART_B_HARNESS_ERROR=true
playtest->>sweep: write run.json {part_b: {harness_error: true}}
sweep->>playtest: retry once (non-quota path)
playtest->>sweep: rc (after retry)
else quota / session limit
playtest->>sweep: short-circuit to quota path (no retry)
end
sweep->>release: aggregate run.json results
release->>release: part_b_harness_error → exclude from score_pass_failed_personas
release->>release: add to cross_persona_sat evidence_gaps
release->>release: exclude from arc_completed failed_part_b
release-->>sweep: harness_contaminated=true, harness_error_personas=[...]
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related issues
Possibly related PRs
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
qa/ui_playtest_app.sh (1)
1119-1121: 💤 Low valueMinor:
-rflag is unnecessary for named files.
grep -ris for recursive directory search. When targeting specific files ("$RUNDIR/backend.log" "$PLAYERDIR/player.err"), the-rflag has no effect but adds slight confusion. Consider using just-qiE.This doesn't affect correctness since grep handles it gracefully.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@qa/ui_playtest_app.sh` around lines 1119 - 1121, The grep command on lines 1119-1121 includes the `-r` flag, which is unnecessary when searching specific named files like "$RUNDIR/backend.log" and "$PLAYERDIR/player.err". The `-r` flag is only needed for recursive directory searches and adds unnecessary confusion here. Remove the `-r` flag from the grep invocation, keeping only the `-qiE` flags with the specific file paths.qa/test_assert_behavioral.py (1)
698-736: 💤 Low valueConsider adding test for clock-not-advanced with arc-resolved.
The four tests thoroughly cover the happy path and three failure modes. One edge case not explicitly tested:
day=1,tod="morning"(clock NOT advanced) withquest_completed=True(arc resolved). This would confirm thatclock_advancedis independently required, not just correlated witharc_resolved.Since the implementation uses a strict AND, the current tests provide adequate coverage—but an explicit test would guard against future refactors that might accidentally broaden the exception to arc-only.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@qa/test_assert_behavioral.py` around lines 698 - 736, Add a new test function after test_party_traveled_still_red_when_arc_resolved_but_too_few_beats that explicitly tests the case where the clock has not advanced (day=1, tod="morning") but the arc is resolved (quest_completed=True). This test should verify that party_traveled fails because clock_advanced is independently required by the AND condition, even when arc_resolved is true. Create test events with beats >= 8, set up state with day=1, tod="morning", quest_completed=True, and visited_count=1, then assert that the output contains "[FAIL] party_traveled" and the return code is 1, confirming that both clock advancement and arc resolution are strictly required by the exception logic.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@qa/vm/sweep_v2.sh`:
- Around line 123-124: The retry gating condition at line 123 in the sweep_v2.sh
file uses the quota_tripped function with only the backend log (_bl variable) to
detect quota-related failures. However, the upstream classifier in
qa/ui_playtest_app.sh checks for quota/429 errors across both backend.log and
player/player.err files. Update the quota_tripped call to align with the
upstream behavior by including both log sources in the quota detection logic,
ensuring that quota-triggered aborts are correctly identified and do not consume
the single retry allocation.
---
Nitpick comments:
In `@qa/test_assert_behavioral.py`:
- Around line 698-736: Add a new test function after
test_party_traveled_still_red_when_arc_resolved_but_too_few_beats that
explicitly tests the case where the clock has not advanced (day=1,
tod="morning") but the arc is resolved (quest_completed=True). This test should
verify that party_traveled fails because clock_advanced is independently
required by the AND condition, even when arc_resolved is true. Create test
events with beats >= 8, set up state with day=1, tod="morning",
quest_completed=True, and visited_count=1, then assert that the output contains
"[FAIL] party_traveled" and the return code is 1, confirming that both clock
advancement and arc resolution are strictly required by the exception logic.
In `@qa/ui_playtest_app.sh`:
- Around line 1119-1121: The grep command on lines 1119-1121 includes the `-r`
flag, which is unnecessary when searching specific named files like
"$RUNDIR/backend.log" and "$PLAYERDIR/player.err". The `-r` flag is only needed
for recursive directory searches and adds unnecessary confusion here. Remove the
`-r` flag from the grep invocation, keeping only the `-qiE` flags with the
specific file paths.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Organization UI
Review profile: CHILL
Plan: Pro Plus
Run ID: 9c1b3513-a2e6-43a9-8de2-5d4be760b9c2
📒 Files selected for processing (11)
qa/assert_behavioral.pyqa/dm_beat_mark.pyqa/lib_beat_driver.shqa/release_readiness.pyqa/test_assert_behavioral.pyqa/test_dm_beat_mark.pyqa/test_release_readiness.pyqa/ui_playtest_app.shqa/vm/sweep_v2.shscripts/play.shscripts/play_party.sh
| if [ "$rc" -ne 0 ] && ! quota_tripped "$_bl"; then | ||
| note " $persona rc=$rc (non-quota harness crash) — retrying ONCE on a clean store" |
There was a problem hiding this comment.
Align retry quota detection with the upstream non-quota predicate.
At Line 123, retry gating checks only backend.log. The upstream Part B classifier (qa/ui_playtest_app.sh) treats quota/429 based on both backend.log and player/player.err; this mismatch can consume the one retry on quota-aborted runs.
Suggested fix
-quota_tripped(){ # $1 = a run dir or a log path; rc 0 if a session-limit/429 is present
- grep -qriE "session limit|HTTP 429|hit your (session|usage) limit" "$1" 2>/dev/null
+quota_tripped(){ # $@ = run dir(s)/log path(s); rc 0 if a session-limit/429 is present
+ grep -qriE "session limit|HTTP 429|hit your (session|usage) limit" "$@" 2>/dev/null
}
@@
- local _bl="qa/ui_playtest_runs/vm2-$persona/backend.log"
- if [ "$rc" -ne 0 ] && ! quota_tripped "$_bl"; then
+ local _bl="qa/ui_playtest_runs/vm2-$persona/backend.log"
+ local _pe="qa/ui_playtest_runs/vm2-$persona/player/player.err"
+ if [ "$rc" -ne 0 ] && ! quota_tripped "$_bl" "$_pe"; then🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
In `@qa/vm/sweep_v2.sh` around lines 123 - 124, The retry gating condition at line
123 in the sweep_v2.sh file uses the quota_tripped function with only the
backend log (_bl variable) to detect quota-related failures. However, the
upstream classifier in qa/ui_playtest_app.sh checks for quota/429 errors across
both backend.log and player/player.err files. Update the quota_tripped call to
align with the upstream behavior by including both log sources in the quota
detection logic, ensuring that quota-triggered aborts are correctly identified
and do not consume the single retry allocation.
…tatus-blind tool count
Adversarial verification of FIX 4 found a real gate-weakening: arc_resolved used
coverage_from_tool_counts' quest_resolved bucket, which counts the set_quest_status TOOL NAME
regardless of the status argument. A frozen DM could pass party_traveled (and dodge the
behavioral RED-cap) on a dead scene with one advance_time + one set_quest_status(status="active").
Reproduced GREEN on a frozen single-scene run.
Fix: arc_resolved now requires an ACTUAL snapshot quest at status=="completed" (which a
set_quest_status("active"/"failed") does not produce). + a regression test that a frozen scene
with a set_quest_status tool call present stays RED. The legitimate single-scene-progression
case (a quest reaching status=="completed") still passes. assert_behavioral 39/39 green.
…ness-aware contract (#623) (#970) test_coldopen_part_a_poll_window_outlasts_max_effort_coldopen pinned the literal `PART_A_DEADLINE="${WOS_APP_PART_A_DEADLINE:-420}"` and the old `seq 1 "$part_a_polls"` for-loop — both of which FIX 3 (#623, shipped in #966) intentionally replaced. #966's fast_gate doesn't run test_adversarial_release.py, so the stale test slipped through and left main RED. The production code is correct: 420 was SHORTER than the cold-open's own model-aware timeout (clawdnd_dm_timeout 1 = 500/550s), so a healthy-but-slow mint was abandoned ~80s early — the exact flake the test NAME warns about. FIX 3 derives the deadline FROM that tier + a positive margin and makes the poll liveness-aware. Rewrite the test to assert that *contract* semantically (extract the margin via regex, assert it's > 0 = the real "outlasts" guarantee; assert the hard-deadline + coldopen_is_live grace; keep the no-hardcoded-`seq 1 70` guard) — so a regression to a flat / too- short / non-positive-margin deadline now fails here, but the intended derivation passes. Test-only; 18/18 in test_adversarial_release.py green; fast_gate 222 intact. Co-authored-by: Eva <arncalso@gmail.com>
Root-caused via a 4-surface analysis workflow; each fix is TARGETED and preserves its integrity gate (risk-mitigations tested). (1) party_traveled no longer false-caps a complete single-scene drama; (2) a persona player crash (rc!=0) is retried+reclassified as harness-inconclusive (evidence gap), not a quality fail; (3) PART_A_DEADLINE derived from the cold-open tier + liveness grace (fixes flaky handoff legs); (4) #623 empty-mark recycle + hang-escalation closed. Tests: assert_behavioral 38 / release_readiness 47 / dm_beat_mark 8 / fast_gate 222 green.
Summary by CodeRabbit
Release Notes
Bug Fixes
Tests