diff --git a/.core/TODO.md b/.core/TODO.md index 2eeef820..e69de29b 100644 --- a/.core/TODO.md +++ b/.core/TODO.md @@ -1 +0,0 @@ -- @bug pkg/contextmenu/service.go:42 — shutdown and menu mutation paths still assume a non-nil platform backend and can panic if the service is wired without one. diff --git a/pkg/contextmenu/service.go b/pkg/contextmenu/service.go index 97196475..b1dc71d5 100644 --- a/pkg/contextmenu/service.go +++ b/pkg/contextmenu/service.go @@ -18,6 +18,10 @@ type Service struct { registeredMenus map[string]ContextMenuDef } +func platformUnavailableError(op string) error { + return coreerr.E("contextmenu."+op, "platform backend unavailable", nil) +} + func (s *Service) OnStartup(_ context.Context) core.Result { s.Core().RegisterQuery(s.handleQuery) s.Core().Action("contextmenu.add", func(_ context.Context, opts core.Options) core.Result { @@ -43,6 +47,10 @@ func (s *Service) OnShutdown(_ context.Context) core.Result { // Destroy all registered menus on shutdown to release platform resources s.mu.Lock() defer s.mu.Unlock() + if s.platform == nil { + s.registeredMenus = make(map[string]ContextMenuDef) + return core.Result{OK: true} + } for name := range s.registeredMenus { _ = s.platform.Remove(name) } @@ -90,6 +98,9 @@ func (s *Service) queryList() map[string]ContextMenuDef { } func (s *Service) taskAdd(t TaskAdd) error { + if s.platform == nil { + return platformUnavailableError("taskAdd") + } s.mu.Lock() defer s.mu.Unlock() // If menu already exists, remove it first (replace semantics). @@ -129,6 +140,9 @@ func (s *Service) taskAdd(t TaskAdd) error { } func (s *Service) taskRemove(t TaskRemove) error { + if s.platform == nil { + return platformUnavailableError("taskRemove") + } s.mu.Lock() defer s.mu.Unlock() if _, exists := s.registeredMenus[t.Name]; !exists { @@ -145,6 +159,9 @@ func (s *Service) taskRemove(t TaskRemove) error { } func (s *Service) taskUpdate(t TaskUpdate) error { + if s.platform == nil { + return platformUnavailableError("taskUpdate") + } s.mu.Lock() defer s.mu.Unlock() oldMenu, exists := s.registeredMenus[t.Name] @@ -182,6 +199,9 @@ func (s *Service) taskUpdate(t TaskUpdate) error { } func (s *Service) taskDestroy(t TaskDestroy) error { + if s.platform == nil { + return platformUnavailableError("taskDestroy") + } s.mu.Lock() defer s.mu.Unlock() if _, exists := s.registeredMenus[t.Name]; !exists { diff --git a/pkg/contextmenu/service_test.go b/pkg/contextmenu/service_test.go index 9695d73d..c46c15e7 100644 --- a/pkg/contextmenu/service_test.go +++ b/pkg/contextmenu/service_test.go @@ -126,6 +126,31 @@ func TestRegister_Good(t *testing.T) { assert.NotNil(t, svc.platform) } +func TestNilPlatform_Good_MutationAndShutdownAreSafe(t *testing.T) { + _, c := newTestContextMenuService(t, nil) + + cases := []struct { + name string + action string + task any + }{ + {name: "add", action: "contextmenu.add", task: TaskAdd{Name: "file-menu", Menu: ContextMenuDef{Name: "file-menu"}}}, + {name: "remove", action: "contextmenu.remove", task: TaskRemove{Name: "file-menu"}}, + {name: "update", action: "contextmenu.update", task: TaskUpdate{Name: "file-menu", Menu: ContextMenuDef{Name: "file-menu"}}}, + {name: "destroy", action: "contextmenu.destroy", task: TaskDestroy{Name: "file-menu"}}, + } + + for _, tc := range cases { + r := taskRun(c, tc.action, tc.task) + assert.False(t, r.OK, tc.name) + err, _ := r.Value.(error) + require.Error(t, err) + assert.Contains(t, err.Error(), "platform backend unavailable") + } + + assert.True(t, c.ServiceShutdown(t.Context()).OK) +} + func TestTaskAdd_Good(t *testing.T) { mp := newMockPlatform() _, c := newTestContextMenuService(t, mp)