testing.go

 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}