7d69a1f320
Loosen AntiDrift's session controller into Keel's general collect→brain→act
loop. A new internal/harness runs at most one mode.Mode at a time, fanning
async completions out to the web SSE and status-bar surfaces.
- internal/mode: the Mode contract (Kind/Command/View/Active) plus optional
EvidenceConsumer and Expirer ports, and the surfacing Envelope.
- internal/mode/focus: the former session/domain/statemachine packages moved
under the mode, now satisfying the harness contracts unchanged.
- internal/mode/offscreen: a one-shot away-from-desk mode built on the new
ai.Proposer, which turns a life-domain brief into one off-screen action.
- cmd/keeld replaces cmd/antidriftd; daemon wires focus + offscreen factories
with per-mode persistence under ~/.keel/modes/<kind>.
- Finish the rename: KEEL_* env refs in the README, /keeld build artifact
ignored, stale antidriftd binary removed.
Include the design + implementation plan this refactor was built from under
docs/superpowers/{specs,plans}/2026-06-04-controller-refactor*.md.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
157 lines
4.3 KiB
Go
157 lines
4.3 KiB
Go
package harness
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"keel/internal/evidence"
|
|
"keel/internal/mode"
|
|
)
|
|
|
|
// fakeMode implements Mode and (optionally) the capability interfaces.
|
|
type fakeMode struct {
|
|
mu sync.Mutex
|
|
kind string
|
|
active bool
|
|
cmds []string
|
|
windows int
|
|
deadline time.Time
|
|
expired bool
|
|
evidence bool // implements EvidenceConsumer when true
|
|
expirer bool // implements Expirer when true
|
|
}
|
|
|
|
func (f *fakeMode) Kind() string { return f.kind }
|
|
func (f *fakeMode) View() any { return map[string]bool{"active": f.active} }
|
|
func (f *fakeMode) Active() bool { f.mu.Lock(); defer f.mu.Unlock(); return f.active }
|
|
func (f *fakeMode) Command(_ context.Context, name string, _ json.RawMessage) error {
|
|
f.mu.Lock()
|
|
defer f.mu.Unlock()
|
|
f.cmds = append(f.cmds, name)
|
|
if name == "finish" {
|
|
f.active = false
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type evidenceMode struct{ *fakeMode }
|
|
|
|
func (e evidenceMode) OnWindow(evidence.WindowSnapshot) { e.mu.Lock(); e.windows++; e.mu.Unlock() }
|
|
|
|
type expirerMode struct{ *fakeMode }
|
|
|
|
func (e expirerMode) Deadline() time.Time { return e.deadline }
|
|
func (e expirerMode) Expire() error {
|
|
e.mu.Lock()
|
|
e.expired = true
|
|
e.active = false
|
|
e.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
func newHarness(m mode.Mode) *Harness {
|
|
return New(Services{Clock: time.Now}, map[string]Factory{
|
|
m.Kind(): func(Services) mode.Mode { return m },
|
|
})
|
|
}
|
|
|
|
func TestStartSingleActiveInvariant(t *testing.T) {
|
|
h := New(Services{}, map[string]Factory{
|
|
"a": func(Services) mode.Mode { return &fakeMode{kind: "a", active: true} },
|
|
"b": func(Services) mode.Mode { return &fakeMode{kind: "b", active: true} },
|
|
})
|
|
if err := h.Start("a"); err != nil {
|
|
t.Fatalf("Start a: %v", err)
|
|
}
|
|
if err := h.Start("a"); err != nil {
|
|
t.Fatalf("re-Start a should be idempotent: %v", err)
|
|
}
|
|
if err := h.Start("b"); err != ErrBusy {
|
|
t.Fatalf("Start b while a active = %v, want ErrBusy", err)
|
|
}
|
|
if got := h.State().ActiveMode; got != "a" {
|
|
t.Fatalf("ActiveMode = %q, want a", got)
|
|
}
|
|
}
|
|
|
|
func TestCommandReleasesWhenInactive(t *testing.T) {
|
|
m := &fakeMode{kind: "a", active: true}
|
|
h := newHarness(m)
|
|
_ = h.Start("a")
|
|
if err := h.Command(context.Background(), "finish", nil); err != nil {
|
|
t.Fatalf("Command: %v", err)
|
|
}
|
|
if got := h.State().ActiveMode; got != "" {
|
|
t.Fatalf("after finish ActiveMode = %q, want idle", got)
|
|
}
|
|
}
|
|
|
|
func TestRecordWindowOnlyToEvidenceConsumers(t *testing.T) {
|
|
plain := &fakeMode{kind: "plain", active: true}
|
|
h := newHarness(plain)
|
|
_ = h.Start("plain")
|
|
h.RecordWindow(evidence.WindowSnapshot{}) // must be a no-op, no panic
|
|
if plain.windows != 0 {
|
|
t.Fatalf("plain mode received %d windows, want 0", plain.windows)
|
|
}
|
|
|
|
ev := evidenceMode{&fakeMode{kind: "ev", active: true}}
|
|
h2 := newHarness(ev)
|
|
_ = h2.Start("ev")
|
|
h2.RecordWindow(evidence.WindowSnapshot{})
|
|
if ev.windows != 1 {
|
|
t.Fatalf("evidence mode received %d windows, want 1", ev.windows)
|
|
}
|
|
}
|
|
|
|
func TestExpiryOnlyForExpirers(t *testing.T) {
|
|
plain := &fakeMode{kind: "plain", active: true}
|
|
h := newHarness(plain)
|
|
_ = h.Start("plain")
|
|
if err := h.Expire(); err != ErrIdle {
|
|
t.Fatalf("Expire on non-expirer = %v, want ErrIdle", err)
|
|
}
|
|
if !h.Deadline().IsZero() {
|
|
t.Fatal("non-expirer Deadline should be zero")
|
|
}
|
|
|
|
ex := expirerMode{&fakeMode{kind: "ex", active: true, deadline: time.Unix(100, 0)}}
|
|
h2 := newHarness(ex)
|
|
_ = h2.Start("ex")
|
|
if got := h2.Deadline(); !got.Equal(time.Unix(100, 0)) {
|
|
t.Fatalf("Deadline = %v, want unix 100", got)
|
|
}
|
|
if err := h2.Expire(); err != nil {
|
|
t.Fatalf("Expire: %v", err)
|
|
}
|
|
if got := h2.State().ActiveMode; got != "" {
|
|
t.Fatalf("after Expire ActiveMode = %q, want idle", got)
|
|
}
|
|
}
|
|
|
|
func TestCommandWhileIdle(t *testing.T) {
|
|
h := New(Services{}, map[string]Factory{})
|
|
if err := h.Command(context.Background(), "x", nil); err != ErrIdle {
|
|
t.Fatalf("Command while idle = %v, want ErrIdle", err)
|
|
}
|
|
}
|
|
|
|
func TestServicesNotifyReachesOnChangeListeners(t *testing.T) {
|
|
h := New(Services{}, map[string]Factory{})
|
|
fired := make(chan struct{}, 1)
|
|
h.AddOnChange(func() { fired <- struct{}{} })
|
|
svc := h.Services()
|
|
if svc.Notify == nil {
|
|
t.Fatal("Services().Notify is nil; restore path would not propagate updates")
|
|
}
|
|
svc.Notify() // simulate a restored mode's async completion firing notify
|
|
select {
|
|
case <-fired:
|
|
case <-time.After(time.Second):
|
|
t.Fatal("Services().Notify did not reach the registered onChange listener")
|
|
}
|
|
}
|