-
Notifications
You must be signed in to change notification settings - Fork 0
fix(#648): grace window so a just-armed narrating spinner survives a spurious clear #666
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -241,6 +241,10 @@ | |
| caps: () => sandbox.window.__LIVE_TAIL_CAPS__, | ||
| beatCount: () => (reactHost.api().chatBeats || []).length, | ||
| drain, | ||
| // #648: advance the fake clock (Date.now reads NOW) so a test can resolve a turn AFTER the | ||
| // armPending grace window — a real DM beat lands ~100–150s post-submit, far past the guard, so a | ||
| // same-tick resolve is unrealistic (and now intentionally a no-op for the freshly-armed spinner). | ||
| advance: (ms) => { NOW += ms || 0; }, | ||
|
Comment on lines
+244
to
+247
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Replace ambiguous EN DASH to satisfy Ruff RUF001. Line 245 includes Minimal fix- // armPending grace window — a real DM beat lands ~100–150s post-submit, far past the guard, so a
+ // armPending grace window - a real DM beat lands ~100-150s post-submit, far past the guard, so a🧰 Tools🪛 Ruff (0.15.15)[warning] 245-245: String contains ambiguous (RUF001) 🤖 Prompt for AI Agents |
||
| }; | ||
|
|
||
| // Each test's `script` is a sequence of statements ending in `return (<resultExpr>)`. The scripts | ||
|
|
@@ -477,6 +481,9 @@ def test_resolved_beat_makes_next_turn_a_later_beat(self): | |
| # Turn 1: arm, then resolve it with a turn-END /chat DM line (bumps the internal beat count). | ||
| "h.arm('open the scene');" | ||
| "h.enqueue('/chat', { items: [{ role: 'dm', text: 'You stand at the gates of Baldur\\u2019s Gate.' }], next: 1 });" | ||
| # a real DM beat lands ~120s post-submit — advance past the #648 armPending grace so the | ||
| # turn-END line resolves the pending turn (a same-tick resolve is unrealistic + now guarded). | ||
| "h.advance(121000);" | ||
| "await h.tick();" | ||
| "var afterTurn1 = h.pending();" # JS string; afterTurn1 should be null (turn resolved) | ||
| # Turn 2: arm again — this pending must be a LATER beat (firstBeat:false). | ||
|
|
@@ -906,6 +913,9 @@ def test_engine_logged_chat_reply_resolves_without_rendering_duplicate(self): | |
| out = self._run( | ||
| "h.arm('ask the sergeant');" | ||
| "h.enqueue('/chat', { items: [{ role: 'dm', text: 'The sergeant refuses to name the captain.', engine_logged: true }], next: 1 });" | ||
| # advance past the #648 armPending grace (a real beat is ~120s) so the engine-logged | ||
| # turn-END line resolves the pending turn. | ||
| "h.advance(121000);" | ||
| "await h.tick();" | ||
| "var afterChat = h.narrationTexts();" | ||
| "var pendingAfterChat = h.pending();" | ||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -275,6 +275,9 @@ def test_clear_pending_resets_state_and_disarms_timers(self): | |||||
| out = self._run( | ||||||
| "h.arm('do thing');" | ||||||
| "var armed = h.pending();" | ||||||
| # advance past the #648 arm-grace so this exercises a GENUINE clear (a real resolution | ||||||
| # lands long after submit); the same-tick protection is covered by the #648 test below. | ||||||
| "h.advance(h.constants().armGraceMs + 1000);" | ||||||
| "h.clear();" | ||||||
| "var cleared = h.pending();" | ||||||
| # advancing past every window after a manual clear must NOT resurrect a stuck flag — | ||||||
|
|
@@ -286,3 +289,28 @@ def test_clear_pending_resets_state_and_disarms_timers(self): | |||||
| self.assertTrue(out["armed_present"]) | ||||||
| self.assertTrue(out["cleared_null"]) | ||||||
| self.assertTrue(out["no_resurrect"], "cleared timers must not fire after clearPending (clean #344 retry re-arm)") | ||||||
|
|
||||||
| # --- #648: a freshly-armed narrating turn survives a SPURIOUS same-tick clear ------------- | ||||||
| def test_armpending_survives_a_same_tick_clear_then_resolves(self): | ||||||
| """#648 (the move-sink → pending-arm contract): after an Enter-submitted Do, a clearPending | ||||||
| can fire milliseconds later — the immediate post-armPending surface poll, a /chat cursor-reset | ||||||
| re-reading the prior resolved turn's line as a fresh resolution, or a transient campaignId | ||||||
| flip tripping the per-run reset. That MUST NOT wipe the just-armed narrating spinner (the | ||||||
| adversarial's '[MAJOR] no spinner, buttons enabled, no DM for 3+ min until I clicked | ||||||
| Continue'). The guard is bounded: a REAL resolution (~100–150s later, past the grace) still | ||||||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Use ASCII hyphen in docstring to avoid Ruff RUF002 warning. Line 300 uses an EN DASH ( Minimal fix- Continue'). The guard is bounded: a REAL resolution (~100–150s later, past the grace) still
+ Continue'). The guard is bounded: a REAL resolution (~100-150s later, past the grace) still📝 Committable suggestion
Suggested change
🧰 Tools🪛 Ruff (0.15.15)[warning] 300-300: Docstring contains ambiguous (RUF002) 🤖 Prompt for AI Agents |
||||||
| clears the turn, so the spinner is never stuck on.""" | ||||||
| out = self._run( | ||||||
| "h.arm('Stand down. That child is with me.');" | ||||||
| "var armed = !!h.pending();" | ||||||
| "h.clear();" # the spurious same-tick clear (must be a no-op) | ||||||
| "var p = h.pending();" | ||||||
| "var survived = !!p;" | ||||||
| "var narrating = !!(p && !p.stuck);" | ||||||
| "h.advance(h.constants().armGraceMs + 1000);" # the real DM beat lands well past the grace | ||||||
| "h.clear();" # genuine resolution → clears | ||||||
| "({ armed: armed, survives: survived, narrating: narrating, resolves_later: h.pending() === null })" | ||||||
| ) | ||||||
| self.assertTrue(out["armed"], "armPending should arm a narrating turn") | ||||||
| self.assertTrue(out["survives"], "#648: a same-tick clear must NOT wipe the just-armed spinner") | ||||||
| self.assertTrue(out["narrating"], "the protected turn stays in the narrating (not stuck) state") | ||||||
| self.assertTrue(out["resolves_later"], "the protected turn still resolves on the real (post-grace) clear") | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Grace-only gating can block legitimate fast turn resolution.
At Line 384-Line 386, every non-streaming clear inside the 10s window is dropped. That also affects real
/chatturn-end clears for fast, chat-only turns; if the resolving line is consumed once,pendingcan remain until recovery/backstop and keep actions gated.Suggested direction (differentiate stale clear vs real resolution)
🤖 Prompt for AI Agents