go/service_test.go

191 lines
4.3 KiB
Go
Raw Permalink Normal View History

package core_test
import (
"context"
"testing"
2026-03-20 21:00:48 +00:00
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Service Registration ---
func TestService_Register_Good(t *testing.T) {
c := New()
r := c.Service("auth", Service{})
assert.True(t, r.OK)
}
func TestService_Register_Duplicate_Bad(t *testing.T) {
c := New()
c.Service("auth", Service{})
r := c.Service("auth", Service{})
assert.False(t, r.OK)
}
func TestService_Register_Empty_Bad(t *testing.T) {
c := New()
r := c.Service("", Service{})
assert.False(t, r.OK)
}
func TestService_Get_Good(t *testing.T) {
c := New()
c.Service("brain", Service{OnStart: func() Result { return Result{OK: true} }})
r := c.Service("brain")
assert.True(t, r.OK)
assert.NotNil(t, r.Value)
}
func TestService_Get_Bad(t *testing.T) {
c := New()
r := c.Service("nonexistent")
assert.False(t, r.OK)
}
func TestService_Names_Good(t *testing.T) {
c := New()
c.Service("a", Service{})
c.Service("b", Service{})
names := c.Services()
assert.Contains(t, names, "a")
assert.Contains(t, names, "b")
assert.Contains(t, names, "cli") // auto-registered by CliRegister in New()
}
// --- Service Lifecycle ---
func TestService_Lifecycle_Good(t *testing.T) {
c := New()
started := false
stopped := false
c.Service("lifecycle", Service{
OnStart: func() Result { started = true; return Result{OK: true} },
OnStop: func() Result { stopped = true; return Result{OK: true} },
})
sr := c.Startables()
assert.True(t, sr.OK)
startables := sr.Value.([]*Service)
assert.Len(t, startables, 1)
startables[0].OnStart()
assert.True(t, started)
tr := c.Stoppables()
assert.True(t, tr.OK)
stoppables := tr.Value.([]*Service)
assert.Len(t, stoppables, 1)
stoppables[0].OnStop()
assert.True(t, stopped)
}
type autoLifecycleService struct {
started bool
stopped bool
messages []Message
}
feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming. Critical bugs (Plan 1): - P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery - P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates - P3-1: Startable/Stoppable return Result (breaking, clean) - P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH - I3: Embed() removed, I15: New() comment fixed - I9: CommandLifecycle removed → Command.Managed field Registry[T] (Plan 2): - Universal thread-safe named collection with 3 lock modes - All 5 registries migrated: services, commands, drive, data, lock - Insertion order preserved (fixes P4-1) - c.RegistryOf("name") cross-cutting accessor Action/Task system (Plan 3): - Action type with Run()/Exists(), ActionHandler signature - c.Action("name") dual-purpose accessor (register/invoke) - TaskDef with Steps — sequential chain, async dispatch, previous-input piping - Panic recovery on all Action execution - broadcast() internal, ACTION() sugar Process primitive (Plan 4): - c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists - No deps added — delegates to c.Action("process.*") - Permission-by-registration: no handler = no capability Missing primitives (Plan 5): - core.ID() — atomic counter + crypto/rand suffix - ValidateName() / SanitisePath() — reusable validation - Fs.WriteAtomic() — write-to-temp-then-rename - Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass - AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly} Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 15:18:25 +00:00
func (s *autoLifecycleService) OnStartup(_ context.Context) Result {
s.started = true
feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming. Critical bugs (Plan 1): - P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery - P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates - P3-1: Startable/Stoppable return Result (breaking, clean) - P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH - I3: Embed() removed, I15: New() comment fixed - I9: CommandLifecycle removed → Command.Managed field Registry[T] (Plan 2): - Universal thread-safe named collection with 3 lock modes - All 5 registries migrated: services, commands, drive, data, lock - Insertion order preserved (fixes P4-1) - c.RegistryOf("name") cross-cutting accessor Action/Task system (Plan 3): - Action type with Run()/Exists(), ActionHandler signature - c.Action("name") dual-purpose accessor (register/invoke) - TaskDef with Steps — sequential chain, async dispatch, previous-input piping - Panic recovery on all Action execution - broadcast() internal, ACTION() sugar Process primitive (Plan 4): - c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists - No deps added — delegates to c.Action("process.*") - Permission-by-registration: no handler = no capability Missing primitives (Plan 5): - core.ID() — atomic counter + crypto/rand suffix - ValidateName() / SanitisePath() — reusable validation - Fs.WriteAtomic() — write-to-temp-then-rename - Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass - AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly} Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 15:18:25 +00:00
return Result{OK: true}
}
feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming. Critical bugs (Plan 1): - P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery - P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates - P3-1: Startable/Stoppable return Result (breaking, clean) - P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH - I3: Embed() removed, I15: New() comment fixed - I9: CommandLifecycle removed → Command.Managed field Registry[T] (Plan 2): - Universal thread-safe named collection with 3 lock modes - All 5 registries migrated: services, commands, drive, data, lock - Insertion order preserved (fixes P4-1) - c.RegistryOf("name") cross-cutting accessor Action/Task system (Plan 3): - Action type with Run()/Exists(), ActionHandler signature - c.Action("name") dual-purpose accessor (register/invoke) - TaskDef with Steps — sequential chain, async dispatch, previous-input piping - Panic recovery on all Action execution - broadcast() internal, ACTION() sugar Process primitive (Plan 4): - c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists - No deps added — delegates to c.Action("process.*") - Permission-by-registration: no handler = no capability Missing primitives (Plan 5): - core.ID() — atomic counter + crypto/rand suffix - ValidateName() / SanitisePath() — reusable validation - Fs.WriteAtomic() — write-to-temp-then-rename - Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass - AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly} Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 15:18:25 +00:00
func (s *autoLifecycleService) OnShutdown(_ context.Context) Result {
s.stopped = true
feat: implement RFC plans 1-5 — Registry[T], Action/Task, Process, primitives Plans 1-5 complete for core/go scope. 456 tests, 84.4% coverage, 100% AX-7 naming. Critical bugs (Plan 1): - P4-3+P7-3: ACTION broadcast calls all handlers with panic recovery - P7-2+P7-4: RunE() with defer ServiceShutdown, Run() delegates - P3-1: Startable/Stoppable return Result (breaking, clean) - P9-1: Zero os/exec — App.Find() rewritten with os.Stat+PATH - I3: Embed() removed, I15: New() comment fixed - I9: CommandLifecycle removed → Command.Managed field Registry[T] (Plan 2): - Universal thread-safe named collection with 3 lock modes - All 5 registries migrated: services, commands, drive, data, lock - Insertion order preserved (fixes P4-1) - c.RegistryOf("name") cross-cutting accessor Action/Task system (Plan 3): - Action type with Run()/Exists(), ActionHandler signature - c.Action("name") dual-purpose accessor (register/invoke) - TaskDef with Steps — sequential chain, async dispatch, previous-input piping - Panic recovery on all Action execution - broadcast() internal, ACTION() sugar Process primitive (Plan 4): - c.Process() returns Action sugar — Run/RunIn/RunWithEnv/Start/Kill/Exists - No deps added — delegates to c.Action("process.*") - Permission-by-registration: no handler = no capability Missing primitives (Plan 5): - core.ID() — atomic counter + crypto/rand suffix - ValidateName() / SanitisePath() — reusable validation - Fs.WriteAtomic() — write-to-temp-then-rename - Fs.NewUnrestricted() / Fs.Root() — legitimate sandbox bypass - AX-7: 456/456 tests renamed to TestFile_Function_{Good,Bad,Ugly} Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-25 15:18:25 +00:00
return Result{OK: true}
}
func (s *autoLifecycleService) HandleIPCEvents(_ *Core, msg Message) Result {
s.messages = append(s.messages, msg)
return Result{OK: true}
}
func TestService_RegisterService_Bad(t *testing.T) {
t.Run("EmptyName", func(t *testing.T) {
c := New()
r := c.RegisterService("", "value")
assert.False(t, r.OK)
err, ok := r.Value.(error)
if assert.True(t, ok) {
assert.Equal(t, "core.RegisterService", Operation(err))
}
})
t.Run("DuplicateName", func(t *testing.T) {
c := New()
assert.True(t, c.RegisterService("svc", "first").OK)
r := c.RegisterService("svc", "second")
assert.False(t, r.OK)
})
t.Run("LockedRegistry", func(t *testing.T) {
c := New()
c.LockEnable()
c.LockApply()
r := c.RegisterService("blocked", "value")
assert.False(t, r.OK)
})
}
func TestService_RegisterService_Ugly(t *testing.T) {
t.Run("AutoDiscoversLifecycleAndIPCHandlers", func(t *testing.T) {
c := New()
svc := &autoLifecycleService{}
r := c.RegisterService("auto", svc)
assert.True(t, r.OK)
assert.True(t, c.ServiceStartup(context.Background(), nil).OK)
assert.True(t, c.ACTION("ping").OK)
assert.True(t, c.ServiceShutdown(context.Background()).OK)
assert.True(t, svc.started)
assert.True(t, svc.stopped)
assert.Contains(t, svc.messages, Message("ping"))
})
t.Run("NilInstanceReturnsServiceDTO", func(t *testing.T) {
c := New()
assert.True(t, c.RegisterService("nil", nil).OK)
r := c.Service("nil")
if assert.True(t, r.OK) {
svc, ok := r.Value.(*Service)
if assert.True(t, ok) {
assert.Equal(t, "nil", svc.Name)
assert.Nil(t, svc.Instance)
}
}
})
}
func TestService_ServiceFor_Bad(t *testing.T) {
typed, ok := ServiceFor[string](New(), "missing")
assert.False(t, ok)
assert.Equal(t, "", typed)
}
func TestService_ServiceFor_Ugly(t *testing.T) {
c := New()
assert.True(t, c.RegisterService("value", "hello").OK)
typed, ok := ServiceFor[int](c, "value")
assert.False(t, ok)
assert.Equal(t, 0, typed)
}
func TestService_MustServiceFor_Bad(t *testing.T) {
c := New()
assert.PanicsWithError(t, `core.MustServiceFor: service "missing" not found or wrong type`, func() {
_ = MustServiceFor[string](c, "missing")
})
}
func TestService_MustServiceFor_Ugly(t *testing.T) {
var c *Core
assert.Panics(t, func() {
_ = MustServiceFor[string](c, "missing")
})
}