test: rewrite test suite for AX primitives API
164 tests, 41.3% coverage. Tests written against the public API only (external test package, no _test.go in pkg/core/). Covers: New(Options), Data, Drive, Config, Service, Error, IPC, Fs, Cli, Lock, Array, Log, App, Runtime, Task. Fixes: NewCommand now inits flagset, New() wires Cli root command. Old tests removed — they referenced With*, RegisterService, and other patterns that no longer exist. Co-Authored-By: Virgil <virgil@lethean.io>
This commit is contained in:
parent
f51c748f49
commit
1ca010e1fb
31 changed files with 1671 additions and 1600 deletions
|
|
@ -45,6 +45,9 @@ func NewCommand(name string, description ...string) *Command {
|
|||
sliceSeparator: make(map[string]string),
|
||||
}
|
||||
|
||||
// Init flagset so flags can be added before Run
|
||||
result.setParentCommandPath("")
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -95,5 +95,10 @@ func New(opts ...Options) *Core {
|
|||
}
|
||||
}
|
||||
|
||||
// Init Cli root command from app name
|
||||
c.cli.rootCommand = NewCommand(c.app.Name)
|
||||
c.cli.rootCommand.setParentCommandPath("")
|
||||
c.cli.rootCommand.setApp(c.cli)
|
||||
|
||||
return c
|
||||
}
|
||||
|
|
|
|||
40
tests/app_test.go
Normal file
40
tests/app_test.go
Normal file
|
|
@ -0,0 +1,40 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- App ---
|
||||
|
||||
func TestApp_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
assert.Equal(t, "myapp", c.App().Name)
|
||||
}
|
||||
|
||||
func TestApp_Empty_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotNil(t, c.App())
|
||||
assert.Equal(t, "", c.App().Name)
|
||||
}
|
||||
|
||||
func TestApp_Runtime_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.App().Runtime = &struct{ Name string }{Name: "wails"}
|
||||
assert.NotNil(t, c.App().Runtime)
|
||||
}
|
||||
|
||||
func TestApp_Find_Good(t *testing.T) {
|
||||
app := Find("go", "go")
|
||||
// Find looks for a binary — go should be in PATH
|
||||
if app != nil {
|
||||
assert.NotEmpty(t, app.Path)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApp_Find_Bad(t *testing.T) {
|
||||
app := Find("nonexistent-binary-xyz", "test")
|
||||
assert.Nil(t, app)
|
||||
}
|
||||
88
tests/array_test.go
Normal file
88
tests/array_test.go
Normal file
|
|
@ -0,0 +1,88 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Array[T] ---
|
||||
|
||||
func TestArray_New_Good(t *testing.T) {
|
||||
a := NewArray("a", "b", "c")
|
||||
assert.Equal(t, 3, a.Len())
|
||||
}
|
||||
|
||||
func TestArray_Add_Good(t *testing.T) {
|
||||
a := NewArray[string]()
|
||||
a.Add("x", "y")
|
||||
assert.Equal(t, 2, a.Len())
|
||||
assert.True(t, a.Contains("x"))
|
||||
assert.True(t, a.Contains("y"))
|
||||
}
|
||||
|
||||
func TestArray_AddUnique_Good(t *testing.T) {
|
||||
a := NewArray("a", "b")
|
||||
a.AddUnique("b", "c")
|
||||
assert.Equal(t, 3, a.Len())
|
||||
}
|
||||
|
||||
func TestArray_Contains_Good(t *testing.T) {
|
||||
a := NewArray(1, 2, 3)
|
||||
assert.True(t, a.Contains(2))
|
||||
assert.False(t, a.Contains(99))
|
||||
}
|
||||
|
||||
func TestArray_Filter_Good(t *testing.T) {
|
||||
a := NewArray(1, 2, 3, 4, 5)
|
||||
evens := a.Filter(func(n int) bool { return n%2 == 0 })
|
||||
assert.Equal(t, 2, evens.Len())
|
||||
assert.True(t, evens.Contains(2))
|
||||
assert.True(t, evens.Contains(4))
|
||||
}
|
||||
|
||||
func TestArray_Each_Good(t *testing.T) {
|
||||
a := NewArray("a", "b", "c")
|
||||
var collected []string
|
||||
a.Each(func(s string) { collected = append(collected, s) })
|
||||
assert.Equal(t, []string{"a", "b", "c"}, collected)
|
||||
}
|
||||
|
||||
func TestArray_Remove_Good(t *testing.T) {
|
||||
a := NewArray("a", "b", "c")
|
||||
a.Remove("b")
|
||||
assert.Equal(t, 2, a.Len())
|
||||
assert.False(t, a.Contains("b"))
|
||||
}
|
||||
|
||||
func TestArray_Remove_Bad(t *testing.T) {
|
||||
a := NewArray("a", "b")
|
||||
a.Remove("missing")
|
||||
assert.Equal(t, 2, a.Len())
|
||||
}
|
||||
|
||||
func TestArray_Deduplicate_Good(t *testing.T) {
|
||||
a := NewArray("a", "b", "a", "c", "b")
|
||||
a.Deduplicate()
|
||||
assert.Equal(t, 3, a.Len())
|
||||
}
|
||||
|
||||
func TestArray_Clear_Good(t *testing.T) {
|
||||
a := NewArray(1, 2, 3)
|
||||
a.Clear()
|
||||
assert.Equal(t, 0, a.Len())
|
||||
}
|
||||
|
||||
func TestArray_AsSlice_Good(t *testing.T) {
|
||||
a := NewArray("x", "y")
|
||||
s := a.AsSlice()
|
||||
assert.Equal(t, []string{"x", "y"}, s)
|
||||
}
|
||||
|
||||
func TestArray_Empty_Good(t *testing.T) {
|
||||
a := NewArray[int]()
|
||||
assert.Equal(t, 0, a.Len())
|
||||
assert.False(t, a.Contains(0))
|
||||
assert.Equal(t, []int(nil), a.AsSlice())
|
||||
}
|
||||
|
|
@ -1,141 +0,0 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"context"
|
||||
"errors"
|
||||
"sync/atomic"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestCore_PerformAsync_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
var completed atomic.Bool
|
||||
var resultReceived any
|
||||
|
||||
c.RegisterAction(func(c *Core, msg Message) error {
|
||||
if tc, ok := msg.(ActionTaskCompleted); ok {
|
||||
resultReceived = tc.Result
|
||||
completed.Store(true)
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.RegisterTask(func(c *Core, task Task) (any, bool, error) {
|
||||
return "async-result", true, nil
|
||||
})
|
||||
|
||||
taskID := c.PerformAsync(TestTask{})
|
||||
assert.NotEmpty(t, taskID)
|
||||
|
||||
// Wait for completion
|
||||
assert.Eventually(t, func() bool {
|
||||
return completed.Load()
|
||||
}, 1*time.Second, 10*time.Millisecond)
|
||||
|
||||
assert.Equal(t, "async-result", resultReceived)
|
||||
}
|
||||
|
||||
func TestCore_PerformAsync_Shutdown(t *testing.T) {
|
||||
c, _ := New()
|
||||
_ = c.ServiceShutdown(context.Background())
|
||||
|
||||
taskID := c.PerformAsync(TestTask{})
|
||||
assert.Empty(t, taskID, "PerformAsync should return empty string if already shut down")
|
||||
}
|
||||
|
||||
func TestCore_Progress_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
var progressReceived float64
|
||||
var messageReceived string
|
||||
|
||||
c.RegisterAction(func(c *Core, msg Message) error {
|
||||
if tp, ok := msg.(ActionTaskProgress); ok {
|
||||
progressReceived = tp.Progress
|
||||
messageReceived = tp.Message
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
c.Progress("task-1", 0.5, "halfway", TestTask{})
|
||||
|
||||
assert.Equal(t, 0.5, progressReceived)
|
||||
assert.Equal(t, "halfway", messageReceived)
|
||||
}
|
||||
|
||||
func TestCore_WithService_UnnamedType(t *testing.T) {
|
||||
// Primitive types have no package path
|
||||
factory := func(c *Core) (any, error) {
|
||||
s := "primitive"
|
||||
return &s, nil
|
||||
}
|
||||
|
||||
_, err := New(WithService(factory))
|
||||
require.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "service name could not be discovered")
|
||||
}
|
||||
|
||||
func TestRuntime_ServiceStartup_ErrorPropagation(t *testing.T) {
|
||||
rt, _ := NewRuntime(nil)
|
||||
|
||||
// Register a service that fails startup
|
||||
errSvc := &MockStartable{err: errors.New("startup failed")}
|
||||
_ = rt.Core.RegisterService("error-svc", errSvc)
|
||||
|
||||
err := rt.ServiceStartup(context.Background(), nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "startup failed")
|
||||
}
|
||||
|
||||
func TestCore_ServiceStartup_ContextCancellation(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
s1 := &MockStartable{}
|
||||
_ = c.RegisterService("s1", s1)
|
||||
|
||||
err := c.ServiceStartup(ctx, nil)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
assert.False(t, s1.started, "Srv should not have started if context was cancelled before loop")
|
||||
}
|
||||
|
||||
func TestCore_ServiceShutdown_ContextCancellation(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel() // Cancel immediately
|
||||
|
||||
s1 := &MockStoppable{}
|
||||
_ = c.RegisterService("s1", s1)
|
||||
|
||||
err := c.ServiceShutdown(ctx)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, context.Canceled)
|
||||
assert.False(t, s1.stopped, "Srv should not have stopped if context was cancelled before loop")
|
||||
}
|
||||
|
||||
type TaskWithIDImpl struct {
|
||||
id string
|
||||
}
|
||||
|
||||
func (t *TaskWithIDImpl) SetTaskID(id string) { t.id = id }
|
||||
func (t *TaskWithIDImpl) GetTaskID() string { return t.id }
|
||||
|
||||
func TestCore_PerformAsync_InjectsID(t *testing.T) {
|
||||
c, _ := New()
|
||||
c.RegisterTask(func(c *Core, t Task) (any, bool, error) { return nil, true, nil })
|
||||
|
||||
task := &TaskWithIDImpl{}
|
||||
taskID := c.PerformAsync(task)
|
||||
|
||||
assert.Equal(t, taskID, task.GetTaskID())
|
||||
}
|
||||
|
|
@ -1,40 +0,0 @@
|
|||
package core_test
|
||||
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func BenchmarkMessageBus_Action(b *testing.B) {
|
||||
c, _ := New()
|
||||
c.RegisterAction(func(c *Core, msg Message) error {
|
||||
return nil
|
||||
})
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = c.ACTION("test")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMessageBus_Query(b *testing.B) {
|
||||
c, _ := New()
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "result", true, nil
|
||||
})
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _ = c.QUERY("test")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkMessageBus_Perform(b *testing.B) {
|
||||
c, _ := New()
|
||||
c.RegisterTask(func(c *Core, t Task) (any, bool, error) {
|
||||
return "result", true, nil
|
||||
})
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
_, _, _ = c.PERFORM("test")
|
||||
}
|
||||
}
|
||||
76
tests/cli_test.go
Normal file
76
tests/cli_test.go
Normal file
|
|
@ -0,0 +1,76 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Cli ---
|
||||
|
||||
func TestCli_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotNil(t, c.Cli())
|
||||
assert.NotNil(t, c.Cli().Command())
|
||||
}
|
||||
|
||||
func TestCli_Named_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
assert.NotNil(t, c.Cli().Command())
|
||||
}
|
||||
|
||||
func TestCli_NewChildCommand_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
child := c.Cli().NewChildCommand("test", "a test command")
|
||||
assert.NotNil(t, child)
|
||||
}
|
||||
|
||||
func TestCli_AddCommand_Good(t *testing.T) {
|
||||
c := New()
|
||||
cmd := NewCommand("hello", "says hello")
|
||||
c.Cli().AddCommand(cmd)
|
||||
}
|
||||
|
||||
func TestCli_Flags_Good(t *testing.T) {
|
||||
c := New()
|
||||
var name string
|
||||
var debug bool
|
||||
var port int
|
||||
c.Cli().StringFlag("name", "app name", &name)
|
||||
c.Cli().BoolFlag("debug", "enable debug", &debug)
|
||||
c.Cli().IntFlag("port", "port number", &port)
|
||||
}
|
||||
|
||||
func TestCli_Run_Good(t *testing.T) {
|
||||
c := New()
|
||||
executed := false
|
||||
c.Cli().Command().Action(func() error {
|
||||
executed = true
|
||||
return nil
|
||||
})
|
||||
err := c.Cli().Run("")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, executed)
|
||||
}
|
||||
|
||||
// --- Command ---
|
||||
|
||||
func TestCommand_New_Good(t *testing.T) {
|
||||
cmd := NewCommand("test", "a test command")
|
||||
assert.NotNil(t, cmd)
|
||||
}
|
||||
|
||||
func TestCommand_Child_Good(t *testing.T) {
|
||||
parent := NewCommand("root")
|
||||
child := parent.NewChildCommand("sub", "a subcommand")
|
||||
assert.NotNil(t, child)
|
||||
}
|
||||
|
||||
func TestCommand_Flags_Good(t *testing.T) {
|
||||
cmd := NewCommand("test")
|
||||
var name string
|
||||
var debug bool
|
||||
cmd.StringFlag("name", "app name", &name)
|
||||
cmd.BoolFlag("debug", "enable debug", &debug)
|
||||
}
|
||||
102
tests/config_test.go
Normal file
102
tests/config_test.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Config ---
|
||||
|
||||
func TestConfig_SetGet_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Config().Set("api_url", "https://api.lthn.ai")
|
||||
c.Config().Set("max_agents", 5)
|
||||
|
||||
val, ok := c.Config().Get("api_url")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "https://api.lthn.ai", val)
|
||||
}
|
||||
|
||||
func TestConfig_Get_Bad(t *testing.T) {
|
||||
c := New()
|
||||
val, ok := c.Config().Get("missing")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, val)
|
||||
}
|
||||
|
||||
func TestConfig_TypedAccessors_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Config().Set("url", "https://lthn.ai")
|
||||
c.Config().Set("port", 8080)
|
||||
c.Config().Set("debug", true)
|
||||
|
||||
assert.Equal(t, "https://lthn.ai", c.Config().String("url"))
|
||||
assert.Equal(t, 8080, c.Config().Int("port"))
|
||||
assert.True(t, c.Config().Bool("debug"))
|
||||
}
|
||||
|
||||
func TestConfig_TypedAccessors_Bad(t *testing.T) {
|
||||
c := New()
|
||||
// Missing keys return zero values
|
||||
assert.Equal(t, "", c.Config().String("missing"))
|
||||
assert.Equal(t, 0, c.Config().Int("missing"))
|
||||
assert.False(t, c.Config().Bool("missing"))
|
||||
}
|
||||
|
||||
// --- Feature Flags ---
|
||||
|
||||
func TestConfig_Features_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Config().Enable("dark-mode")
|
||||
c.Config().Enable("beta")
|
||||
|
||||
assert.True(t, c.Config().Enabled("dark-mode"))
|
||||
assert.True(t, c.Config().Enabled("beta"))
|
||||
assert.False(t, c.Config().Enabled("missing"))
|
||||
}
|
||||
|
||||
func TestConfig_Features_Disable_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Config().Enable("feature")
|
||||
assert.True(t, c.Config().Enabled("feature"))
|
||||
|
||||
c.Config().Disable("feature")
|
||||
assert.False(t, c.Config().Enabled("feature"))
|
||||
}
|
||||
|
||||
func TestConfig_Features_CaseSensitive(t *testing.T) {
|
||||
c := New()
|
||||
c.Config().Enable("Feature")
|
||||
assert.True(t, c.Config().Enabled("Feature"))
|
||||
assert.False(t, c.Config().Enabled("feature"))
|
||||
}
|
||||
|
||||
func TestConfig_EnabledFeatures_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Config().Enable("a")
|
||||
c.Config().Enable("b")
|
||||
c.Config().Enable("c")
|
||||
c.Config().Disable("b")
|
||||
|
||||
features := c.Config().EnabledFeatures()
|
||||
assert.Contains(t, features, "a")
|
||||
assert.Contains(t, features, "c")
|
||||
assert.NotContains(t, features, "b")
|
||||
}
|
||||
|
||||
// --- ConfigVar ---
|
||||
|
||||
func TestConfigVar_Good(t *testing.T) {
|
||||
v := NewConfigVar("hello")
|
||||
assert.True(t, v.IsSet())
|
||||
assert.Equal(t, "hello", v.Get())
|
||||
|
||||
v.Set("world")
|
||||
assert.Equal(t, "world", v.Get())
|
||||
|
||||
v.Unset()
|
||||
assert.False(t, v.IsSet())
|
||||
assert.Equal(t, "", v.Get())
|
||||
}
|
||||
|
|
@ -1,45 +0,0 @@
|
|||
package core_test
|
||||
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MockServiceWithIPC struct {
|
||||
MockService
|
||||
handled bool
|
||||
}
|
||||
|
||||
func (m *MockServiceWithIPC) HandleIPCEvents(c *Core, msg Message) error {
|
||||
m.handled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCore_WithService_IPC(t *testing.T) {
|
||||
svc := &MockServiceWithIPC{MockService: MockService{Name: "ipc-service"}}
|
||||
factory := func(c *Core) (any, error) {
|
||||
return svc, nil
|
||||
}
|
||||
c, err := New(WithService(factory))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Trigger ACTION to verify handler was registered
|
||||
err = c.ACTION(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, svc.handled)
|
||||
}
|
||||
|
||||
func TestCore_ACTION_Bad(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
errHandler := func(c *Core, msg Message) error {
|
||||
return assert.AnError
|
||||
}
|
||||
c.RegisterAction(errHandler)
|
||||
err = c.ACTION(nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), assert.AnError.Error())
|
||||
}
|
||||
|
|
@ -1,165 +0,0 @@
|
|||
package core_test
|
||||
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type MockStartable struct {
|
||||
started bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *MockStartable) OnStartup(ctx context.Context) error {
|
||||
m.started = true
|
||||
return m.err
|
||||
}
|
||||
|
||||
type MockStoppable struct {
|
||||
stopped bool
|
||||
err error
|
||||
}
|
||||
|
||||
func (m *MockStoppable) OnShutdown(ctx context.Context) error {
|
||||
m.stopped = true
|
||||
return m.err
|
||||
}
|
||||
|
||||
type MockLifecycle struct {
|
||||
MockStartable
|
||||
MockStoppable
|
||||
}
|
||||
|
||||
func TestCore_LifecycleInterfaces(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
startable := &MockStartable{}
|
||||
stoppable := &MockStoppable{}
|
||||
lifecycle := &MockLifecycle{}
|
||||
|
||||
// Register services
|
||||
err = c.RegisterService("startable", startable)
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("stoppable", stoppable)
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("lifecycle", lifecycle)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Startup
|
||||
err = c.ServiceStartup(context.Background(), nil)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, startable.started)
|
||||
assert.True(t, lifecycle.started)
|
||||
assert.False(t, stoppable.stopped)
|
||||
|
||||
// Shutdown
|
||||
err = c.ServiceShutdown(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, stoppable.stopped)
|
||||
assert.True(t, lifecycle.stopped)
|
||||
}
|
||||
|
||||
type MockLifecycleWithLog struct {
|
||||
id string
|
||||
log *[]string
|
||||
}
|
||||
|
||||
func (m *MockLifecycleWithLog) OnStartup(ctx context.Context) error {
|
||||
*m.log = append(*m.log, "start-"+m.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *MockLifecycleWithLog) OnShutdown(ctx context.Context) error {
|
||||
*m.log = append(*m.log, "stop-"+m.id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCore_LifecycleOrder(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
var callOrder []string
|
||||
|
||||
s1 := &MockLifecycleWithLog{id: "1", log: &callOrder}
|
||||
s2 := &MockLifecycleWithLog{id: "2", log: &callOrder}
|
||||
|
||||
err = c.RegisterService("s1", s1)
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("s2", s2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Startup
|
||||
err = c.ServiceStartup(context.Background(), nil)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"start-1", "start-2"}, callOrder)
|
||||
|
||||
// Reset log
|
||||
callOrder = nil
|
||||
|
||||
// Shutdown
|
||||
err = c.ServiceShutdown(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []string{"stop-2", "stop-1"}, callOrder)
|
||||
}
|
||||
|
||||
func TestCore_LifecycleErrors(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
s1 := &MockStartable{err: assert.AnError}
|
||||
s2 := &MockStoppable{err: assert.AnError}
|
||||
|
||||
_ = c.RegisterService("s1", s1)
|
||||
_ = c.RegisterService("s2", s2)
|
||||
|
||||
err = c.ServiceStartup(context.Background(), nil)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
|
||||
err = c.ServiceShutdown(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
}
|
||||
|
||||
func TestCore_LifecycleErrors_Aggregated(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Register action that fails
|
||||
c.RegisterAction(func(c *Core, msg Message) error {
|
||||
if _, ok := msg.(ActionServiceStartup); ok {
|
||||
return errors.New("startup action error")
|
||||
}
|
||||
if _, ok := msg.(ActionServiceShutdown); ok {
|
||||
return errors.New("shutdown action error")
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
// Register service that fails
|
||||
s1 := &MockStartable{err: errors.New("startup service error")}
|
||||
s2 := &MockStoppable{err: errors.New("shutdown service error")}
|
||||
|
||||
err = c.RegisterService("s1", s1)
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("s2", s2)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Startup
|
||||
err = c.ServiceStartup(context.Background(), nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "startup action error")
|
||||
assert.Contains(t, err.Error(), "startup service error")
|
||||
|
||||
// Shutdown
|
||||
err = c.ServiceShutdown(context.Background())
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "shutdown action error")
|
||||
assert.Contains(t, err.Error(), "shutdown service error")
|
||||
}
|
||||
|
|
@ -1,346 +1,63 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"context"
|
||||
"embed"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// mockApp is a simple mock for testing app injection
|
||||
type mockApp struct{}
|
||||
// --- New ---
|
||||
|
||||
func TestCore_New_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
func TestNew_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
|
||||
// Mock service for testing
|
||||
type MockService struct {
|
||||
Name string
|
||||
func TestNew_WithOptions_Good(t *testing.T) {
|
||||
c := New(Options{{K: "name", V: "myapp"}})
|
||||
assert.NotNil(t, c)
|
||||
assert.Equal(t, "myapp", c.App().Name)
|
||||
}
|
||||
|
||||
func (m *MockService) GetName() string {
|
||||
return m.Name
|
||||
func TestNew_WithOptions_Bad(t *testing.T) {
|
||||
// Empty options — should still create a valid Core
|
||||
c := New(Options{})
|
||||
assert.NotNil(t, c)
|
||||
}
|
||||
|
||||
func TestCore_WithService_Good(t *testing.T) {
|
||||
factory := func(c *Core) (any, error) {
|
||||
return &MockService{Name: "test"}, nil
|
||||
}
|
||||
c, err := New(WithService(factory))
|
||||
assert.NoError(t, err)
|
||||
svc := c.Service().Get("core")
|
||||
assert.NotNil(t, svc)
|
||||
mockSvc, ok := svc.(*MockService)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test", mockSvc.GetName())
|
||||
}
|
||||
// --- Accessors ---
|
||||
|
||||
func TestCore_WithService_Bad(t *testing.T) {
|
||||
factory := func(c *Core) (any, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
_, err := New(WithService(factory))
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
}
|
||||
|
||||
type MockConfigService struct{}
|
||||
|
||||
func (m *MockConfigService) Get(key string, out any) error { return nil }
|
||||
func (m *MockConfigService) Set(key string, v any) error { return nil }
|
||||
|
||||
func TestCore_Services_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = c.RegisterService("config", &MockConfigService{})
|
||||
assert.NoError(t, err)
|
||||
|
||||
svc := c.Service("config")
|
||||
assert.NotNil(t, svc)
|
||||
|
||||
// Cfg() returns Cfg (always available, not a service)
|
||||
cfg := c.Config()
|
||||
assert.NotNil(t, cfg)
|
||||
}
|
||||
|
||||
func TestCore_App_Good(t *testing.T) {
|
||||
app := &mockApp{}
|
||||
c, err := New(WithApp(app))
|
||||
assert.NoError(t, err)
|
||||
|
||||
// To test the global CoreGUI() function, we need to set the global instance.
|
||||
originalInstance := GetInstance()
|
||||
SetInstance(c)
|
||||
defer SetInstance(originalInstance)
|
||||
|
||||
assert.Equal(t, app, CoreGUI())
|
||||
}
|
||||
|
||||
func TestCore_App_Ugly(t *testing.T) {
|
||||
// This test ensures that calling CoreGUI() before the core is initialized panics.
|
||||
originalInstance := GetInstance()
|
||||
ClearInstance()
|
||||
defer SetInstance(originalInstance)
|
||||
assert.Panics(t, func() {
|
||||
CoreGUI()
|
||||
})
|
||||
}
|
||||
|
||||
func TestCore_Core_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
func TestAccessors_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotNil(t, c.App())
|
||||
assert.NotNil(t, c.Data())
|
||||
assert.NotNil(t, c.Drive())
|
||||
assert.NotNil(t, c.Fs())
|
||||
assert.NotNil(t, c.Config())
|
||||
assert.NotNil(t, c.Error())
|
||||
assert.NotNil(t, c.Log())
|
||||
assert.NotNil(t, c.Cli())
|
||||
assert.NotNil(t, c.IPC())
|
||||
assert.NotNil(t, c.I18n())
|
||||
assert.Equal(t, c, c.Core())
|
||||
}
|
||||
|
||||
func TestEtc_Features_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
c.Config().Enable("feature1")
|
||||
c.Config().Enable("feature2")
|
||||
|
||||
assert.True(t, c.Config().Enabled("feature1"))
|
||||
assert.True(t, c.Config().Enabled("feature2"))
|
||||
assert.False(t, c.Config().Enabled("feature3"))
|
||||
assert.False(t, c.Config().Enabled(""))
|
||||
}
|
||||
|
||||
func TestEtc_Settings_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
c.Config().Set("api_url", "https://api.lthn.sh")
|
||||
c.Config().Set("max_agents", 5)
|
||||
|
||||
assert.Equal(t, "https://api.lthn.sh", c.Config().GetString("api_url"))
|
||||
assert.Equal(t, 5, c.Config().GetInt("max_agents"))
|
||||
assert.Equal(t, "", c.Config().GetString("missing"))
|
||||
}
|
||||
|
||||
func TestEtc_Features_Edge(t *testing.T) {
|
||||
c, _ := New()
|
||||
c.Config().Enable("foo")
|
||||
assert.True(t, c.Config().Enabled("foo"))
|
||||
assert.False(t, c.Config().Enabled("FOO")) // Case sensitive
|
||||
|
||||
c.Config().Disable("foo")
|
||||
assert.False(t, c.Config().Enabled("foo"))
|
||||
}
|
||||
|
||||
func TestCore_ServiceLifecycle_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
var messageReceived Message
|
||||
handler := func(c *Core, msg Message) error {
|
||||
messageReceived = msg
|
||||
return nil
|
||||
}
|
||||
c.RegisterAction(handler)
|
||||
|
||||
// Test Startup
|
||||
_ = c.ServiceStartup(context.TODO(), nil)
|
||||
_, ok := messageReceived.(ActionServiceStartup)
|
||||
assert.True(t, ok, "expected ActionServiceStartup message")
|
||||
|
||||
// Test Shutdown
|
||||
_ = c.ServiceShutdown(context.TODO())
|
||||
_, ok = messageReceived.(ActionServiceShutdown)
|
||||
assert.True(t, ok, "expected ActionServiceShutdown message")
|
||||
}
|
||||
|
||||
func TestCore_WithApp_Good(t *testing.T) {
|
||||
app := &mockApp{}
|
||||
c, err := New(WithApp(app))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, app, c.App().Runtime)
|
||||
}
|
||||
|
||||
//go:embed testdata
|
||||
var testFS embed.FS
|
||||
|
||||
func TestCore_WithAssets_Good(t *testing.T) {
|
||||
c, err := New(WithAssets(testFS))
|
||||
assert.NoError(t, err)
|
||||
file, err := c.Embed().Open("testdata/test.txt")
|
||||
assert.NoError(t, err)
|
||||
defer func() { _ = file.Close() }()
|
||||
content, err := io.ReadAll(file)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello from testdata\n", string(content))
|
||||
}
|
||||
|
||||
func TestCore_WithServiceLock_Good(t *testing.T) {
|
||||
c, err := New(WithServiceLock())
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("test", &MockService{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCore_RegisterService_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("test", &MockService{Name: "test"})
|
||||
assert.NoError(t, err)
|
||||
svc := c.Service("test")
|
||||
assert.NotNil(t, svc)
|
||||
mockSvc, ok := svc.(*MockService)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test", mockSvc.GetName())
|
||||
}
|
||||
|
||||
func TestCore_RegisterService_Bad(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("test", &MockService{})
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("test", &MockService{})
|
||||
assert.Error(t, err)
|
||||
err = c.RegisterService("", &MockService{})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCore_ServiceFor_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("test", &MockService{Name: "test"})
|
||||
assert.NoError(t, err)
|
||||
svc, err := ServiceFor[*MockService](c, "test")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "test", svc.GetName())
|
||||
}
|
||||
|
||||
func TestCore_ServiceFor_Bad(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
_, err = ServiceFor[*MockService](c, "nonexistent")
|
||||
assert.Error(t, err)
|
||||
err = c.RegisterService("test", "not a service")
|
||||
assert.NoError(t, err)
|
||||
_, err = ServiceFor[*MockService](c, "test")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestCore_MustServiceFor_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("test", &MockService{Name: "test"})
|
||||
assert.NoError(t, err)
|
||||
svc := MustServiceFor[*MockService](c, "test")
|
||||
assert.Equal(t, "test", svc.GetName())
|
||||
}
|
||||
|
||||
func TestCore_MustServiceFor_Ugly(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// MustServiceFor panics on missing service
|
||||
assert.Panics(t, func() {
|
||||
MustServiceFor[*MockService](c, "nonexistent")
|
||||
})
|
||||
|
||||
err = c.RegisterService("test", "not a service")
|
||||
assert.NoError(t, err)
|
||||
|
||||
// MustServiceFor panics on type mismatch
|
||||
assert.Panics(t, func() {
|
||||
MustServiceFor[*MockService](c, "test")
|
||||
func TestOptions_Accessor_Good(t *testing.T) {
|
||||
c := New(Options{
|
||||
{K: "name", V: "testapp"},
|
||||
{K: "port", V: 8080},
|
||||
{K: "debug", V: true},
|
||||
})
|
||||
opts := c.Options()
|
||||
assert.NotNil(t, opts)
|
||||
assert.Equal(t, "testapp", opts.String("name"))
|
||||
assert.Equal(t, 8080, opts.Int("port"))
|
||||
assert.True(t, opts.Bool("debug"))
|
||||
}
|
||||
|
||||
type MockAction struct {
|
||||
handled bool
|
||||
}
|
||||
|
||||
func (a *MockAction) Handle(c *Core, msg Message) error {
|
||||
a.handled = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestCore_ACTION_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
action := &MockAction{}
|
||||
c.RegisterAction(action.Handle)
|
||||
err = c.ACTION(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, action.handled)
|
||||
}
|
||||
|
||||
func TestCore_RegisterActions_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
action1 := &MockAction{}
|
||||
action2 := &MockAction{}
|
||||
c.RegisterActions(action1.Handle, action2.Handle)
|
||||
err = c.ACTION(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, action1.handled)
|
||||
assert.True(t, action2.handled)
|
||||
}
|
||||
|
||||
func TestCore_WithName_Good(t *testing.T) {
|
||||
factory := func(c *Core) (any, error) {
|
||||
return &MockService{Name: "test"}, nil
|
||||
}
|
||||
c, err := New(WithName("my-service", factory))
|
||||
assert.NoError(t, err)
|
||||
svc := c.Service("my-service")
|
||||
assert.NotNil(t, svc)
|
||||
mockSvc, ok := svc.(*MockService)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test", mockSvc.GetName())
|
||||
}
|
||||
|
||||
func TestCore_WithName_Bad(t *testing.T) {
|
||||
factory := func(c *Core) (any, error) {
|
||||
return nil, assert.AnError
|
||||
}
|
||||
_, err := New(WithName("my-service", factory))
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
}
|
||||
|
||||
func TestCore_GlobalInstance_ThreadSafety_Good(t *testing.T) {
|
||||
// Save original instance
|
||||
original := GetInstance()
|
||||
defer SetInstance(original)
|
||||
|
||||
// Test SetInstance/GetInstance
|
||||
c1, _ := New()
|
||||
SetInstance(c1)
|
||||
assert.Equal(t, c1, GetInstance())
|
||||
|
||||
// Test ClearInstance
|
||||
ClearInstance()
|
||||
assert.Nil(t, GetInstance())
|
||||
|
||||
// Test concurrent access (race detector should catch issues)
|
||||
c2, _ := New(WithApp(&mockApp{}))
|
||||
done := make(chan bool)
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
go func() {
|
||||
SetInstance(c2)
|
||||
_ = GetInstance()
|
||||
done <- true
|
||||
}()
|
||||
go func() {
|
||||
inst := GetInstance()
|
||||
if inst != nil {
|
||||
_ = inst.App
|
||||
}
|
||||
done <- true
|
||||
}()
|
||||
}
|
||||
|
||||
// Wait for all goroutines
|
||||
for i := 0; i < 20; i++ {
|
||||
<-done
|
||||
}
|
||||
func TestOptions_Accessor_Nil(t *testing.T) {
|
||||
c := New()
|
||||
// No options passed — Options() returns nil
|
||||
assert.Nil(t, c.Options())
|
||||
}
|
||||
|
|
|
|||
127
tests/data_test.go
Normal file
127
tests/data_test.go
Normal file
|
|
@ -0,0 +1,127 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
//go:embed testdata
|
||||
var testFS embed.FS
|
||||
|
||||
// --- Data (Embedded Content Mounts) ---
|
||||
|
||||
func TestData_New_Good(t *testing.T) {
|
||||
c := New()
|
||||
r := c.Data().New(Options{
|
||||
{K: "name", V: "test"},
|
||||
{K: "source", V: testFS},
|
||||
{K: "path", V: "testdata"},
|
||||
})
|
||||
assert.True(t, r.OK)
|
||||
assert.NotNil(t, r.Value)
|
||||
}
|
||||
|
||||
func TestData_New_Bad(t *testing.T) {
|
||||
c := New()
|
||||
|
||||
// Missing name
|
||||
r := c.Data().New(Options{
|
||||
{K: "source", V: testFS},
|
||||
})
|
||||
assert.False(t, r.OK)
|
||||
|
||||
// Missing source
|
||||
r = c.Data().New(Options{
|
||||
{K: "name", V: "test"},
|
||||
})
|
||||
assert.False(t, r.OK)
|
||||
|
||||
// Wrong source type
|
||||
r = c.Data().New(Options{
|
||||
{K: "name", V: "test"},
|
||||
{K: "source", V: "not-an-fs"},
|
||||
})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestData_ReadString_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{
|
||||
{K: "name", V: "app"},
|
||||
{K: "source", V: testFS},
|
||||
{K: "path", V: "testdata"},
|
||||
})
|
||||
content, err := c.Data().ReadString("app/test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello from testdata\n", content)
|
||||
}
|
||||
|
||||
func TestData_ReadString_Bad(t *testing.T) {
|
||||
c := New()
|
||||
_, err := c.Data().ReadString("nonexistent/file.txt")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestData_ReadFile_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{
|
||||
{K: "name", V: "app"},
|
||||
{K: "source", V: testFS},
|
||||
{K: "path", V: "testdata"},
|
||||
})
|
||||
data, err := c.Data().ReadFile("app/test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello from testdata\n", string(data))
|
||||
}
|
||||
|
||||
func TestData_Get_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{
|
||||
{K: "name", V: "brain"},
|
||||
{K: "source", V: testFS},
|
||||
{K: "path", V: "testdata"},
|
||||
})
|
||||
emb := c.Data().Get("brain")
|
||||
assert.NotNil(t, emb)
|
||||
|
||||
// Read via the Embed directly
|
||||
file, err := emb.Open("test.txt")
|
||||
assert.NoError(t, err)
|
||||
defer file.Close()
|
||||
content, _ := io.ReadAll(file)
|
||||
assert.Equal(t, "hello from testdata\n", string(content))
|
||||
}
|
||||
|
||||
func TestData_Get_Bad(t *testing.T) {
|
||||
c := New()
|
||||
emb := c.Data().Get("nonexistent")
|
||||
assert.Nil(t, emb)
|
||||
}
|
||||
|
||||
func TestData_Mounts_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{{K: "name", V: "a"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}})
|
||||
c.Data().New(Options{{K: "name", V: "b"}, {K: "source", V: testFS}, {K: "path", V: "testdata"}})
|
||||
mounts := c.Data().Mounts()
|
||||
assert.Len(t, mounts, 2)
|
||||
assert.Contains(t, mounts, "a")
|
||||
assert.Contains(t, mounts, "b")
|
||||
}
|
||||
|
||||
// --- Legacy Embed() accessor ---
|
||||
|
||||
func TestEmbed_Legacy_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Data().New(Options{
|
||||
{K: "name", V: "app"},
|
||||
{K: "source", V: testFS},
|
||||
{K: "path", V: "testdata"},
|
||||
})
|
||||
// Legacy accessor reads from Data mount "app"
|
||||
emb := c.Embed()
|
||||
assert.NotNil(t, emb)
|
||||
}
|
||||
77
tests/drive_test.go
Normal file
77
tests/drive_test.go
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Drive (Transport Handles) ---
|
||||
|
||||
func TestDrive_New_Good(t *testing.T) {
|
||||
c := New()
|
||||
r := c.Drive().New(Options{
|
||||
{K: "name", V: "api"},
|
||||
{K: "transport", V: "https://api.lthn.ai"},
|
||||
})
|
||||
assert.True(t, r.OK)
|
||||
assert.Equal(t, "api", r.Value.Name)
|
||||
assert.Equal(t, "https://api.lthn.ai", r.Value.Transport)
|
||||
}
|
||||
|
||||
func TestDrive_New_Bad(t *testing.T) {
|
||||
c := New()
|
||||
// Missing name
|
||||
r := c.Drive().New(Options{
|
||||
{K: "transport", V: "https://api.lthn.ai"},
|
||||
})
|
||||
assert.False(t, r.OK)
|
||||
}
|
||||
|
||||
func TestDrive_Get_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Drive().New(Options{
|
||||
{K: "name", V: "ssh"},
|
||||
{K: "transport", V: "ssh://claude@10.69.69.165"},
|
||||
})
|
||||
handle := c.Drive().Get("ssh")
|
||||
assert.NotNil(t, handle)
|
||||
assert.Equal(t, "ssh://claude@10.69.69.165", handle.Transport)
|
||||
}
|
||||
|
||||
func TestDrive_Get_Bad(t *testing.T) {
|
||||
c := New()
|
||||
handle := c.Drive().Get("nonexistent")
|
||||
assert.Nil(t, handle)
|
||||
}
|
||||
|
||||
func TestDrive_Has_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Drive().New(Options{{K: "name", V: "mcp"}, {K: "transport", V: "mcp://mcp.lthn.sh"}})
|
||||
assert.True(t, c.Drive().Has("mcp"))
|
||||
assert.False(t, c.Drive().Has("missing"))
|
||||
}
|
||||
|
||||
func TestDrive_Names_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Drive().New(Options{{K: "name", V: "api"}, {K: "transport", V: "https://api.lthn.ai"}})
|
||||
c.Drive().New(Options{{K: "name", V: "ssh"}, {K: "transport", V: "ssh://claude@10.69.69.165"}})
|
||||
c.Drive().New(Options{{K: "name", V: "mcp"}, {K: "transport", V: "mcp://mcp.lthn.sh"}})
|
||||
names := c.Drive().Names()
|
||||
assert.Len(t, names, 3)
|
||||
assert.Contains(t, names, "api")
|
||||
assert.Contains(t, names, "ssh")
|
||||
assert.Contains(t, names, "mcp")
|
||||
}
|
||||
|
||||
func TestDrive_OptionsPreserved_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Drive().New(Options{
|
||||
{K: "name", V: "api"},
|
||||
{K: "transport", V: "https://api.lthn.ai"},
|
||||
{K: "timeout", V: 30},
|
||||
})
|
||||
handle := c.Drive().Get("api")
|
||||
assert.Equal(t, 30, handle.Options.Int("timeout"))
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
package core_test
|
||||
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestE_Good(t *testing.T) {
|
||||
err := E("test.op", "test message", assert.AnError)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "test.op: test message: assert.AnError general error for testing", err.Error())
|
||||
|
||||
err = E("test.op", "test message", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "test.op: test message", err.Error())
|
||||
}
|
||||
|
||||
func TestE_Unwrap(t *testing.T) {
|
||||
originalErr := errors.New("original error")
|
||||
err := E("test.op", "test message", originalErr)
|
||||
|
||||
assert.True(t, errors.Is(err, originalErr))
|
||||
|
||||
var eErr *Err
|
||||
assert.True(t, errors.As(err, &eErr))
|
||||
assert.Equal(t, "test.op", eErr.Op)
|
||||
}
|
||||
132
tests/embed_test.go
Normal file
132
tests/embed_test.go
Normal file
|
|
@ -0,0 +1,132 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"encoding/base64"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Embed (Mount + ReadFile + Sub) ---
|
||||
|
||||
func TestMount_Good(t *testing.T) {
|
||||
emb, err := Mount(testFS, "testdata")
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, emb)
|
||||
}
|
||||
|
||||
func TestMount_Bad(t *testing.T) {
|
||||
_, err := Mount(testFS, "nonexistent")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestEmbed_ReadFile_Good(t *testing.T) {
|
||||
emb, _ := Mount(testFS, "testdata")
|
||||
data, err := emb.ReadFile("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello from testdata\n", string(data))
|
||||
}
|
||||
|
||||
func TestEmbed_ReadString_Good(t *testing.T) {
|
||||
emb, _ := Mount(testFS, "testdata")
|
||||
s, err := emb.ReadString("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello from testdata\n", s)
|
||||
}
|
||||
|
||||
func TestEmbed_Open_Good(t *testing.T) {
|
||||
emb, _ := Mount(testFS, "testdata")
|
||||
f, err := emb.Open("test.txt")
|
||||
assert.NoError(t, err)
|
||||
defer f.Close()
|
||||
}
|
||||
|
||||
func TestEmbed_ReadDir_Good(t *testing.T) {
|
||||
emb, _ := Mount(testFS, "testdata")
|
||||
entries, err := emb.ReadDir(".")
|
||||
assert.NoError(t, err)
|
||||
assert.NotEmpty(t, entries)
|
||||
}
|
||||
|
||||
func TestEmbed_Sub_Good(t *testing.T) {
|
||||
emb, _ := Mount(testFS, ".")
|
||||
sub, err := emb.Sub("testdata")
|
||||
assert.NoError(t, err)
|
||||
data, err := sub.ReadFile("test.txt")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello from testdata\n", string(data))
|
||||
}
|
||||
|
||||
func TestEmbed_BaseDir_Good(t *testing.T) {
|
||||
emb, _ := Mount(testFS, "testdata")
|
||||
assert.Equal(t, "testdata", emb.BaseDir())
|
||||
}
|
||||
|
||||
func TestEmbed_FS_Good(t *testing.T) {
|
||||
emb, _ := Mount(testFS, "testdata")
|
||||
assert.NotNil(t, emb.FS())
|
||||
}
|
||||
|
||||
func TestEmbed_EmbedFS_Good(t *testing.T) {
|
||||
emb, _ := Mount(testFS, "testdata")
|
||||
efs := emb.EmbedFS()
|
||||
// Should return the original embed.FS
|
||||
_, err := efs.ReadFile("testdata/test.txt")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// --- Extract (Template Directory) ---
|
||||
|
||||
func TestExtract_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
err := Extract(testFS, dir, nil)
|
||||
assert.NoError(t, err)
|
||||
|
||||
// testdata/test.txt should be extracted
|
||||
content, err := os.ReadFile(filepath.Join(dir, "testdata", "test.txt"))
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello from testdata\n", string(content))
|
||||
}
|
||||
|
||||
// --- Asset Pack (Build-time) ---
|
||||
|
||||
func TestAddGetAsset_Good(t *testing.T) {
|
||||
AddAsset("test-group", "greeting", mustCompress("hello world"))
|
||||
result, err := GetAsset("test-group", "greeting")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello world", result)
|
||||
}
|
||||
|
||||
func TestGetAsset_Bad(t *testing.T) {
|
||||
_, err := GetAsset("missing-group", "missing")
|
||||
assert.Error(t, err)
|
||||
|
||||
AddAsset("exists", "item", mustCompress("data"))
|
||||
_, err = GetAsset("exists", "missing-item")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestGetAssetBytes_Good(t *testing.T) {
|
||||
AddAsset("bytes-group", "file", mustCompress("binary content"))
|
||||
data, err := GetAssetBytes("bytes-group", "file")
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []byte("binary content"), data)
|
||||
}
|
||||
|
||||
// mustCompress is a test helper — compresses a string the way AddAsset expects.
|
||||
func mustCompress(input string) string {
|
||||
// AddAsset stores pre-compressed data. We need to compress it the same way.
|
||||
// Use the internal format: base64(gzip(input))
|
||||
var buf bytes.Buffer
|
||||
b64 := base64.NewEncoder(base64.StdEncoding, &buf)
|
||||
gz, _ := gzip.NewWriterLevel(b64, gzip.BestCompression)
|
||||
gz.Write([]byte(input))
|
||||
gz.Close()
|
||||
b64.Close()
|
||||
return buf.String()
|
||||
}
|
||||
196
tests/error_test.go
Normal file
196
tests/error_test.go
Normal file
|
|
@ -0,0 +1,196 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Error Creation ---
|
||||
|
||||
func TestE_Good(t *testing.T) {
|
||||
err := E("user.Save", "failed to save", nil)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "user.Save")
|
||||
assert.Contains(t, err.Error(), "failed to save")
|
||||
}
|
||||
|
||||
func TestE_WithCause_Good(t *testing.T) {
|
||||
cause := errors.New("connection refused")
|
||||
err := E("db.Connect", "database unavailable", cause)
|
||||
assert.ErrorIs(t, err, cause)
|
||||
}
|
||||
|
||||
func TestWrap_Good(t *testing.T) {
|
||||
cause := errors.New("timeout")
|
||||
err := Wrap(cause, "api.Call", "request failed")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, cause)
|
||||
}
|
||||
|
||||
func TestWrap_Nil_Good(t *testing.T) {
|
||||
err := Wrap(nil, "api.Call", "request failed")
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestWrapCode_Good(t *testing.T) {
|
||||
cause := errors.New("invalid email")
|
||||
err := WrapCode(cause, "VALIDATION_ERROR", "user.Validate", "bad input")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "VALIDATION_ERROR", ErrCode(err))
|
||||
}
|
||||
|
||||
func TestNewCode_Good(t *testing.T) {
|
||||
err := NewCode("NOT_FOUND", "resource not found")
|
||||
assert.Error(t, err)
|
||||
assert.Equal(t, "NOT_FOUND", ErrCode(err))
|
||||
}
|
||||
|
||||
// --- Error Introspection ---
|
||||
|
||||
func TestOp_Good(t *testing.T) {
|
||||
err := E("brain.Recall", "search failed", nil)
|
||||
assert.Equal(t, "brain.Recall", Op(err))
|
||||
}
|
||||
|
||||
func TestOp_Bad(t *testing.T) {
|
||||
err := errors.New("plain error")
|
||||
assert.Equal(t, "", Op(err))
|
||||
}
|
||||
|
||||
func TestErrorMessage_Good(t *testing.T) {
|
||||
err := E("op", "the message", nil)
|
||||
assert.Equal(t, "the message", ErrorMessage(err))
|
||||
}
|
||||
|
||||
func TestErrorMessage_Plain(t *testing.T) {
|
||||
err := errors.New("plain")
|
||||
assert.Equal(t, "plain", ErrorMessage(err))
|
||||
}
|
||||
|
||||
func TestErrorMessage_Nil(t *testing.T) {
|
||||
assert.Equal(t, "", ErrorMessage(nil))
|
||||
}
|
||||
|
||||
func TestRoot_Good(t *testing.T) {
|
||||
root := errors.New("root cause")
|
||||
wrapped := Wrap(root, "layer1", "first wrap")
|
||||
double := Wrap(wrapped, "layer2", "second wrap")
|
||||
assert.Equal(t, root, Root(double))
|
||||
}
|
||||
|
||||
func TestRoot_Nil(t *testing.T) {
|
||||
assert.Nil(t, Root(nil))
|
||||
}
|
||||
|
||||
func TestStackTrace_Good(t *testing.T) {
|
||||
err := Wrap(E("inner", "cause", nil), "outer", "wrapper")
|
||||
stack := StackTrace(err)
|
||||
assert.Len(t, stack, 2)
|
||||
assert.Equal(t, "outer", stack[0])
|
||||
assert.Equal(t, "inner", stack[1])
|
||||
}
|
||||
|
||||
func TestFormatStackTrace_Good(t *testing.T) {
|
||||
err := Wrap(E("a", "x", nil), "b", "y")
|
||||
formatted := FormatStackTrace(err)
|
||||
assert.Equal(t, "b -> a", formatted)
|
||||
}
|
||||
|
||||
// --- ErrorLog ---
|
||||
|
||||
func TestErrorLog_Good(t *testing.T) {
|
||||
c := New()
|
||||
cause := errors.New("boom")
|
||||
err := c.Log().Error(cause, "test.Op", "something broke")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, cause)
|
||||
}
|
||||
|
||||
func TestErrorLog_Nil_Good(t *testing.T) {
|
||||
c := New()
|
||||
err := c.Log().Error(nil, "test.Op", "no error")
|
||||
assert.Nil(t, err)
|
||||
}
|
||||
|
||||
func TestErrorLog_Warn_Good(t *testing.T) {
|
||||
c := New()
|
||||
cause := errors.New("warning")
|
||||
err := c.Log().Warn(cause, "test.Op", "heads up")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestErrorLog_Must_Ugly(t *testing.T) {
|
||||
c := New()
|
||||
assert.Panics(t, func() {
|
||||
c.Log().Must(errors.New("fatal"), "test.Op", "must fail")
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorLog_Must_Nil_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotPanics(t, func() {
|
||||
c.Log().Must(nil, "test.Op", "no error")
|
||||
})
|
||||
}
|
||||
|
||||
// --- ErrorPanic ---
|
||||
|
||||
func TestErrorPanic_Recover_Good(t *testing.T) {
|
||||
c := New()
|
||||
// Should not panic — Recover catches it
|
||||
assert.NotPanics(t, func() {
|
||||
defer c.Error().Recover()
|
||||
panic("test panic")
|
||||
})
|
||||
}
|
||||
|
||||
func TestErrorPanic_SafeGo_Good(t *testing.T) {
|
||||
c := New()
|
||||
done := make(chan bool, 1)
|
||||
c.Error().SafeGo(func() {
|
||||
done <- true
|
||||
})
|
||||
assert.True(t, <-done)
|
||||
}
|
||||
|
||||
func TestErrorPanic_SafeGo_Panic_Good(t *testing.T) {
|
||||
c := New()
|
||||
done := make(chan bool, 1)
|
||||
c.Error().SafeGo(func() {
|
||||
defer func() { done <- true }()
|
||||
panic("caught by SafeGo")
|
||||
})
|
||||
// SafeGo recovers — goroutine completes without crashing the process
|
||||
<-done
|
||||
}
|
||||
|
||||
// --- Standard Library Wrappers ---
|
||||
|
||||
func TestIs_Good(t *testing.T) {
|
||||
target := errors.New("target")
|
||||
wrapped := Wrap(target, "op", "msg")
|
||||
assert.True(t, Is(wrapped, target))
|
||||
}
|
||||
|
||||
func TestAs_Good(t *testing.T) {
|
||||
err := E("op", "msg", nil)
|
||||
var e *Err
|
||||
assert.True(t, As(err, &e))
|
||||
assert.Equal(t, "op", e.Op)
|
||||
}
|
||||
|
||||
func TestNewError_Good(t *testing.T) {
|
||||
err := NewError("simple error")
|
||||
assert.Equal(t, "simple error", err.Error())
|
||||
}
|
||||
|
||||
func TestJoin_Good(t *testing.T) {
|
||||
e1 := errors.New("first")
|
||||
e2 := errors.New("second")
|
||||
joined := Join(e1, e2)
|
||||
assert.ErrorIs(t, joined, e1)
|
||||
assert.ErrorIs(t, joined, e2)
|
||||
}
|
||||
211
tests/fs_test.go
Normal file
211
tests/fs_test.go
Normal file
|
|
@ -0,0 +1,211 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Fs (Sandboxed Filesystem) ---
|
||||
|
||||
func TestFs_WriteRead_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "test.txt")
|
||||
err := c.Fs().Write(path, "hello core")
|
||||
assert.NoError(t, err)
|
||||
|
||||
content, err := c.Fs().Read(path)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "hello core", content)
|
||||
}
|
||||
|
||||
func TestFs_Read_Bad(t *testing.T) {
|
||||
c := New()
|
||||
_, err := c.Fs().Read("/nonexistent/path/to/file.txt")
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestFs_EnsureDir_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
path := filepath.Join(dir, "sub", "dir")
|
||||
err := c.Fs().EnsureDir(path)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, c.Fs().IsDir(path))
|
||||
}
|
||||
|
||||
func TestFs_IsDir_Good(t *testing.T) {
|
||||
c := New()
|
||||
dir := t.TempDir()
|
||||
assert.True(t, c.Fs().IsDir(dir))
|
||||
assert.False(t, c.Fs().IsDir(filepath.Join(dir, "nonexistent")))
|
||||
assert.False(t, c.Fs().IsDir(""))
|
||||
}
|
||||
|
||||
func TestFs_IsFile_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "test.txt")
|
||||
c.Fs().Write(path, "data")
|
||||
|
||||
assert.True(t, c.Fs().IsFile(path))
|
||||
assert.False(t, c.Fs().IsFile(dir)) // dir, not file
|
||||
assert.False(t, c.Fs().IsFile(""))
|
||||
}
|
||||
|
||||
func TestFs_Exists_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "exists.txt")
|
||||
c.Fs().Write(path, "yes")
|
||||
|
||||
assert.True(t, c.Fs().Exists(path))
|
||||
assert.True(t, c.Fs().Exists(dir))
|
||||
assert.False(t, c.Fs().Exists(filepath.Join(dir, "nope")))
|
||||
}
|
||||
|
||||
func TestFs_List_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
c.Fs().Write(filepath.Join(dir, "a.txt"), "a")
|
||||
c.Fs().Write(filepath.Join(dir, "b.txt"), "b")
|
||||
|
||||
entries, err := c.Fs().List(dir)
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, entries, 2)
|
||||
}
|
||||
|
||||
func TestFs_Stat_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "stat.txt")
|
||||
c.Fs().Write(path, "data")
|
||||
|
||||
info, err := c.Fs().Stat(path)
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, "stat.txt", info.Name())
|
||||
}
|
||||
|
||||
func TestFs_Open_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "open.txt")
|
||||
c.Fs().Write(path, "content")
|
||||
|
||||
file, err := c.Fs().Open(path)
|
||||
assert.NoError(t, err)
|
||||
file.Close()
|
||||
}
|
||||
|
||||
func TestFs_Create_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "sub", "created.txt")
|
||||
w, err := c.Fs().Create(path)
|
||||
assert.NoError(t, err)
|
||||
w.Write([]byte("hello"))
|
||||
w.Close()
|
||||
|
||||
content, _ := c.Fs().Read(path)
|
||||
assert.Equal(t, "hello", content)
|
||||
}
|
||||
|
||||
func TestFs_Append_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "append.txt")
|
||||
c.Fs().Write(path, "first")
|
||||
|
||||
w, err := c.Fs().Append(path)
|
||||
assert.NoError(t, err)
|
||||
w.Write([]byte(" second"))
|
||||
w.Close()
|
||||
|
||||
content, _ := c.Fs().Read(path)
|
||||
assert.Equal(t, "first second", content)
|
||||
}
|
||||
|
||||
func TestFs_ReadStream_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "stream.txt")
|
||||
c.Fs().Write(path, "streamed")
|
||||
|
||||
r, err := c.Fs().ReadStream(path)
|
||||
assert.NoError(t, err)
|
||||
r.Close()
|
||||
}
|
||||
|
||||
func TestFs_WriteStream_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "sub", "ws.txt")
|
||||
w, err := c.Fs().WriteStream(path)
|
||||
assert.NoError(t, err)
|
||||
w.Write([]byte("stream"))
|
||||
w.Close()
|
||||
}
|
||||
|
||||
func TestFs_Delete_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "delete.txt")
|
||||
c.Fs().Write(path, "gone")
|
||||
|
||||
err := c.Fs().Delete(path)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, c.Fs().Exists(path))
|
||||
}
|
||||
|
||||
func TestFs_DeleteAll_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
sub := filepath.Join(dir, "deep", "nested")
|
||||
c.Fs().EnsureDir(sub)
|
||||
c.Fs().Write(filepath.Join(sub, "file.txt"), "data")
|
||||
|
||||
err := c.Fs().DeleteAll(filepath.Join(dir, "deep"))
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, c.Fs().Exists(filepath.Join(dir, "deep")))
|
||||
}
|
||||
|
||||
func TestFs_Rename_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
old := filepath.Join(dir, "old.txt")
|
||||
new := filepath.Join(dir, "new.txt")
|
||||
c.Fs().Write(old, "data")
|
||||
|
||||
err := c.Fs().Rename(old, new)
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, c.Fs().Exists(old))
|
||||
assert.True(t, c.Fs().Exists(new))
|
||||
}
|
||||
|
||||
func TestFs_WriteMode_Good(t *testing.T) {
|
||||
dir := t.TempDir()
|
||||
c := New()
|
||||
|
||||
path := filepath.Join(dir, "secret.txt")
|
||||
err := c.Fs().WriteMode(path, "secret", 0600)
|
||||
assert.NoError(t, err)
|
||||
|
||||
info, _ := c.Fs().Stat(path)
|
||||
assert.Equal(t, "secret.txt", info.Name())
|
||||
}
|
||||
|
|
@ -1,104 +0,0 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// FuzzE exercises the E() error constructor with arbitrary input.
|
||||
func FuzzE(f *testing.F) {
|
||||
f.Add("svc.Method", "something broke", true)
|
||||
f.Add("", "", false)
|
||||
f.Add("a.b.c.d.e.f", "unicode: \u00e9\u00e8\u00ea", true)
|
||||
|
||||
f.Fuzz(func(t *testing.T, op, msg string, withErr bool) {
|
||||
var underlying error
|
||||
if withErr {
|
||||
underlying = errors.New("wrapped")
|
||||
}
|
||||
|
||||
e := E(op, msg, underlying)
|
||||
if e == nil {
|
||||
t.Fatal("E() returned nil")
|
||||
}
|
||||
|
||||
s := e.Error()
|
||||
if s == "" && (op != "" || msg != "") {
|
||||
t.Fatal("Error() returned empty string for non-empty op/msg")
|
||||
}
|
||||
|
||||
// Round-trip: Unwrap should return the underlying error
|
||||
var coreErr *Err
|
||||
if !errors.As(e, &coreErr) {
|
||||
t.Fatal("errors.As failed for *Err")
|
||||
}
|
||||
if withErr && coreErr.Unwrap() == nil {
|
||||
t.Fatal("Unwrap() returned nil with underlying error")
|
||||
}
|
||||
if !withErr && coreErr.Unwrap() != nil {
|
||||
t.Fatal("Unwrap() returned non-nil without underlying error")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzServiceRegistration exercises service registration with arbitrary names.
|
||||
func FuzzServiceRegistration(f *testing.F) {
|
||||
f.Add("myservice")
|
||||
f.Add("")
|
||||
f.Add("a/b/c")
|
||||
f.Add("service with spaces")
|
||||
f.Add("service\x00null")
|
||||
|
||||
f.Fuzz(func(t *testing.T, name string) {
|
||||
c, _ := New()
|
||||
|
||||
err := c.RegisterService(name, struct{}{})
|
||||
if name == "" {
|
||||
if err == nil {
|
||||
t.Fatal("expected error for empty name")
|
||||
}
|
||||
return
|
||||
}
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for name %q: %v", name, err)
|
||||
}
|
||||
|
||||
// Retrieve should return the same service
|
||||
got := c.Service(name)
|
||||
if got == nil {
|
||||
t.Fatalf("service %q not found after registration", name)
|
||||
}
|
||||
|
||||
// Duplicate registration should fail
|
||||
err = c.RegisterService(name, struct{}{})
|
||||
if err == nil {
|
||||
t.Fatalf("expected duplicate error for name %q", name)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// FuzzMessageDispatch exercises action dispatch with concurrent registrations.
|
||||
func FuzzMessageDispatch(f *testing.F) {
|
||||
f.Add("hello")
|
||||
f.Add("")
|
||||
f.Add("test\nmultiline")
|
||||
|
||||
f.Fuzz(func(t *testing.T, payload string) {
|
||||
c, _ := New()
|
||||
|
||||
var received string
|
||||
c.IPC().RegisterAction(func(_ *Core, msg Message) error {
|
||||
received = msg.(string)
|
||||
return nil
|
||||
})
|
||||
|
||||
err := c.IPC().Action(payload)
|
||||
if err != nil {
|
||||
t.Fatalf("action dispatch failed: %v", err)
|
||||
}
|
||||
if received != payload {
|
||||
t.Fatalf("got %q, want %q", received, payload)
|
||||
}
|
||||
})
|
||||
}
|
||||
26
tests/i18n_test.go
Normal file
26
tests/i18n_test.go
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestI18n_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotNil(t, c.I18n())
|
||||
}
|
||||
|
||||
func TestI18n_AddLocales_Good(t *testing.T) {
|
||||
c := New()
|
||||
// AddLocales takes *Embed mounts — mount testdata and add it
|
||||
r := c.Data().New(Options{
|
||||
{K: "name", V: "lang"},
|
||||
{K: "source", V: testFS},
|
||||
{K: "path", V: "testdata"},
|
||||
})
|
||||
if r.OK {
|
||||
c.I18n().AddLocales(r.Value)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,121 +1,97 @@
|
|||
package core_test
|
||||
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type IPCTestQuery struct{ Value string }
|
||||
type IPCTestTask struct{ Value string }
|
||||
// --- IPC: Actions ---
|
||||
|
||||
func TestIPC_Query(t *testing.T) {
|
||||
c, _ := New()
|
||||
type testMessage struct{ payload string }
|
||||
|
||||
// No handler
|
||||
res, handled, err := c.QUERY(IPCTestQuery{})
|
||||
assert.False(t, handled)
|
||||
assert.Nil(t, res)
|
||||
assert.Nil(t, err)
|
||||
|
||||
// With handler
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
if tq, ok := q.(IPCTestQuery); ok {
|
||||
return tq.Value + "-response", true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
})
|
||||
|
||||
res, handled, err = c.QUERY(IPCTestQuery{Value: "test"})
|
||||
assert.True(t, handled)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "test-response", res)
|
||||
}
|
||||
|
||||
func TestIPC_QueryAll(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "h1", true, nil
|
||||
})
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "h2", true, nil
|
||||
})
|
||||
|
||||
results, err := c.QUERYALL(IPCTestQuery{})
|
||||
assert.Nil(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
assert.Contains(t, results, "h1")
|
||||
assert.Contains(t, results, "h2")
|
||||
}
|
||||
|
||||
func TestIPC_Perform(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
c.RegisterTask(func(c *Core, task Task) (any, bool, error) {
|
||||
if tt, ok := task.(IPCTestTask); ok {
|
||||
if tt.Value == "error" {
|
||||
return nil, true, errors.New("task error")
|
||||
}
|
||||
return "done", true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
})
|
||||
|
||||
// Success
|
||||
res, handled, err := c.PERFORM(IPCTestTask{Value: "run"})
|
||||
assert.True(t, handled)
|
||||
assert.Nil(t, err)
|
||||
assert.Equal(t, "done", res)
|
||||
|
||||
// Error
|
||||
res, handled, err = c.PERFORM(IPCTestTask{Value: "error"})
|
||||
assert.True(t, handled)
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, res)
|
||||
}
|
||||
|
||||
func TestIPC_PerformAsync(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
type AsyncResult struct {
|
||||
TaskID string
|
||||
Result any
|
||||
Error error
|
||||
}
|
||||
done := make(chan AsyncResult, 1)
|
||||
|
||||
c.RegisterTask(func(c *Core, task Task) (any, bool, error) {
|
||||
if tt, ok := task.(IPCTestTask); ok {
|
||||
return tt.Value + "-done", true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
})
|
||||
|
||||
c.RegisterAction(func(c *Core, msg Message) error {
|
||||
if m, ok := msg.(ActionTaskCompleted); ok {
|
||||
done <- AsyncResult{
|
||||
TaskID: m.TaskID,
|
||||
Result: m.Result,
|
||||
Error: m.Error,
|
||||
}
|
||||
}
|
||||
func TestAction_Good(t *testing.T) {
|
||||
c := New()
|
||||
var received Message
|
||||
c.RegisterAction(func(_ *Core, msg Message) error {
|
||||
received = msg
|
||||
return nil
|
||||
})
|
||||
|
||||
taskID := c.PerformAsync(IPCTestTask{Value: "async"})
|
||||
assert.NotEmpty(t, taskID)
|
||||
|
||||
select {
|
||||
case res := <-done:
|
||||
assert.Equal(t, taskID, res.TaskID)
|
||||
assert.Equal(t, "async-done", res.Result)
|
||||
assert.Nil(t, res.Error)
|
||||
case <-time.After(time.Second):
|
||||
t.Fatal("timed out waiting for task completion")
|
||||
}
|
||||
err := c.ACTION(testMessage{payload: "hello"})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, testMessage{payload: "hello"}, received)
|
||||
}
|
||||
|
||||
func TestAction_Multiple_Good(t *testing.T) {
|
||||
c := New()
|
||||
count := 0
|
||||
handler := func(_ *Core, _ Message) error { count++; return nil }
|
||||
c.RegisterActions(handler, handler, handler)
|
||||
_ = c.ACTION(nil)
|
||||
assert.Equal(t, 3, count)
|
||||
}
|
||||
|
||||
func TestAction_None_Good(t *testing.T) {
|
||||
c := New()
|
||||
// No handlers registered — should not error
|
||||
err := c.ACTION(nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
// --- IPC: Queries ---
|
||||
|
||||
func TestQuery_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterQuery(func(_ *Core, q Query) (any, bool, error) {
|
||||
if q == "ping" {
|
||||
return "pong", true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
})
|
||||
result, handled, err := c.QUERY("ping")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "pong", result)
|
||||
}
|
||||
|
||||
func TestQuery_Unhandled_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterQuery(func(_ *Core, q Query) (any, bool, error) {
|
||||
return nil, false, nil
|
||||
})
|
||||
_, handled, err := c.QUERY("unknown")
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, handled)
|
||||
}
|
||||
|
||||
func TestQueryAll_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterQuery(func(_ *Core, _ Query) (any, bool, error) {
|
||||
return "a", true, nil
|
||||
})
|
||||
c.RegisterQuery(func(_ *Core, _ Query) (any, bool, error) {
|
||||
return "b", true, nil
|
||||
})
|
||||
results, err := c.QUERYALL("anything")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
assert.Contains(t, results, "a")
|
||||
assert.Contains(t, results, "b")
|
||||
}
|
||||
|
||||
// --- IPC: Tasks ---
|
||||
|
||||
func TestPerform_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterTask(func(_ *Core, t Task) (any, bool, error) {
|
||||
if t == "compute" {
|
||||
return 42, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
})
|
||||
result, handled, err := c.PERFORM("compute")
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, 42, result)
|
||||
}
|
||||
|
|
|
|||
69
tests/lock_test.go
Normal file
69
tests/lock_test.go
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Lock (Named Mutexes) ---
|
||||
|
||||
func TestLock_Good(t *testing.T) {
|
||||
c := New()
|
||||
lock := c.Lock("test")
|
||||
assert.NotNil(t, lock)
|
||||
assert.NotNil(t, lock.Mu)
|
||||
}
|
||||
|
||||
func TestLock_SameName_Good(t *testing.T) {
|
||||
c := New()
|
||||
l1 := c.Lock("shared")
|
||||
l2 := c.Lock("shared")
|
||||
// Same name returns same lock
|
||||
assert.Equal(t, l1, l2)
|
||||
}
|
||||
|
||||
func TestLock_DifferentName_Good(t *testing.T) {
|
||||
c := New()
|
||||
l1 := c.Lock("a")
|
||||
l2 := c.Lock("b")
|
||||
assert.NotEqual(t, l1, l2)
|
||||
}
|
||||
|
||||
func TestLock_MutexWorks_Good(t *testing.T) {
|
||||
c := New()
|
||||
lock := c.Lock("counter")
|
||||
counter := 0
|
||||
lock.Mu.Lock()
|
||||
counter++
|
||||
lock.Mu.Unlock()
|
||||
assert.Equal(t, 1, counter)
|
||||
}
|
||||
|
||||
func TestLockEnable_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Service("early", struct{}{})
|
||||
c.LockEnable()
|
||||
c.LockApply()
|
||||
|
||||
// After lock, registration should fail
|
||||
result := c.Service("late", struct{}{})
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestStartables_Good(t *testing.T) {
|
||||
c := New()
|
||||
svc := &testService{name: "s"}
|
||||
c.Service("s", svc)
|
||||
startables := c.Startables()
|
||||
assert.Len(t, startables, 1)
|
||||
}
|
||||
|
||||
func TestStoppables_Good(t *testing.T) {
|
||||
c := New()
|
||||
svc := &testService{name: "s"}
|
||||
c.Service("s", svc)
|
||||
stoppables := c.Stoppables()
|
||||
assert.Len(t, stoppables, 1)
|
||||
}
|
||||
37
tests/log_test.go
Normal file
37
tests/log_test.go
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Log (Structured Logger) ---
|
||||
|
||||
func TestLog_New_Good(t *testing.T) {
|
||||
l := NewLog(LogOpts{Level: LevelInfo})
|
||||
assert.NotNil(t, l)
|
||||
}
|
||||
|
||||
func TestLog_Levels_Good(t *testing.T) {
|
||||
for _, level := range []Level{LevelDebug, LevelInfo, LevelWarn, LevelError} {
|
||||
l := NewLog(LogOpts{Level: level})
|
||||
l.Debug("debug msg")
|
||||
l.Info("info msg")
|
||||
l.Warn("warn msg")
|
||||
l.Error("error msg")
|
||||
}
|
||||
}
|
||||
|
||||
func TestLog_CoreLog_Good(t *testing.T) {
|
||||
c := New()
|
||||
assert.NotNil(t, c.Log())
|
||||
}
|
||||
|
||||
func TestLog_ErrorSink_Interface(t *testing.T) {
|
||||
l := NewLog(LogOpts{Level: LevelInfo})
|
||||
var sink ErrorSink = l
|
||||
sink.Error("test", "key", "val")
|
||||
sink.Warn("test", "key", "val")
|
||||
}
|
||||
|
|
@ -1,176 +0,0 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"errors"
|
||||
"sync"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestBus_Action_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
var received []Message
|
||||
c.IPC().RegisterAction(func(_ *Core, msg Message) error {
|
||||
received = append(received, msg)
|
||||
return nil
|
||||
})
|
||||
c.IPC().RegisterAction(func(_ *Core, msg Message) error {
|
||||
received = append(received, msg)
|
||||
return nil
|
||||
})
|
||||
|
||||
err := c.IPC().Action("hello")
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, received, 2)
|
||||
}
|
||||
|
||||
func TestBus_Action_Bad(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
err1 := errors.New("handler1 failed")
|
||||
err2 := errors.New("handler2 failed")
|
||||
|
||||
c.IPC().RegisterAction(func(_ *Core, msg Message) error { return err1 })
|
||||
c.IPC().RegisterAction(func(_ *Core, msg Message) error { return nil })
|
||||
c.IPC().RegisterAction(func(_ *Core, msg Message) error { return err2 })
|
||||
|
||||
err := c.IPC().Action("test")
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, err1)
|
||||
assert.ErrorIs(t, err, err2)
|
||||
}
|
||||
|
||||
func TestBus_RegisterAction_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
var coreRef *Core
|
||||
c.IPC().RegisterAction(func(core *Core, msg Message) error {
|
||||
coreRef = core
|
||||
return nil
|
||||
})
|
||||
|
||||
_ = c.IPC().Action(nil)
|
||||
assert.Same(t, c, coreRef, "handler should receive the Core reference")
|
||||
}
|
||||
|
||||
func TestBus_Query_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
c.IPC().RegisterQuery(func(_ *Core, q Query) (any, bool, error) {
|
||||
return "first", true, nil
|
||||
})
|
||||
|
||||
result, handled, err := c.IPC().Query(TestQuery{Value: "test"})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "first", result)
|
||||
}
|
||||
|
||||
func TestBus_QueryAll_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
c.IPC().RegisterQuery(func(_ *Core, q Query) (any, bool, error) {
|
||||
return "a", true, nil
|
||||
})
|
||||
c.IPC().RegisterQuery(func(_ *Core, q Query) (any, bool, error) {
|
||||
return nil, false, nil // skips
|
||||
})
|
||||
c.IPC().RegisterQuery(func(_ *Core, q Query) (any, bool, error) {
|
||||
return "b", true, nil
|
||||
})
|
||||
|
||||
results, err := c.IPC().QueryAll(TestQuery{})
|
||||
assert.NoError(t, err)
|
||||
assert.Equal(t, []any{"a", "b"}, results)
|
||||
}
|
||||
|
||||
func TestBus_Perform_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
c.IPC().RegisterTask(func(_ *Core, t Task) (any, bool, error) {
|
||||
return "done", true, nil
|
||||
})
|
||||
|
||||
result, handled, err := c.IPC().Perform(TestTask{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "done", result)
|
||||
}
|
||||
|
||||
func TestBus_ConcurrentAccess_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
const goroutines = 20
|
||||
|
||||
// Concurrent register + dispatch
|
||||
for i := 0; i < goroutines; i++ {
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
c.IPC().RegisterAction(func(_ *Core, msg Message) error { return nil })
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_ = c.IPC().Action("ping")
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < goroutines; i++ {
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
c.IPC().RegisterQuery(func(_ *Core, q Query) (any, bool, error) { return nil, false, nil })
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _ = c.IPC().QueryAll(TestQuery{})
|
||||
}()
|
||||
}
|
||||
|
||||
for i := 0; i < goroutines; i++ {
|
||||
wg.Add(2)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
c.IPC().RegisterTask(func(_ *Core, t Task) (any, bool, error) { return nil, false, nil })
|
||||
}()
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
_, _, _ = c.IPC().Perform(TestTask{})
|
||||
}()
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
func TestBus_Action_NoHandlers(t *testing.T) {
|
||||
c, _ := New()
|
||||
err := c.IPC().Action("no one listening")
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestBus_Query_NoHandlers(t *testing.T) {
|
||||
c, _ := New()
|
||||
result, handled, err := c.IPC().Query(TestQuery{})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, handled)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestBus_QueryAll_NoHandlers(t *testing.T) {
|
||||
c, _ := New()
|
||||
results, err := c.IPC().QueryAll(TestQuery{})
|
||||
assert.NoError(t, err)
|
||||
assert.Empty(t, results)
|
||||
}
|
||||
|
||||
func TestBus_Perform_NoHandlers(t *testing.T) {
|
||||
c, _ := New()
|
||||
result, handled, err := c.IPC().Perform(TestTask{})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, handled)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
94
tests/options_test.go
Normal file
94
tests/options_test.go
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- Option / Options ---
|
||||
|
||||
func TestOptions_Get_Good(t *testing.T) {
|
||||
opts := Options{
|
||||
{K: "name", V: "brain"},
|
||||
{K: "port", V: 8080},
|
||||
}
|
||||
val, ok := opts.Get("name")
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "brain", val)
|
||||
}
|
||||
|
||||
func TestOptions_Get_Bad(t *testing.T) {
|
||||
opts := Options{{K: "name", V: "brain"}}
|
||||
val, ok := opts.Get("missing")
|
||||
assert.False(t, ok)
|
||||
assert.Nil(t, val)
|
||||
}
|
||||
|
||||
func TestOptions_Has_Good(t *testing.T) {
|
||||
opts := Options{{K: "debug", V: true}}
|
||||
assert.True(t, opts.Has("debug"))
|
||||
assert.False(t, opts.Has("missing"))
|
||||
}
|
||||
|
||||
func TestOptions_String_Good(t *testing.T) {
|
||||
opts := Options{{K: "name", V: "brain"}}
|
||||
assert.Equal(t, "brain", opts.String("name"))
|
||||
}
|
||||
|
||||
func TestOptions_String_Bad(t *testing.T) {
|
||||
opts := Options{{K: "port", V: 8080}}
|
||||
// Wrong type — returns empty string
|
||||
assert.Equal(t, "", opts.String("port"))
|
||||
// Missing key — returns empty string
|
||||
assert.Equal(t, "", opts.String("missing"))
|
||||
}
|
||||
|
||||
func TestOptions_Int_Good(t *testing.T) {
|
||||
opts := Options{{K: "port", V: 8080}}
|
||||
assert.Equal(t, 8080, opts.Int("port"))
|
||||
}
|
||||
|
||||
func TestOptions_Int_Bad(t *testing.T) {
|
||||
opts := Options{{K: "name", V: "brain"}}
|
||||
assert.Equal(t, 0, opts.Int("name"))
|
||||
assert.Equal(t, 0, opts.Int("missing"))
|
||||
}
|
||||
|
||||
func TestOptions_Bool_Good(t *testing.T) {
|
||||
opts := Options{{K: "debug", V: true}}
|
||||
assert.True(t, opts.Bool("debug"))
|
||||
}
|
||||
|
||||
func TestOptions_Bool_Bad(t *testing.T) {
|
||||
opts := Options{{K: "name", V: "brain"}}
|
||||
assert.False(t, opts.Bool("name"))
|
||||
assert.False(t, opts.Bool("missing"))
|
||||
}
|
||||
|
||||
func TestOptions_TypedStruct_Good(t *testing.T) {
|
||||
// Packages plug typed structs into Option.V
|
||||
type BrainConfig struct {
|
||||
Name string
|
||||
OllamaURL string
|
||||
Collection string
|
||||
}
|
||||
cfg := BrainConfig{Name: "brain", OllamaURL: "http://localhost:11434", Collection: "openbrain"}
|
||||
opts := Options{{K: "config", V: cfg}}
|
||||
|
||||
val, ok := opts.Get("config")
|
||||
assert.True(t, ok)
|
||||
bc, ok := val.(BrainConfig)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "brain", bc.Name)
|
||||
assert.Equal(t, "http://localhost:11434", bc.OllamaURL)
|
||||
}
|
||||
|
||||
func TestOptions_Empty_Good(t *testing.T) {
|
||||
opts := Options{}
|
||||
assert.False(t, opts.Has("anything"))
|
||||
assert.Equal(t, "", opts.String("anything"))
|
||||
assert.Equal(t, 0, opts.Int("anything"))
|
||||
assert.False(t, opts.Bool("anything"))
|
||||
}
|
||||
|
|
@ -1,203 +0,0 @@
|
|||
package core_test
|
||||
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type TestQuery struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
type TestTask struct {
|
||||
Value string
|
||||
}
|
||||
|
||||
func TestCore_QUERY_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Register a handler that responds to TestQuery
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
if tq, ok := q.(TestQuery); ok {
|
||||
return "result-" + tq.Value, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
})
|
||||
|
||||
result, handled, err := c.QUERY(TestQuery{Value: "test"})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "result-test", result)
|
||||
}
|
||||
|
||||
func TestCore_QUERY_NotHandled(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// No handlers registered
|
||||
result, handled, err := c.QUERY(TestQuery{Value: "test"})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, handled)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestCore_QUERY_FirstResponder(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// First handler responds
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "first", true, nil
|
||||
})
|
||||
|
||||
// Second handler would respond but shouldn't be called
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "second", true, nil
|
||||
})
|
||||
|
||||
result, handled, err := c.QUERY(TestQuery{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "first", result)
|
||||
}
|
||||
|
||||
func TestCore_QUERY_SkipsNonHandlers(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// First handler doesn't handle
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return nil, false, nil
|
||||
})
|
||||
|
||||
// Second handler responds
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "second", true, nil
|
||||
})
|
||||
|
||||
result, handled, err := c.QUERY(TestQuery{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "second", result)
|
||||
}
|
||||
|
||||
func TestCore_QUERYALL_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// Multiple handlers respond
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "first", true, nil
|
||||
})
|
||||
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "second", true, nil
|
||||
})
|
||||
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return nil, false, nil // Doesn't handle
|
||||
})
|
||||
|
||||
results, err := c.QUERYALL(TestQuery{})
|
||||
assert.NoError(t, err)
|
||||
assert.Len(t, results, 2)
|
||||
assert.Contains(t, results, "first")
|
||||
assert.Contains(t, results, "second")
|
||||
}
|
||||
|
||||
func TestCore_QUERYALL_AggregatesErrors(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
err1 := errors.New("error1")
|
||||
err2 := errors.New("error2")
|
||||
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "result1", true, err1
|
||||
})
|
||||
|
||||
c.RegisterQuery(func(c *Core, q Query) (any, bool, error) {
|
||||
return "result2", true, err2
|
||||
})
|
||||
|
||||
results, err := c.QUERYALL(TestQuery{})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, err1)
|
||||
assert.ErrorIs(t, err, err2)
|
||||
assert.Len(t, results, 2)
|
||||
}
|
||||
|
||||
func TestCore_PERFORM_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
executed := false
|
||||
c.RegisterTask(func(c *Core, t Task) (any, bool, error) {
|
||||
if tt, ok := t.(TestTask); ok {
|
||||
executed = true
|
||||
return "done-" + tt.Value, true, nil
|
||||
}
|
||||
return nil, false, nil
|
||||
})
|
||||
|
||||
result, handled, err := c.PERFORM(TestTask{Value: "work"})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.True(t, executed)
|
||||
assert.Equal(t, "done-work", result)
|
||||
}
|
||||
|
||||
func TestCore_PERFORM_NotHandled(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
// No handlers registered
|
||||
result, handled, err := c.PERFORM(TestTask{Value: "work"})
|
||||
assert.NoError(t, err)
|
||||
assert.False(t, handled)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
||||
func TestCore_PERFORM_FirstResponder(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
callCount := 0
|
||||
|
||||
c.RegisterTask(func(c *Core, t Task) (any, bool, error) {
|
||||
callCount++
|
||||
return "first", true, nil
|
||||
})
|
||||
|
||||
c.RegisterTask(func(c *Core, t Task) (any, bool, error) {
|
||||
callCount++
|
||||
return "second", true, nil
|
||||
})
|
||||
|
||||
result, handled, err := c.PERFORM(TestTask{})
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, handled)
|
||||
assert.Equal(t, "first", result)
|
||||
assert.Equal(t, 1, callCount) // Only first handler called
|
||||
}
|
||||
|
||||
func TestCore_PERFORM_WithError(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
expectedErr := errors.New("task failed")
|
||||
c.RegisterTask(func(c *Core, t Task) (any, bool, error) {
|
||||
return nil, true, expectedErr
|
||||
})
|
||||
|
||||
result, handled, err := c.PERFORM(TestTask{})
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, expectedErr)
|
||||
assert.True(t, handled)
|
||||
assert.Nil(t, result)
|
||||
}
|
||||
|
|
@ -1,20 +0,0 @@
|
|||
package core_test
|
||||
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewWithFactories_EmptyName(t *testing.T) {
|
||||
factories := map[string]ServiceFactory{
|
||||
"": func() (any, error) {
|
||||
return &MockService{Name: "test"}, nil
|
||||
},
|
||||
}
|
||||
_, err := NewWithFactories(nil, factories)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "service name cannot be empty")
|
||||
}
|
||||
|
|
@ -1,129 +0,0 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestNewRuntime(t *testing.T) {
|
||||
testCases := []struct {
|
||||
name string
|
||||
app any
|
||||
factories map[string]ServiceFactory
|
||||
expectErr bool
|
||||
expectErrStr string
|
||||
checkRuntime func(*testing.T, *Runtime)
|
||||
}{
|
||||
{
|
||||
name: "Good path",
|
||||
app: nil,
|
||||
factories: map[string]ServiceFactory{},
|
||||
expectErr: false,
|
||||
checkRuntime: func(t *testing.T, rt *Runtime) {
|
||||
assert.NotNil(t, rt)
|
||||
assert.NotNil(t, rt.Core)
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "With non-nil app",
|
||||
app: &mockApp{},
|
||||
factories: map[string]ServiceFactory{},
|
||||
expectErr: false,
|
||||
checkRuntime: func(t *testing.T, rt *Runtime) {
|
||||
assert.NotNil(t, rt)
|
||||
assert.NotNil(t, rt.Core)
|
||||
assert.NotNil(t, rt.Core.App)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
rt, err := NewRuntime(tc.app)
|
||||
|
||||
if tc.expectErr {
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), tc.expectErrStr)
|
||||
assert.Nil(t, rt)
|
||||
} else {
|
||||
assert.NoError(t, err)
|
||||
if tc.checkRuntime != nil {
|
||||
tc.checkRuntime(t, rt)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNewWithFactories_Good(t *testing.T) {
|
||||
factories := map[string]ServiceFactory{
|
||||
"test": func() (any, error) {
|
||||
return &MockService{Name: "test"}, nil
|
||||
},
|
||||
}
|
||||
rt, err := NewWithFactories(nil, factories)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, rt)
|
||||
svc := rt.Core.Service("test")
|
||||
assert.NotNil(t, svc)
|
||||
mockSvc, ok := svc.(*MockService)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "test", mockSvc.Name)
|
||||
}
|
||||
|
||||
func TestNewWithFactories_Bad(t *testing.T) {
|
||||
factories := map[string]ServiceFactory{
|
||||
"test": func() (any, error) {
|
||||
return nil, assert.AnError
|
||||
},
|
||||
}
|
||||
_, err := NewWithFactories(nil, factories)
|
||||
assert.Error(t, err)
|
||||
assert.ErrorIs(t, err, assert.AnError)
|
||||
}
|
||||
|
||||
func TestNewWithFactories_Ugly(t *testing.T) {
|
||||
factories := map[string]ServiceFactory{
|
||||
"test": nil,
|
||||
}
|
||||
_, err := NewWithFactories(nil, factories)
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "factory is nil")
|
||||
}
|
||||
|
||||
func TestRuntime_Lifecycle_Good(t *testing.T) {
|
||||
rt, err := NewRuntime(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, rt)
|
||||
|
||||
// ServiceName
|
||||
assert.Equal(t, "Core", rt.ServiceName())
|
||||
|
||||
// ServiceStartup & ServiceShutdown
|
||||
// These are simple wrappers around the core methods, which are tested in core_test.go.
|
||||
// We call them here to ensure coverage.
|
||||
rt.ServiceStartup(context.TODO(), nil)
|
||||
rt.ServiceShutdown(context.TODO())
|
||||
|
||||
// Test shutdown with nil core
|
||||
rt.Core = nil
|
||||
rt.ServiceShutdown(context.TODO())
|
||||
}
|
||||
|
||||
func TestNewServiceRuntime_Good(t *testing.T) {
|
||||
c, err := New()
|
||||
assert.NoError(t, err)
|
||||
|
||||
sr := NewServiceRuntime(c, "test options")
|
||||
assert.NotNil(t, sr)
|
||||
assert.Equal(t, c, sr.Core())
|
||||
|
||||
// We can't directly test sr.Cfg() without a registered config service,
|
||||
// as it will panic.
|
||||
assert.Panics(t, func() {
|
||||
sr.Config()
|
||||
})
|
||||
}
|
||||
97
tests/runtime_test.go
Normal file
97
tests/runtime_test.go
Normal file
|
|
@ -0,0 +1,97 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- ServiceRuntime ---
|
||||
|
||||
type testOpts struct {
|
||||
URL string
|
||||
Timeout int
|
||||
}
|
||||
|
||||
type runtimeService struct {
|
||||
*ServiceRuntime[testOpts]
|
||||
}
|
||||
|
||||
func TestServiceRuntime_Good(t *testing.T) {
|
||||
c := New()
|
||||
opts := testOpts{URL: "https://api.lthn.ai", Timeout: 30}
|
||||
rt := NewServiceRuntime(c, opts)
|
||||
|
||||
assert.Equal(t, c, rt.Core())
|
||||
assert.Equal(t, opts, rt.Opts())
|
||||
assert.Equal(t, "https://api.lthn.ai", rt.Opts().URL)
|
||||
assert.Equal(t, 30, rt.Opts().Timeout)
|
||||
assert.NotNil(t, rt.Config())
|
||||
}
|
||||
|
||||
func TestServiceRuntime_Embedded_Good(t *testing.T) {
|
||||
c := New()
|
||||
svc := &runtimeService{
|
||||
ServiceRuntime: NewServiceRuntime(c, testOpts{URL: "https://lthn.sh"}),
|
||||
}
|
||||
assert.Equal(t, "https://lthn.sh", svc.Opts().URL)
|
||||
}
|
||||
|
||||
// --- NewWithFactories ---
|
||||
|
||||
func TestNewWithFactories_Good(t *testing.T) {
|
||||
rt, err := NewWithFactories(nil, map[string]ServiceFactory{
|
||||
"svc1": func() (any, error) { return &testService{name: "one"}, nil },
|
||||
"svc2": func() (any, error) { return &testService{name: "two"}, nil },
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, rt)
|
||||
assert.NotNil(t, rt.Core)
|
||||
|
||||
svc := rt.Core.Service("svc1")
|
||||
assert.NotNil(t, svc)
|
||||
ts, ok := svc.(*testService)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "one", ts.name)
|
||||
}
|
||||
|
||||
func TestNewWithFactories_Bad(t *testing.T) {
|
||||
// Nil factory
|
||||
_, err := NewWithFactories(nil, map[string]ServiceFactory{
|
||||
"bad": nil,
|
||||
})
|
||||
assert.Error(t, err)
|
||||
|
||||
// Factory returns error
|
||||
_, err = NewWithFactories(nil, map[string]ServiceFactory{
|
||||
"fail": func() (any, error) { return nil, errors.New("factory failed") },
|
||||
})
|
||||
assert.Error(t, err)
|
||||
}
|
||||
|
||||
func TestNewRuntime_Good(t *testing.T) {
|
||||
rt, err := NewRuntime(nil)
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, rt)
|
||||
}
|
||||
|
||||
// --- Lifecycle via Runtime ---
|
||||
|
||||
func TestRuntime_Lifecycle_Good(t *testing.T) {
|
||||
svc := &testService{name: "lifecycle"}
|
||||
rt, err := NewWithFactories(nil, map[string]ServiceFactory{
|
||||
"test": func() (any, error) { return svc, nil },
|
||||
})
|
||||
assert.NoError(t, err)
|
||||
|
||||
err = rt.ServiceStartup(context.Background(), nil)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, svc.started)
|
||||
|
||||
err = rt.ServiceShutdown(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, svc.stopped)
|
||||
}
|
||||
|
|
@ -1,116 +0,0 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestServiceManager_RegisterService_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
err := c.RegisterService("svc1", &MockService{Name: "one"})
|
||||
assert.NoError(t, err)
|
||||
|
||||
got := c.Service("svc1")
|
||||
assert.NotNil(t, got)
|
||||
assert.Equal(t, "one", got.(*MockService).GetName())
|
||||
}
|
||||
|
||||
func TestServiceManager_RegisterService_Bad(t *testing.T) {
|
||||
c, _ := New()
|
||||
|
||||
// Empty name
|
||||
err := c.RegisterService("", &MockService{})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "cannot be empty")
|
||||
|
||||
// Duplicate
|
||||
err = c.RegisterService("dup", &MockService{})
|
||||
assert.NoError(t, err)
|
||||
err = c.RegisterService("dup", &MockService{})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "already registered")
|
||||
|
||||
// Locked
|
||||
c2, _ := New(WithServiceLock())
|
||||
err = c2.RegisterService("late", &MockService{})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "serviceLock")
|
||||
}
|
||||
|
||||
func TestServiceManager_ServiceNotFound_Good(t *testing.T) {
|
||||
c, _ := New()
|
||||
assert.Nil(t, c.Service("nonexistent"))
|
||||
}
|
||||
|
||||
func TestServiceManager_Startables_Good(t *testing.T) {
|
||||
s1 := &MockStartable{}
|
||||
s2 := &MockStartable{}
|
||||
|
||||
c, _ := New(
|
||||
WithName("s1", func(_ *Core) (any, error) { return s1, nil }),
|
||||
WithName("s2", func(_ *Core) (any, error) { return s2, nil }),
|
||||
)
|
||||
|
||||
// Startup should call both
|
||||
err := c.ServiceStartup(context.Background(), nil)
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestServiceManager_Stoppables_Good(t *testing.T) {
|
||||
s1 := &MockStoppable{}
|
||||
s2 := &MockStoppable{}
|
||||
|
||||
c, _ := New(
|
||||
WithName("s1", func(_ *Core) (any, error) { return s1, nil }),
|
||||
WithName("s2", func(_ *Core) (any, error) { return s2, nil }),
|
||||
)
|
||||
|
||||
// Shutdown should call both
|
||||
err := c.ServiceShutdown(context.Background())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func TestServiceManager_Lock_Good(t *testing.T) {
|
||||
c, _ := New(
|
||||
WithName("early", func(_ *Core) (any, error) { return &MockService{}, nil }),
|
||||
WithServiceLock(),
|
||||
)
|
||||
|
||||
// Register after lock — should fail
|
||||
err := c.RegisterService("late", &MockService{})
|
||||
assert.Error(t, err)
|
||||
assert.Contains(t, err.Error(), "serviceLock")
|
||||
|
||||
// Early service is still accessible
|
||||
assert.NotNil(t, c.Service("early"))
|
||||
}
|
||||
|
||||
func TestServiceManager_LockNotAppliedWithoutEnable_Good(t *testing.T) {
|
||||
// No WithServiceLock — should allow registration after New()
|
||||
c, _ := New()
|
||||
err := c.RegisterService("svc", &MockService{})
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
type mockFullLifecycle struct{}
|
||||
|
||||
func (m *mockFullLifecycle) OnStartup(_ context.Context) error { return nil }
|
||||
func (m *mockFullLifecycle) OnShutdown(_ context.Context) error { return nil }
|
||||
|
||||
func TestServiceManager_LifecycleBoth_Good(t *testing.T) {
|
||||
svc := &mockFullLifecycle{}
|
||||
|
||||
c, _ := New(
|
||||
WithName("both", func(_ *Core) (any, error) { return svc, nil }),
|
||||
)
|
||||
|
||||
// Should participate in both startup and shutdown
|
||||
err := c.ServiceStartup(context.Background(), nil)
|
||||
assert.NoError(t, err)
|
||||
err = c.ServiceShutdown(context.Background())
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
102
tests/service_test.go
Normal file
102
tests/service_test.go
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type testService struct {
|
||||
name string
|
||||
started bool
|
||||
stopped bool
|
||||
}
|
||||
|
||||
func (s *testService) OnStartup(_ context.Context) error { s.started = true; return nil }
|
||||
func (s *testService) OnShutdown(_ context.Context) error { s.stopped = true; return nil }
|
||||
|
||||
// --- Service Registration ---
|
||||
|
||||
func TestService_Register_Good(t *testing.T) {
|
||||
c := New()
|
||||
svc := &testService{name: "auth"}
|
||||
result := c.Service("auth", svc)
|
||||
assert.Nil(t, result) // nil = success
|
||||
|
||||
got := c.Service("auth")
|
||||
assert.Equal(t, svc, got)
|
||||
}
|
||||
|
||||
func TestService_Register_Bad(t *testing.T) {
|
||||
c := New()
|
||||
svc := &testService{name: "auth"}
|
||||
|
||||
// Register once — ok
|
||||
c.Service("auth", svc)
|
||||
|
||||
// Register duplicate — returns error
|
||||
result := c.Service("auth", svc)
|
||||
assert.NotNil(t, result)
|
||||
|
||||
// Empty name — returns error
|
||||
result = c.Service("", svc)
|
||||
assert.NotNil(t, result)
|
||||
}
|
||||
|
||||
func TestService_Get_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Service("brain", &testService{name: "brain"})
|
||||
|
||||
svc := c.Service("brain")
|
||||
assert.NotNil(t, svc)
|
||||
|
||||
ts, ok := svc.(*testService)
|
||||
assert.True(t, ok)
|
||||
assert.Equal(t, "brain", ts.name)
|
||||
}
|
||||
|
||||
func TestService_Get_Bad(t *testing.T) {
|
||||
c := New()
|
||||
svc := c.Service("nonexistent")
|
||||
assert.Nil(t, svc)
|
||||
}
|
||||
|
||||
func TestService_Registry_Good(t *testing.T) {
|
||||
c := New()
|
||||
// Zero args returns *Service
|
||||
registry := c.Service()
|
||||
assert.NotNil(t, registry)
|
||||
}
|
||||
|
||||
// --- Service Lifecycle ---
|
||||
|
||||
func TestService_Lifecycle_Good(t *testing.T) {
|
||||
c := New()
|
||||
svc := &testService{name: "lifecycle"}
|
||||
c.Service("lifecycle", svc)
|
||||
|
||||
// Startup
|
||||
err := c.ServiceStartup(context.Background(), nil)
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, svc.started)
|
||||
|
||||
// Shutdown
|
||||
err = c.ServiceShutdown(context.Background())
|
||||
assert.NoError(t, err)
|
||||
assert.True(t, svc.stopped)
|
||||
}
|
||||
|
||||
func TestService_Lock_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.Service("early", &testService{name: "early"})
|
||||
|
||||
// Lock service registration
|
||||
c.LockEnable()
|
||||
c.LockApply()
|
||||
|
||||
// Attempt to register after lock
|
||||
result := c.Service("late", &testService{name: "late"})
|
||||
assert.NotNil(t, result) // error — locked
|
||||
}
|
||||
66
tests/task_test.go
Normal file
66
tests/task_test.go
Normal file
|
|
@ -0,0 +1,66 @@
|
|||
package core_test
|
||||
|
||||
import (
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
. "forge.lthn.ai/core/go/pkg/core"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// --- PerformAsync ---
|
||||
|
||||
func TestPerformAsync_Good(t *testing.T) {
|
||||
c := New()
|
||||
var mu sync.Mutex
|
||||
var result string
|
||||
|
||||
c.RegisterTask(func(_ *Core, task Task) (any, bool, error) {
|
||||
mu.Lock()
|
||||
result = "done"
|
||||
mu.Unlock()
|
||||
return "completed", true, nil
|
||||
})
|
||||
|
||||
taskID := c.PerformAsync("work")
|
||||
assert.NotEmpty(t, taskID)
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
mu.Lock()
|
||||
assert.Equal(t, "done", result)
|
||||
mu.Unlock()
|
||||
}
|
||||
|
||||
func TestPerformAsync_Progress_Good(t *testing.T) {
|
||||
c := New()
|
||||
c.RegisterTask(func(_ *Core, task Task) (any, bool, error) {
|
||||
return nil, true, nil
|
||||
})
|
||||
|
||||
taskID := c.PerformAsync("work")
|
||||
c.Progress(taskID, 0.5, "halfway", "work")
|
||||
}
|
||||
|
||||
// --- RegisterAction + RegisterActions ---
|
||||
|
||||
func TestRegisterAction_Good(t *testing.T) {
|
||||
c := New()
|
||||
called := false
|
||||
c.RegisterAction(func(_ *Core, _ Message) error {
|
||||
called = true
|
||||
return nil
|
||||
})
|
||||
_ = c.Action(nil)
|
||||
assert.True(t, called)
|
||||
}
|
||||
|
||||
func TestRegisterActions_Good(t *testing.T) {
|
||||
c := New()
|
||||
count := 0
|
||||
h := func(_ *Core, _ Message) error { count++; return nil }
|
||||
c.RegisterActions(h, h)
|
||||
_ = c.Action(nil)
|
||||
assert.Equal(t, 2, count)
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue