1package app
2
3import (
4 "context"
5 "sync"
6
7 tea "charm.land/bubbletea/v2"
8 "github.com/charmbracelet/crush/internal/agent/notify"
9 "github.com/charmbracelet/crush/internal/permission"
10 "github.com/charmbracelet/crush/internal/pubsub"
11)
12
13// NewForTest constructs a minimal [App] suitable for in-process tests
14// that need a working event broker and permission service without
15// booting a real config, database, LSP, MCP, or agent coordinator.
16//
17// The returned App has:
18//
19// - A live `events` broker that [App.SendEvent] publishes to and
20// [App.Events] subscribes from.
21// - A real [permission.Service] whose request and notification
22// brokers are fanned into the events broker, so subscribers to
23// [App.Events] observe the same permission events the production
24// wiring would deliver to SSE clients.
25// - An [App.agentNotifications] broker.
26//
27// The caller owns lifetime: cancel ctx (or call [App.Shutdown]) to
28// tear down the fan-in goroutines and the events broker.
29func NewForTest(ctx context.Context) *App {
30 app := &App{
31 Permissions: permission.NewPermissionService("", false, nil),
32 globalCtx: ctx,
33 events: pubsub.NewBroker[tea.Msg](),
34 serviceEventsWG: &sync.WaitGroup{},
35 tuiWG: &sync.WaitGroup{},
36 agentNotifications: pubsub.NewBroker[notify.Notification](),
37 runCompletions: pubsub.NewBroker[notify.RunComplete](),
38 }
39
40 eventsCtx, cancel := context.WithCancel(ctx)
41 app.eventsCtx = eventsCtx
42 setupSubscriber(eventsCtx, app.serviceEventsWG, "permissions",
43 app.Permissions.Subscribe, app.events)
44 setupSubscriber(eventsCtx, app.serviceEventsWG, "permissions-notifications",
45 app.Permissions.SubscribeNotifications, app.events)
46 setupSubscriber(eventsCtx, app.serviceEventsWG, "agent-notifications",
47 app.agentNotifications.Subscribe, app.events)
48 setupSubscriber(eventsCtx, app.serviceEventsWG, "run-completions",
49 app.runCompletions.Subscribe, app.events)
50 app.cleanupFuncs = append(app.cleanupFuncs, func(context.Context) error {
51 cancel()
52 app.serviceEventsWG.Wait()
53 app.events.Shutdown()
54 return nil
55 })
56 return app
57}
58
59// ShutdownForTest tears down the App's event broker and fan-in
60// goroutines. It is safe to call multiple times.
61//
62// Use this in tests instead of [App.Shutdown], which drives a full
63// production shutdown path (database release, LSP teardown, MCP
64// shutdown) that synthetic test apps cannot satisfy.
65func (app *App) ShutdownForTest() {
66 for _, cleanup := range app.cleanupFuncs {
67 if cleanup != nil {
68 _ = cleanup(context.Background())
69 }
70 }
71 app.cleanupFuncs = nil
72}