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 }
38
39 eventsCtx, cancel := context.WithCancel(ctx)
40 app.eventsCtx = eventsCtx
41 setupSubscriber(eventsCtx, app.serviceEventsWG, "permissions",
42 app.Permissions.Subscribe, app.events)
43 setupSubscriber(eventsCtx, app.serviceEventsWG, "permissions-notifications",
44 app.Permissions.SubscribeNotifications, app.events)
45 setupSubscriber(eventsCtx, app.serviceEventsWG, "agent-notifications",
46 app.agentNotifications.Subscribe, app.events)
47 app.cleanupFuncs = append(app.cleanupFuncs, func(context.Context) error {
48 cancel()
49 app.serviceEventsWG.Wait()
50 app.events.Shutdown()
51 return nil
52 })
53 return app
54}
55
56// ShutdownForTest tears down the App's event broker and fan-in
57// goroutines. It is safe to call multiple times.
58//
59// Use this in tests instead of [App.Shutdown], which drives a full
60// production shutdown path (database release, LSP teardown, MCP
61// shutdown) that synthetic test apps cannot satisfy.
62func (app *App) ShutdownForTest() {
63 for _, cleanup := range app.cleanupFuncs {
64 if cleanup != nil {
65 _ = cleanup(context.Background())
66 }
67 }
68 app.cleanupFuncs = nil
69}