fix(ambient): re-check active mode after the judge call (coexistence TOCTOU)
This commit is contained in:
@@ -236,6 +236,16 @@ func (s *Sentinel) evaluate(ctx context.Context) {
|
|||||||
cancel()
|
cancel()
|
||||||
|
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
// A mode may have activated during the (off-lock) brain call; re-check so a
|
||||||
|
// result computed while idle is not surfaced over an now-active mode.
|
||||||
|
if s.activeModeLocked() != "" {
|
||||||
|
changed := s.clearLocked()
|
||||||
|
s.mu.Unlock()
|
||||||
|
if changed {
|
||||||
|
s.fireOnChange()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
s.lastEvalTitles = joined
|
s.lastEvalTitles = joined
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.mu.Unlock()
|
s.mu.Unlock()
|
||||||
|
|||||||
@@ -228,6 +228,42 @@ func TestEvaluateCheapGateSkipsUnchangedOnTrack(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// flippingJudge simulates a harness mode activating DURING the brain call: it
|
||||||
|
// returns a drift verdict but flips the active-mode flag as a side effect, so by
|
||||||
|
// the time evaluate re-acquires the lock a mode is active.
|
||||||
|
type flippingJudge struct{ active *string }
|
||||||
|
|
||||||
|
func (f *flippingJudge) AmbientDrift(_ context.Context, _ string, _ []string) (string, error) {
|
||||||
|
*f.active = "focus"
|
||||||
|
return "deep in YouTube", nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A mode activating mid-evaluation must suppress the surface: no line, no toast,
|
||||||
|
// no memory record (coexistence rule holds across the unlock-for-I/O window).
|
||||||
|
func TestEvaluateModeActivatingDuringJudgeStaysSilent(t *testing.T) {
|
||||||
|
active := ""
|
||||||
|
n := &fakeNotifier{}
|
||||||
|
mem := &memoryFake{}
|
||||||
|
s := New(Deps{
|
||||||
|
AI: &flippingJudge{active: &active},
|
||||||
|
Notifier: n,
|
||||||
|
Memory: mem,
|
||||||
|
Clock: func() time.Time { return time.Unix(1_700_000_000, 0) },
|
||||||
|
ActiveMode: func() string { return active },
|
||||||
|
}, time.Minute, modeNotify)
|
||||||
|
s.OnWindow(snap("YouTube - Brave", "Brave"))
|
||||||
|
s.evaluate(context.Background())
|
||||||
|
if s.Line() != "" {
|
||||||
|
t.Fatalf("mode active after judge must leave line empty, got %q", s.Line())
|
||||||
|
}
|
||||||
|
if n.calls != 0 {
|
||||||
|
t.Fatalf("mode active after judge must not toast, got %d", n.calls)
|
||||||
|
}
|
||||||
|
if len(mem.events) != 0 {
|
||||||
|
t.Fatalf("mode active after judge must not record memory, got %d", len(mem.events))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestSnoozeMutesAndClears(t *testing.T) {
|
func TestSnoozeMutesAndClears(t *testing.T) {
|
||||||
j := &fakeJudge{out: []string{"drift", "drift"}}
|
j := &fakeJudge{out: []string{"drift", "drift"}}
|
||||||
active := ""
|
active := ""
|
||||||
|
|||||||
Reference in New Issue
Block a user