Tie nudge validity to the on-task stretch
A soft nudge advisory belongs to one continuous on-task stretch in an allowed app. Guard it with an on-task-stretch epoch (advanced on session reset and on any non-on-task-match observation) instead of a session-only counter, and clear the advisory on leaving the allowed app, so a stale 'Heads up' never resurfaces after a drift episode. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -693,11 +693,26 @@ func TestNilNudgerStaysSilentOnTask(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNudgeSurvivesInterleavedDriftJudgment(t *testing.T) {
|
||||
func TestNudgeClearedOnLeavingAllowedApp(t *testing.T) {
|
||||
c, _ := newTestController(t)
|
||||
fn := &fakeNudger{msg: "kept", gate: make(chan struct{})}
|
||||
c.SetNudge(&fakeNudger{msg: "drifting within editor"})
|
||||
startActive(t, c, []string{"code"})
|
||||
c.RecordWindow(obs("code", "a.go"))
|
||||
c.RecordWindow(obs("code", "b.go"))
|
||||
waitNudge(t, c, "drifting within editor")
|
||||
// Leaving the allowed app ends the on-task stretch and voids the advisory.
|
||||
c.RecordWindow(obs("firefox", "YouTube"))
|
||||
if st := c.State(); st.Drift == nil || st.Drift.Nudge != "" {
|
||||
t.Fatalf("advisory must clear when leaving the allowed app: %+v", st.Drift)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNudgeInFlightDiscardedAfterDriftEpisode(t *testing.T) {
|
||||
c, _ := newTestController(t)
|
||||
now := time.Now()
|
||||
c.SetClock(func() time.Time { return now })
|
||||
fn := &fakeNudger{msg: "stale", gate: make(chan struct{})}
|
||||
c.SetNudge(fn)
|
||||
c.SetDriftJudge(&fakeJudge{verdict: ai.Verdict{OnTask: false, Reason: "off-task"}})
|
||||
startActive(t, c, []string{"code"})
|
||||
c.RecordWindow(obs("code", "a.go"))
|
||||
c.RecordWindow(obs("code", "b.go")) // nudge launches, blocks on gate
|
||||
@@ -705,11 +720,13 @@ func TestNudgeSurvivesInterleavedDriftJudgment(t *testing.T) {
|
||||
for time.Now().Before(deadline) && atomic.LoadInt32(&fn.calls) == 0 {
|
||||
time.Sleep(5 * time.Millisecond)
|
||||
}
|
||||
// An interleaved drift judgment bumps driftGen; the nudge must NOT be
|
||||
// discarded by it — it is guarded by session identity, not driftGen.
|
||||
c.RecordWindow(obs("firefox", "YouTube"))
|
||||
close(fn.gate) // nudge returns within the same session
|
||||
waitNudge(t, c, "kept")
|
||||
c.RecordWindow(obs("firefox", "YouTube")) // ends the on-task stretch -> epoch++
|
||||
close(fn.gate) // in-flight nudge returns now
|
||||
c.RecordWindow(obs("code", "c.go")) // back on task (debounced; no new nudge)
|
||||
time.Sleep(50 * time.Millisecond)
|
||||
if st := c.State(); st.Drift == nil || st.Drift.Nudge != "" {
|
||||
t.Fatalf("in-flight nudge from an ended stretch must not surface: %+v", st.Drift)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStaleNudgeDiscardedAfterEnd(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user