package main import ( "reflect" "testing" "github.com/kami/maven/internal/decision" "github.com/kami/maven/internal/event" "github.com/kami/maven/internal/ipc" "github.com/kami/maven/internal/store" "github.com/kami/maven/internal/voice" ) // fullDeps — a deployment with every optional piece present. Nothing here is // run: newDaemonAPI takes method values and backgroundWorkers is pure, so // zero-value wirings are enough to say what WOULD be started. func fullDeps() bootDeps { h := &reactiveHandler{ ecosystem: &ecosystemWiring{nexus: &nexusClient{}}, decisions: decision.NewRing(), } return bootDeps{ coreFor: func() ipc.CoreAPI { return ipc.UnimplementedCoreAPI{} }, tl: &tickLoop{}, evBus: event.NewBus(4), st: &store.Store{}, factWorker: &factEnrichmentWorker{}, evalWorker: &memoryEvalWorker{}, feedWkr: &feedWorker{}, crawlWkr: &crawlWorker{}, voiceW: &voiceWiring{ server: &voice.Server{}, handler: h, mcp: &mcpWiring{}, home: &homeWiring{}, }, } } // The unlock path used to build its own daemonAPI literal and leave nexus and // getMCPServers nil (V-639). Both paths call newDaemonAPI now, so the drift // that can still happen is a field added to the struct and not to the // constructor. This catches that one, by name. func TestNewDaemonAPISetsEveryField(t *testing.T) { prev := allowSeedOnStart allowSeedOnStart = true defer func() { allowSeedOnStart = prev }() api := newDaemonAPI(fullDeps()) v := reflect.ValueOf(*api) for i := range v.NumField() { if v.Field(i).IsZero() { t.Errorf("newDaemonAPI left %s unset — a fully wired deployment must fill every field", v.Type().Field(i).Name) } } } // The handler is wired with the bare store adapter and cannot serve the day // plan until upgradeAPI hands it the real one. The unlocked path did that and // the unlock path did it too; keep it a property of the constructor. func TestNewDaemonAPIUpgradesTheHandler(t *testing.T) { d := fullDeps() api := newDaemonAPI(d) if d.voiceW.handler.api != ipc.CoreAPI(api) { t.Fatal("newDaemonAPI did not hand the handler the API it built") } } // Every worker the daemon runs goes through startBackground, so shutdown can // wait for it. The unlock path used to start seven of these as bare // `go func()` under a shadowed WaitGroup. func TestBackgroundWorkersFullSet(t *testing.T) { want := []string{"voice", "tick", "fact-enrichment", "memory-eval", "feed", "crawl", "mcp", "home"} var got []string for _, w := range backgroundWorkers(fullDeps()) { got = append(got, w.name) } if !reflect.DeepEqual(got, want) { t.Errorf("workers = %v, want %v", got, want) } } // A default box configures none of the optional blocks. Two workers always run // and the rest stay dark, rather than a nil run being scheduled. func TestBackgroundWorkersFloor(t *testing.T) { d := fullDeps() d.evalWorker, d.feedWkr, d.crawlWkr, d.voiceW = nil, nil, nil, nil want := []string{"tick", "fact-enrichment"} var got []string for _, w := range backgroundWorkers(d) { got = append(got, w.name) } if !reflect.DeepEqual(got, want) { t.Errorf("workers = %v, want %v", got, want) } }