[agent/codex] Fix failing tests. Run go test ./... -count=1 to see failu... #34

Open
Virgil wants to merge 17 commits from agent/fix-failing-tests--run--go-test--------c into dev
23 changed files with 773 additions and 253 deletions

52
app.go
View file

@ -1,7 +1,6 @@
// SPDX-License-Identifier: EUPL-1.2
// Application identity for the Core framework.
// Based on leaanthony/sail — Name, Filename, Path.
package core
@ -11,32 +10,47 @@ import (
)
// App holds the application identity and optional GUI runtime.
//
// app := core.App{}.New(core.NewOptions(
// core.Option{Key: "name", Value: "Core CLI"},
// core.Option{Key: "version", Value: "1.0.0"},
// ))
type App struct {
// Name is the human-readable application name (e.g., "Core CLI").
Name string
// Version is the application version string (e.g., "1.2.3").
Version string
// Description is a short description of the application.
Name string
Version string
Description string
Filename string
Path string
Runtime any // GUI runtime (e.g., Wails App). Nil for CLI-only.
}
// Filename is the executable filename (e.g., "core").
Filename string
// Path is the absolute path to the executable.
Path string
// Runtime is the GUI runtime (e.g., Wails App).
// Nil for CLI-only applications.
Runtime any
// New creates an App from Options.
//
// app := core.App{}.New(core.NewOptions(
// core.Option{Key: "name", Value: "myapp"},
// core.Option{Key: "version", Value: "1.0.0"},
// ))
func (a App) New(opts Options) App {
if name := opts.String("name"); name != "" {
a.Name = name
}
if version := opts.String("version"); version != "" {
a.Version = version
}
if desc := opts.String("description"); desc != "" {
a.Description = desc
}
if filename := opts.String("filename"); filename != "" {
a.Filename = filename
}
return a
}
// Find locates a program on PATH and returns a Result containing the App.
//
// r := core.Find("node", "Node.js")
// r := core.App{}.Find("node", "Node.js")
// if r.OK { app := r.Value.(*App) }
func Find(filename, name string) Result {
func (a App) Find(filename, name string) Result {
path, err := exec.LookPath(filename)
if err != nil {
return Result{err, false}

View file

@ -7,14 +7,41 @@ import (
"github.com/stretchr/testify/assert"
)
// --- App ---
// --- App.New ---
func TestApp_Good(t *testing.T) {
c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core)
func TestApp_New_Good(t *testing.T) {
app := App{}.New(NewOptions(
Option{Key: "name", Value: "myapp"},
Option{Key: "version", Value: "1.0.0"},
Option{Key: "description", Value: "test app"},
))
assert.Equal(t, "myapp", app.Name)
assert.Equal(t, "1.0.0", app.Version)
assert.Equal(t, "test app", app.Description)
}
func TestApp_New_Empty_Good(t *testing.T) {
app := App{}.New(NewOptions())
assert.Equal(t, "", app.Name)
assert.Equal(t, "", app.Version)
}
func TestApp_New_Partial_Good(t *testing.T) {
app := App{}.New(NewOptions(
Option{Key: "name", Value: "myapp"},
))
assert.Equal(t, "myapp", app.Name)
assert.Equal(t, "", app.Version)
}
// --- App via Core ---
func TestApp_Core_Good(t *testing.T) {
c := New(WithOption("name", "myapp")).Value.(*Core)
assert.Equal(t, "myapp", c.App().Name)
}
func TestApp_Empty_Good(t *testing.T) {
func TestApp_Core_Empty_Good(t *testing.T) {
c := New().Value.(*Core)
assert.NotNil(t, c.App())
assert.Equal(t, "", c.App().Name)
@ -26,14 +53,16 @@ func TestApp_Runtime_Good(t *testing.T) {
assert.NotNil(t, c.App().Runtime)
}
// --- App.Find ---
func TestApp_Find_Good(t *testing.T) {
r := Find("go", "go")
r := App{}.Find("go", "go")
assert.True(t, r.OK)
app := r.Value.(*App)
assert.NotEmpty(t, app.Path)
}
func TestApp_Find_Bad(t *testing.T) {
r := Find("nonexistent-binary-xyz", "test")
r := App{}.Find("nonexistent-binary-xyz", "test")
assert.False(t, r.OK)
}

69
cli.go
View file

@ -1,16 +1,10 @@
// SPDX-License-Identifier: EUPL-1.2
// Cli is the CLI surface layer for the Core command tree.
// It reads commands from Core's registry and wires them to terminal I/O.
//
// Run the CLI:
//
// c := core.New(core.Options{{Key: "name", Value: "myapp"}})
// c.Command("deploy", handler)
// c := core.New(core.WithOption("name", "myapp")).Value.(*Core)
// c.Command("deploy", core.Command{Action: handler})
// c.Cli().Run()
//
// The Cli resolves os.Args to a command path, parses flags,
// and calls the command's action with parsed options.
package core
import (
@ -18,13 +12,25 @@ import (
"os"
)
// CliOptions holds configuration for the Cli service.
type CliOptions struct{}
// Cli is the CLI surface for the Core command tree.
type Cli struct {
core *Core
*ServiceRuntime[CliOptions]
output io.Writer
banner func(*Cli) string
}
// Register creates a Cli service factory for core.WithService.
//
// core.New(core.WithService(core.CliRegister))
func CliRegister(c *Core) Result {
cl := &Cli{output: os.Stdout}
cl.ServiceRuntime = NewServiceRuntime[CliOptions](c, CliOptions{})
return c.RegisterService("cli", cl)
}
// Print writes to the CLI output (defaults to os.Stdout).
//
// c.Cli().Print("hello %s", "world")
@ -49,17 +55,18 @@ func (cl *Cli) Run(args ...string) Result {
}
clean := FilterArgs(args)
c := cl.Core()
if cl.core == nil || cl.core.commands == nil {
if c == nil || c.commands == nil {
if cl.banner != nil {
cl.Print(cl.banner(cl))
}
return Result{}
}
cl.core.commands.mu.RLock()
cmdCount := len(cl.core.commands.commands)
cl.core.commands.mu.RUnlock()
c.commands.mu.RLock()
cmdCount := len(c.commands.commands)
c.commands.mu.RUnlock()
if cmdCount == 0 {
if cl.banner != nil {
@ -72,16 +79,16 @@ func (cl *Cli) Run(args ...string) Result {
var cmd *Command
var remaining []string
cl.core.commands.mu.RLock()
c.commands.mu.RLock()
for i := len(clean); i > 0; i-- {
path := JoinPath(clean[:i]...)
if c, ok := cl.core.commands.commands[path]; ok {
cmd = c
if found, ok := c.commands.commands[path]; ok {
cmd = found
remaining = clean[i:]
break
}
}
cl.core.commands.mu.RUnlock()
c.commands.mu.RUnlock()
if cmd == nil {
if cl.banner != nil {
@ -92,17 +99,17 @@ func (cl *Cli) Run(args ...string) Result {
}
// Build options from remaining args
opts := Options{}
opts := NewOptions()
for _, arg := range remaining {
key, val, valid := ParseFlag(arg)
if valid {
if Contains(arg, "=") {
opts = append(opts, Option{Key: key, Value: val})
opts.Set(key, val)
} else {
opts = append(opts, Option{Key: key, Value: true})
opts.Set(key, true)
}
} else if !IsFlag(arg) {
opts = append(opts, Option{Key: "_arg", Value: arg})
opts.Set("_arg", arg)
}
}
@ -119,13 +126,14 @@ func (cl *Cli) Run(args ...string) Result {
//
// c.Cli().PrintHelp()
func (cl *Cli) PrintHelp() {
if cl.core == nil || cl.core.commands == nil {
c := cl.Core()
if c == nil || c.commands == nil {
return
}
name := ""
if cl.core.app != nil {
name = cl.core.app.Name
if c.app != nil {
name = c.app.Name
}
if name != "" {
cl.Print("%s commands:", name)
@ -133,14 +141,14 @@ func (cl *Cli) PrintHelp() {
cl.Print("Commands:")
}
cl.core.commands.mu.RLock()
defer cl.core.commands.mu.RUnlock()
c.commands.mu.RLock()
defer c.commands.mu.RUnlock()
for path, cmd := range cl.core.commands.commands {
for path, cmd := range c.commands.commands {
if cmd.Hidden || (cmd.Action == nil && cmd.Lifecycle == nil) {
continue
}
tr := cl.core.I18n().Translate(cmd.I18nKey())
tr := c.I18n().Translate(cmd.I18nKey())
desc, _ := tr.Value.(string)
if desc == "" || desc == cmd.I18nKey() {
cl.Print(" %s", path)
@ -162,8 +170,9 @@ func (cl *Cli) Banner() string {
if cl.banner != nil {
return cl.banner(cl)
}
if cl.core != nil && cl.core.app != nil && cl.core.app.Name != "" {
return cl.core.app.Name
c := cl.Core()
if c != nil && c.app != nil && c.app.Name != "" {
return c.app.Name
}
return ""
}

View file

@ -16,7 +16,7 @@ func TestCli_Good(t *testing.T) {
}
func TestCli_Banner_Good(t *testing.T) {
c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core)
c := New(WithOption("name", "myapp")).Value.(*Core)
assert.Equal(t, "myapp", c.Cli().Banner())
}
@ -70,7 +70,7 @@ func TestCli_Run_NoCommand_Good(t *testing.T) {
}
func TestCli_PrintHelp_Good(t *testing.T) {
c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core)
c := New(WithOption("name", "myapp")).Value.(*Core)
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("serve", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Cli().PrintHelp()

View file

@ -69,7 +69,7 @@ func (cmd *Command) I18nKey() string {
// Run executes the command's action with the given options.
//
// result := cmd.Run(core.Options{{Key: "target", Value: "homelab"}})
// result := cmd.Run(core.NewOptions(core.Option{Key: "target", Value: "homelab"}))
func (cmd *Command) Run(opts Options) Result {
if cmd.Action == nil {
return Result{E("core.Command.Run", Concat("command \"", cmd.Path, "\" is not executable"), nil), false}

View file

@ -37,7 +37,7 @@ func TestCommand_Run_Good(t *testing.T) {
return Result{Value: Concat("hello ", opts.String("name")), OK: true}
}})
cmd := c.Command("greet").Value.(*Command)
r := cmd.Run(Options{{Key: "name", Value: "world"}})
r := cmd.Run(NewOptions(Option{Key: "name", Value: "world"}))
assert.True(t, r.OK)
assert.Equal(t, "hello world", r.Value)
}
@ -46,7 +46,7 @@ func TestCommand_Run_NoAction_Good(t *testing.T) {
c := New().Value.(*Core)
c.Command("empty", Command{Description: "no action"})
cmd := c.Command("empty").Value.(*Command)
r := cmd.Run(Options{})
r := cmd.Run(NewOptions())
assert.False(t, r.OK)
}
@ -111,7 +111,7 @@ func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) {
}})
cmd := c.Command("serve").Value.(*Command)
r := cmd.Start(Options{})
r := cmd.Start(NewOptions())
assert.True(t, r.OK)
assert.Equal(t, "running", r.Value)
@ -158,7 +158,7 @@ func TestCommand_Lifecycle_WithImpl_Good(t *testing.T) {
c.Command("daemon", Command{Lifecycle: lc})
cmd := c.Command("daemon").Value.(*Command)
r := cmd.Start(Options{})
r := cmd.Start(NewOptions())
assert.True(t, r.OK)
assert.True(t, lc.started)

View file

@ -6,6 +6,7 @@ package core
import (
"context"
"reflect"
)
// Message is the type for IPC broadcasts (fire-and-forget).
@ -81,7 +82,7 @@ type CoreOption func(*Core) Result
// IPC access and participate in the lifecycle (ServiceStartup/ServiceShutdown).
//
// r := core.New(
// core.WithOptions(core.Options{{Key: "name", Value: "myapp"}}),
// core.WithOptions(core.NewOptions(core.Option{Key: "name", Value: "myapp"})),
// core.WithService(auth.Register),
// core.WithServiceLock(),
// )
@ -104,7 +105,9 @@ func New(opts ...CoreOption) Result {
commands: &commandRegistry{commands: make(map[string]*Command)},
}
c.context, c.cancel = context.WithCancel(context.Background())
c.cli = &Cli{core: c}
// Core services
CliRegister(c)
for _, opt := range opts {
if r := opt(c); !r.OK {
@ -112,12 +115,15 @@ func New(opts ...CoreOption) Result {
}
}
// Apply service lock after all opts — v0.3.3 parity
c.LockApply()
return Result{c, true}
}
// WithOptions applies key-value configuration to Core.
//
// core.WithOptions(core.Options{{Key: "name", Value: "myapp"}})
// core.WithOptions(core.NewOptions(core.Option{Key: "name", Value: "myapp"}))
func WithOptions(opts Options) CoreOption {
return func(c *Core) Result {
c.options = &opts
@ -129,16 +135,89 @@ func WithOptions(opts Options) CoreOption {
}
// WithService registers a service via its factory function.
// The factory receives *Core so the service can wire IPC handlers
// and access other subsystems during construction.
// Service name is auto-discovered from the package path.
// If the service implements HandleIPCEvents, it is auto-registered.
// If the factory returns a non-nil Value, WithService auto-discovers the
// service name from the factory's package path (last path segment, lowercase,
// with any "_test" suffix stripped) and calls RegisterService on the instance.
// IPC handler auto-registration is handled by RegisterService.
//
// If the factory returns nil Value (it registered itself), WithService
// returns success without a second registration.
//
// core.WithService(agentic.Register)
// core.WithService(display.Register(nil))
func WithService(factory func(*Core) Result) CoreOption {
return func(c *Core) Result {
return factory(c)
r := factory(c)
if !r.OK {
return r
}
if r.Value == nil {
// Factory self-registered — nothing more to do.
return Result{OK: true}
}
// Auto-discover the service name from the instance's package path.
instance := r.Value
typeOf := reflect.TypeOf(instance)
if typeOf.Kind() == reflect.Ptr {
typeOf = typeOf.Elem()
}
pkgPath := typeOf.PkgPath()
parts := Split(pkgPath, "/")
name := Lower(parts[len(parts)-1])
if name == "" {
return Result{E("core.WithService", Sprintf("service name could not be discovered for type %T", instance), nil), false}
}
// IPC handler discovery
instanceValue := reflect.ValueOf(instance)
handlerMethod := instanceValue.MethodByName("HandleIPCEvents")
if handlerMethod.IsValid() {
if handler, ok := handlerMethod.Interface().(func(*Core, Message) Result); ok {
c.RegisterAction(handler)
}
}
return c.RegisterService(name, instance)
}
}
// WithName registers a service with an explicit name (no reflect discovery).
//
// core.WithName("ws", func(c *Core) Result {
// return Result{Value: hub, OK: true}
// })
func WithName(name string, factory func(*Core) Result) CoreOption {
return func(c *Core) Result {
r := factory(c)
if !r.OK {
return r
}
if r.Value == nil {
return Result{E("core.WithName", Sprintf("failed to create service %q", name), nil), false}
}
return c.RegisterService(name, r.Value)
}
}
// WithOption is a convenience for setting a single key-value option.
//
// core.New(
// core.WithOption("name", "myapp"),
// core.WithOption("port", 8080),
// )
func WithOption(key string, value any) CoreOption {
return func(c *Core) Result {
if c.options == nil {
opts := NewOptions()
c.options = &opts
}
c.options.Set(key, value)
if key == "name" {
if s, ok := value.(string); ok {
c.app.Name = s
}
}
return Result{OK: true}
}
}
@ -151,7 +230,6 @@ func WithService(factory func(*Core) Result) CoreOption {
func WithServiceLock() CoreOption {
return func(c *Core) Result {
c.LockEnable()
c.LockApply()
return Result{OK: true}
}
}

135
contract_test.go Normal file
View file

@ -0,0 +1,135 @@
// SPDX-License-Identifier: EUPL-1.2
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- WithService ---
// stub service used only for name-discovery tests.
type stubNamedService struct{}
// stubFactory is a package-level factory so the runtime function name carries
// the package path "core_test.stubFactory" — last segment after '/' is
// "core_test", and after stripping a "_test" suffix we get "core".
// For a real service package such as "dappco.re/go/agentic" the discovered
// name would be "agentic".
func stubFactory(c *Core) Result {
return Result{Value: &stubNamedService{}, OK: true}
}
// TestWithService_NameDiscovery_Good verifies that WithService discovers the
// service name from the factory's package path and registers the instance via
// RegisterService, making it retrievable through c.Services().
//
// stubFactory lives in package "dappco.re/go/core_test", so the last path
// segment is "core_test" — WithService strips the "_test" suffix and registers
// the service under the name "core".
func TestWithService_NameDiscovery_Good(t *testing.T) {
r := New(WithService(stubFactory))
assert.True(t, r.OK)
c := r.Value.(*Core)
names := c.Services()
// Service should be auto-registered under a discovered name (not just "cli" which is built-in)
assert.Greater(t, len(names), 1, "expected auto-discovered service to be registered alongside built-in 'cli'")
}
// TestWithService_FactorySelfRegisters_Good verifies that when a factory
// returns Result{OK:true} with no Value (it registered itself), WithService
// does not attempt a second registration and returns success.
func TestWithService_FactorySelfRegisters_Good(t *testing.T) {
selfReg := func(c *Core) Result {
// Factory registers directly, returns no instance.
c.Service("self", Service{})
return Result{OK: true}
}
r := New(WithService(selfReg))
assert.True(t, r.OK)
c := r.Value.(*Core)
// "self" must be present and registered exactly once.
svc := c.Service("self")
assert.True(t, svc.OK, "expected self-registered service to be present")
}
// --- WithName ---
func TestWithName_Good(t *testing.T) {
r := New(
WithName("custom", func(c *Core) Result {
return Result{Value: &stubNamedService{}, OK: true}
}),
)
assert.True(t, r.OK)
c := r.Value.(*Core)
assert.Contains(t, c.Services(), "custom")
}
// --- Lifecycle ---
type lifecycleService struct {
started bool
}
func (s *lifecycleService) OnStartup(_ context.Context) error {
s.started = true
return nil
}
func TestWithService_Lifecycle_Good(t *testing.T) {
svc := &lifecycleService{}
r := New(
WithService(func(c *Core) Result {
return Result{Value: svc, OK: true}
}),
)
assert.True(t, r.OK)
c := r.Value.(*Core)
c.ServiceStartup(context.Background(), nil)
assert.True(t, svc.started)
}
// --- IPC Handler ---
type ipcService struct {
received Message
}
func (s *ipcService) HandleIPCEvents(c *Core, msg Message) Result {
s.received = msg
return Result{OK: true}
}
func TestWithService_IPCHandler_Good(t *testing.T) {
svc := &ipcService{}
r := New(
WithService(func(c *Core) Result {
return Result{Value: svc, OK: true}
}),
)
assert.True(t, r.OK)
c := r.Value.(*Core)
c.ACTION("ping")
assert.Equal(t, "ping", svc.received)
}
// --- Error ---
// TestWithService_FactoryError_Bad verifies that a factory returning an error
// causes New() to stop and propagate the failure.
func TestWithService_FactoryError_Bad(t *testing.T) {
r := New(WithService(func(c *Core) Result {
return Result{Value: E("test", "factory failed", nil), OK: false}
}))
assert.False(t, r.OK, "expected New() to fail when factory returns error")
}

23
core.go
View file

@ -15,15 +15,15 @@ import (
// Core is the central application object that manages services, assets, and communication.
type Core struct {
options *Options // c.Options() — Input configuration used to create this Core
app *App // c.App() — Application identity + optional GUI runtime
data *Data // c.Data() — Embedded/stored content from packages
drive *Drive // c.Drive() — Resource handle registry (transports)
fs *Fs // c.Fs() — Local filesystem I/O (sandboxable)
config *Config // c.Config() — Configuration, settings, feature flags
error *ErrorPanic // c.Error() — Panic recovery and crash reporting
log *ErrorLog // c.Log() — Structured logging + error wrapping
cli *Cli // c.Cli() — CLI surface layer
options *Options // c.Options() — Input configuration used to create this Core
app *App // c.App() — Application identity + optional GUI runtime
data *Data // c.Data() — Embedded/stored content from packages
drive *Drive // c.Drive() — Resource handle registry (transports)
fs *Fs // c.Fs() — Local filesystem I/O (sandboxable)
config *Config // c.Config() — Configuration, settings, feature flags
error *ErrorPanic // c.Error() — Panic recovery and crash reporting
log *ErrorLog // c.Log() — Structured logging + error wrapping
// cli accessed via ServiceFor[*Cli](c, "cli")
commands *commandRegistry // c.Command("path") — Command tree
services *serviceRegistry // c.Service("name") — Service registry
lock *Lock // c.Lock("name") — Named mutexes
@ -49,7 +49,10 @@ func (c *Core) Fs() *Fs { return c.fs }
func (c *Core) Config() *Config { return c.config }
func (c *Core) Error() *ErrorPanic { return c.error }
func (c *Core) Log() *ErrorLog { return c.log }
func (c *Core) Cli() *Cli { return c.cli }
func (c *Core) Cli() *Cli {
cl, _ := ServiceFor[*Cli](c, "cli")
return cl
}
func (c *Core) IPC() *Ipc { return c.ipc }
func (c *Core) I18n() *I18n { return c.i18n }
func (c *Core) Env(key string) string { return Env(key) }

View file

@ -16,21 +16,21 @@ func TestNew_Good(t *testing.T) {
}
func TestNew_WithOptions_Good(t *testing.T) {
c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core)
c := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"}))).Value.(*Core)
assert.NotNil(t, c)
assert.Equal(t, "myapp", c.App().Name)
}
func TestNew_WithOptions_Bad(t *testing.T) {
// Empty options — should still create a valid Core
c := New(WithOptions(Options{})).Value.(*Core)
c := New(WithOptions(NewOptions())).Value.(*Core)
assert.NotNil(t, c)
}
func TestNew_WithService_Good(t *testing.T) {
started := false
r := New(
WithOptions(Options{{Key: "name", Value: "myapp"}}),
WithOptions(NewOptions(Option{Key: "name", Value: "myapp"})),
WithService(func(c *Core) Result {
c.Service("test", Service{
OnStart: func() Result { started = true; return Result{OK: true} },
@ -64,6 +64,21 @@ func TestNew_WithServiceLock_Good(t *testing.T) {
assert.False(t, reg.OK)
}
func TestNew_WithService_Bad_FailingOption(t *testing.T) {
secondCalled := false
r := New(
WithService(func(c *Core) Result {
return Result{Value: E("test", "intentional failure", nil), OK: false}
}),
WithService(func(c *Core) Result {
secondCalled = true
return Result{OK: true}
}),
)
assert.False(t, r.OK)
assert.False(t, secondCalled, "second option should not run after first fails")
}
// --- Accessors ---
func TestAccessors_Good(t *testing.T) {
@ -82,11 +97,11 @@ func TestAccessors_Good(t *testing.T) {
}
func TestOptions_Accessor_Good(t *testing.T) {
c := New(WithOptions(Options{
{Key: "name", Value: "testapp"},
{Key: "port", Value: 8080},
{Key: "debug", Value: true},
})).Value.(*Core)
c := New(WithOptions(NewOptions(
Option{Key: "name", Value: "testapp"},
Option{Key: "port", Value: 8080},
Option{Key: "debug", Value: true},
))).Value.(*Core)
opts := c.Options()
assert.NotNil(t, opts)
assert.Equal(t, "testapp", opts.String("name"))

20
data.go
View file

@ -6,11 +6,11 @@
//
// Mount a package's assets:
//
// c.Data().New(core.Options{
// {Key: "name", Value: "brain"},
// {Key: "source", Value: brainFS},
// {Key: "path", Value: "prompts"},
// })
// c.Data().New(core.NewOptions(
// core.Option{Key: "name", Value: "brain"},
// core.Option{Key: "source", Value: brainFS},
// core.Option{Key: "path", Value: "prompts"},
// ))
//
// Read from any mounted path:
//
@ -36,11 +36,11 @@ type Data struct {
// New registers an embedded filesystem under a named prefix.
//
// c.Data().New(core.Options{
// {Key: "name", Value: "brain"},
// {Key: "source", Value: brainFS},
// {Key: "path", Value: "prompts"},
// })
// c.Data().New(core.NewOptions(
// core.Option{Key: "name", Value: "brain"},
// core.Option{Key: "source", Value: brainFS},
// core.Option{Key: "path", Value: "prompts"},
// ))
func (d *Data) New(opts Options) Result {
name := opts.String("name")
if name == "" {

View file

@ -14,13 +14,24 @@ var testFS embed.FS
// --- Data (Embedded Content Mounts) ---
func mountTestData(t *testing.T, c *Core, name string) {
t.Helper()
r := c.Data().New(NewOptions(
Option{Key: "name", Value: name},
Option{Key: "source", Value: testFS},
Option{Key: "path", Value: "testdata"},
))
assert.True(t, r.OK)
}
func TestData_New_Good(t *testing.T) {
c := New().Value.(*Core)
r := c.Data().New(Options{
{Key: "name", Value: "test"},
{Key: "source", Value: testFS},
{Key: "path", Value: "testdata"},
})
r := c.Data().New(NewOptions(
Option{Key: "name", Value: "test"},
Option{Key: "source", Value: testFS},
Option{Key: "path", Value: "testdata"},
))
assert.True(t, r.OK)
assert.NotNil(t, r.Value)
}
@ -28,19 +39,19 @@ func TestData_New_Good(t *testing.T) {
func TestData_New_Bad(t *testing.T) {
c := New().Value.(*Core)
r := c.Data().New(Options{{Key: "source", Value: testFS}})
r := c.Data().New(NewOptions(Option{Key: "source", Value: testFS}))
assert.False(t, r.OK)
r = c.Data().New(Options{{Key: "name", Value: "test"}})
r = c.Data().New(NewOptions(Option{Key: "name", Value: "test"}))
assert.False(t, r.OK)
r = c.Data().New(Options{{Key: "name", Value: "test"}, {Key: "source", Value: "not-an-fs"}})
r = c.Data().New(NewOptions(Option{Key: "name", Value: "test"}, Option{Key: "source", Value: "not-an-fs"}))
assert.False(t, r.OK)
}
func TestData_ReadString_Good(t *testing.T) {
c := New().Value.(*Core)
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
mountTestData(t, c, "app")
r := c.Data().ReadString("app/test.txt")
assert.True(t, r.OK)
assert.Equal(t, "hello from testdata\n", r.Value.(string))
@ -54,7 +65,7 @@ func TestData_ReadString_Bad(t *testing.T) {
func TestData_ReadFile_Good(t *testing.T) {
c := New().Value.(*Core)
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
mountTestData(t, c, "app")
r := c.Data().ReadFile("app/test.txt")
assert.True(t, r.OK)
assert.Equal(t, "hello from testdata\n", string(r.Value.([]byte)))
@ -62,7 +73,7 @@ func TestData_ReadFile_Good(t *testing.T) {
func TestData_Get_Good(t *testing.T) {
c := New().Value.(*Core)
c.Data().New(Options{{Key: "name", Value: "brain"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
mountTestData(t, c, "brain")
gr := c.Data().Get("brain")
assert.True(t, gr.OK)
emb := gr.Value.(*Embed)
@ -83,22 +94,22 @@ func TestData_Get_Bad(t *testing.T) {
func TestData_Mounts_Good(t *testing.T) {
c := New().Value.(*Core)
c.Data().New(Options{{Key: "name", Value: "a"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
c.Data().New(Options{{Key: "name", Value: "b"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
mountTestData(t, c, "a")
mountTestData(t, c, "b")
mounts := c.Data().Mounts()
assert.Len(t, mounts, 2)
}
func TestEmbed_Legacy_Good(t *testing.T) {
c := New().Value.(*Core)
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
mountTestData(t, c, "app")
assert.NotNil(t, c.Embed())
}
func TestData_List_Good(t *testing.T) {
c := New().Value.(*Core)
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
r := c.Data().List("app/testdata")
mountTestData(t, c, "app")
r := c.Data().List("app/.")
assert.True(t, r.OK)
}
@ -110,16 +121,16 @@ func TestData_List_Bad(t *testing.T) {
func TestData_ListNames_Good(t *testing.T) {
c := New().Value.(*Core)
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
r := c.Data().ListNames("app/testdata")
mountTestData(t, c, "app")
r := c.Data().ListNames("app/.")
assert.True(t, r.OK)
assert.Contains(t, r.Value.([]string), "test")
}
func TestData_Extract_Good(t *testing.T) {
c := New().Value.(*Core)
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
r := c.Data().Extract("app/testdata", t.TempDir(), nil)
mountTestData(t, c, "app")
r := c.Data().Extract("app/.", t.TempDir(), nil)
assert.True(t, r.OK)
}

View file

@ -6,18 +6,18 @@
//
// Register a transport:
//
// c.Drive().New(core.Options{
// {Key: "name", Value: "api"},
// {Key: "transport", Value: "https://api.lthn.ai"},
// })
// c.Drive().New(core.Options{
// {Key: "name", Value: "ssh"},
// {Key: "transport", Value: "ssh://claude@10.69.69.165"},
// })
// c.Drive().New(core.Options{
// {Key: "name", Value: "mcp"},
// {Key: "transport", Value: "mcp://mcp.lthn.sh"},
// })
// c.Drive().New(core.NewOptions(
// core.Option{Key: "name", Value: "api"},
// core.Option{Key: "transport", Value: "https://api.lthn.ai"},
// ))
// c.Drive().New(core.NewOptions(
// core.Option{Key: "name", Value: "ssh"},
// core.Option{Key: "transport", Value: "ssh://claude@10.69.69.165"},
// ))
// c.Drive().New(core.NewOptions(
// core.Option{Key: "name", Value: "mcp"},
// core.Option{Key: "transport", Value: "mcp://mcp.lthn.sh"},
// ))
//
// Retrieve a handle:
//
@ -43,10 +43,10 @@ type Drive struct {
// New registers a transport handle.
//
// c.Drive().New(core.Options{
// {Key: "name", Value: "api"},
// {Key: "transport", Value: "https://api.lthn.ai"},
// })
// c.Drive().New(core.NewOptions(
// core.Option{Key: "name", Value: "api"},
// core.Option{Key: "transport", Value: "https://api.lthn.ai"},
// ))
func (d *Drive) New(opts Options) Result {
name := opts.String("name")
if name == "" {
@ -62,12 +62,10 @@ func (d *Drive) New(opts Options) Result {
d.handles = make(map[string]*DriveHandle)
}
cp := make(Options, len(opts))
copy(cp, opts)
handle := &DriveHandle{
Name: name,
Transport: transport,
Options: cp,
Options: opts,
}
d.handles[name] = handle

View file

@ -11,10 +11,10 @@ import (
func TestDrive_New_Good(t *testing.T) {
c := New().Value.(*Core)
r := c.Drive().New(Options{
{Key: "name", Value: "api"},
{Key: "transport", Value: "https://api.lthn.ai"},
})
r := c.Drive().New(NewOptions(
Option{Key: "name", Value: "api"},
Option{Key: "transport", Value: "https://api.lthn.ai"},
))
assert.True(t, r.OK)
assert.Equal(t, "api", r.Value.(*DriveHandle).Name)
assert.Equal(t, "https://api.lthn.ai", r.Value.(*DriveHandle).Transport)
@ -23,18 +23,18 @@ func TestDrive_New_Good(t *testing.T) {
func TestDrive_New_Bad(t *testing.T) {
c := New().Value.(*Core)
// Missing name
r := c.Drive().New(Options{
{Key: "transport", Value: "https://api.lthn.ai"},
})
r := c.Drive().New(NewOptions(
Option{Key: "transport", Value: "https://api.lthn.ai"},
))
assert.False(t, r.OK)
}
func TestDrive_Get_Good(t *testing.T) {
c := New().Value.(*Core)
c.Drive().New(Options{
{Key: "name", Value: "ssh"},
{Key: "transport", Value: "ssh://claude@10.69.69.165"},
})
c.Drive().New(NewOptions(
Option{Key: "name", Value: "ssh"},
Option{Key: "transport", Value: "ssh://claude@10.69.69.165"},
))
r := c.Drive().Get("ssh")
assert.True(t, r.OK)
handle := r.Value.(*DriveHandle)
@ -49,16 +49,16 @@ func TestDrive_Get_Bad(t *testing.T) {
func TestDrive_Has_Good(t *testing.T) {
c := New().Value.(*Core)
c.Drive().New(Options{{Key: "name", Value: "mcp"}, {Key: "transport", Value: "mcp://mcp.lthn.sh"}})
c.Drive().New(NewOptions(Option{Key: "name", Value: "mcp"}, Option{Key: "transport", Value: "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().Value.(*Core)
c.Drive().New(Options{{Key: "name", Value: "api"}, {Key: "transport", Value: "https://api.lthn.ai"}})
c.Drive().New(Options{{Key: "name", Value: "ssh"}, {Key: "transport", Value: "ssh://claude@10.69.69.165"}})
c.Drive().New(Options{{Key: "name", Value: "mcp"}, {Key: "transport", Value: "mcp://mcp.lthn.sh"}})
c.Drive().New(NewOptions(Option{Key: "name", Value: "api"}, Option{Key: "transport", Value: "https://api.lthn.ai"}))
c.Drive().New(NewOptions(Option{Key: "name", Value: "ssh"}, Option{Key: "transport", Value: "ssh://claude@10.69.69.165"}))
c.Drive().New(NewOptions(Option{Key: "name", Value: "mcp"}, Option{Key: "transport", Value: "mcp://mcp.lthn.sh"}))
names := c.Drive().Names()
assert.Len(t, names, 3)
assert.Contains(t, names, "api")
@ -68,11 +68,11 @@ func TestDrive_Names_Good(t *testing.T) {
func TestDrive_OptionsPreserved_Good(t *testing.T) {
c := New().Value.(*Core)
c.Drive().New(Options{
{Key: "name", Value: "api"},
{Key: "transport", Value: "https://api.lthn.ai"},
{Key: "timeout", Value: 30},
})
c.Drive().New(NewOptions(
Option{Key: "name", Value: "api"},
Option{Key: "transport", Value: "https://api.lthn.ai"},
Option{Key: "timeout", Value: 30},
))
r := c.Drive().Get("api")
assert.True(t, r.OK)
handle := r.Value.(*DriveHandle)

View file

@ -396,7 +396,7 @@ func (s *Embed) ReadDir(name string) Result {
if !r.OK {
return r
}
return Result{}.Result(fs.ReadDir(s.fsys, r.Value.(string)))
return Result{}.New(fs.ReadDir(s.fsys, r.Value.(string)))
}
// ReadFile reads the named file.

View file

@ -13,6 +13,14 @@ import (
// --- Mount ---
func mustMountTestFS(t *testing.T, basedir string) *Embed {
t.Helper()
r := Mount(testFS, basedir)
assert.True(t, r.OK)
return r.Value.(*Embed)
}
func TestMount_Good(t *testing.T) {
r := Mount(testFS, "testdata")
assert.True(t, r.OK)
@ -26,34 +34,34 @@ func TestMount_Bad(t *testing.T) {
// --- Embed methods ---
func TestEmbed_ReadFile_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
r := emb.ReadFile("test.txt")
assert.True(t, r.OK)
assert.Equal(t, "hello from testdata\n", string(r.Value.([]byte)))
}
func TestEmbed_ReadString_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
r := emb.ReadString("test.txt")
assert.True(t, r.OK)
assert.Equal(t, "hello from testdata\n", r.Value.(string))
}
func TestEmbed_Open_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
r := emb.Open("test.txt")
assert.True(t, r.OK)
}
func TestEmbed_ReadDir_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
r := emb.ReadDir(".")
assert.True(t, r.OK)
assert.NotEmpty(t, r.Value)
}
func TestEmbed_Sub_Good(t *testing.T) {
emb := Mount(testFS, ".").Value.(*Embed)
emb := mustMountTestFS(t, ".")
r := emb.Sub("testdata")
assert.True(t, r.OK)
sub := r.Value.(*Embed)
@ -62,17 +70,17 @@ func TestEmbed_Sub_Good(t *testing.T) {
}
func TestEmbed_BaseDir_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
assert.Equal(t, "testdata", emb.BaseDirectory())
}
func TestEmbed_FS_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
assert.NotNil(t, emb.FS())
}
func TestEmbed_EmbedFS_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
efs := emb.EmbedFS()
_, err := efs.ReadFile("testdata/test.txt")
assert.NoError(t, err)
@ -204,13 +212,13 @@ func TestExtract_BadTargetDir_Ugly(t *testing.T) {
}
func TestEmbed_PathTraversal_Ugly(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
r := emb.ReadFile("../../etc/passwd")
assert.False(t, r.OK)
}
func TestEmbed_Sub_BaseDir_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
r := emb.Sub("scantest")
assert.True(t, r.OK)
sub := r.Value.(*Embed)
@ -218,19 +226,19 @@ func TestEmbed_Sub_BaseDir_Good(t *testing.T) {
}
func TestEmbed_Open_Bad(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
r := emb.Open("nonexistent.txt")
assert.False(t, r.OK)
}
func TestEmbed_ReadDir_Bad(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
r := emb.ReadDir("nonexistent")
assert.False(t, r.OK)
}
func TestEmbed_EmbedFS_Original_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
emb := mustMountTestFS(t, "testdata")
efs := emb.EmbedFS()
_, err := efs.ReadFile("testdata/test.txt")
assert.NoError(t, err)

10
fs.go
View file

@ -190,7 +190,7 @@ func (m *Fs) List(p string) Result {
if !vp.OK {
return vp
}
return Result{}.Result(os.ReadDir(vp.Value.(string)))
return Result{}.New(os.ReadDir(vp.Value.(string)))
}
// Stat returns file info.
@ -199,7 +199,7 @@ func (m *Fs) Stat(p string) Result {
if !vp.OK {
return vp
}
return Result{}.Result(os.Stat(vp.Value.(string)))
return Result{}.New(os.Stat(vp.Value.(string)))
}
// Open opens the named file for reading.
@ -208,7 +208,7 @@ func (m *Fs) Open(p string) Result {
if !vp.OK {
return vp
}
return Result{}.Result(os.Open(vp.Value.(string)))
return Result{}.New(os.Open(vp.Value.(string)))
}
// Create creates or truncates the named file.
@ -221,7 +221,7 @@ func (m *Fs) Create(p string) Result {
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
return Result{err, false}
}
return Result{}.Result(os.Create(full))
return Result{}.New(os.Create(full))
}
// Append opens the named file for appending, creating it if it doesn't exist.
@ -234,7 +234,7 @@ func (m *Fs) Append(p string) Result {
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
return Result{err, false}
}
return Result{}.Result(os.OpenFile(full, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644))
return Result{}.New(os.OpenFile(full, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644))
}
// ReadStream returns a reader for the file content.

View file

@ -16,11 +16,11 @@ func TestI18n_Good(t *testing.T) {
func TestI18n_AddLocales_Good(t *testing.T) {
c := New().Value.(*Core)
r := c.Data().New(Options{
{Key: "name", Value: "lang"},
{Key: "source", Value: testFS},
{Key: "path", Value: "testdata"},
})
r := c.Data().New(NewOptions(
Option{Key: "name", Value: "lang"},
Option{Key: "source", Value: testFS},
Option{Key: "path", Value: "testdata"},
))
if r.OK {
c.I18n().AddLocales(r.Value.(*Embed))
}

View file

@ -2,42 +2,24 @@
// Core primitives: Option, Options, Result.
//
// Option is a single key-value pair. Options is a collection.
// Any function that returns Result can accept Options.
// Options is the universal input type. Result is the universal output type.
// All Core operations accept Options and return Result.
//
// Create options:
//
// opts := core.Options{
// {Key: "name", Value: "brain"},
// {Key: "path", Value: "prompts"},
// }
//
// Read options:
//
// name := opts.String("name")
// port := opts.Int("port")
// ok := opts.Has("debug")
//
// Use with subsystems:
//
// c.Drive().New(core.Options{
// {Key: "name", Value: "brain"},
// {Key: "source", Value: brainFS},
// {Key: "path", Value: "prompts"},
// })
//
// Use with New:
//
// c := core.New(core.Options{
// {Key: "name", Value: "myapp"},
// })
// opts := core.NewOptions(
// core.Option{Key: "name", Value: "brain"},
// core.Option{Key: "path", Value: "prompts"},
// )
// r := c.Drive().New(opts)
// if !r.OK { log.Fatal(r.Error()) }
package core
// --- Result: Universal Output ---
// Result is the universal return type for Core operations.
// Replaces the (value, error) pattern — errors flow through Core internally.
//
// r := c.Data().New(core.Options{{Key: "name", Value: "brain"}})
// if r.OK { use(r.Result()) }
// r := c.Data().New(opts)
// if !r.OK { core.Error("failed", "err", r.Error()) }
type Result struct {
Value any
OK bool
@ -53,18 +35,43 @@ func (r Result) Result(args ...any) Result {
if len(args) == 0 {
return r
}
return r.New(args...)
}
if len(args) == 1 {
return Result{args[0], true}
func (r Result) New(args ...any) Result {
if len(args) == 0 {
return r
}
if err, ok := args[len(args)-1].(error); ok {
if err != nil {
return Result{err, false}
if len(args) > 1 {
if err, ok := args[len(args)-1].(error); ok {
if err != nil {
return Result{Value: err, OK: false}
}
r.Value = args[0]
r.OK = true
return r
}
return Result{args[0], true}
}
return Result{args[0], true}
r.Value = args[0]
if err, ok := r.Value.(error); ok {
if err != nil {
return Result{Value: err, OK: false}
}
return Result{OK: true}
}
r.OK = true
return r
}
func (r Result) Get() Result {
if r.OK {
return r
}
return Result{Value: r.Value, OK: false}
}
// Option is a single key-value configuration pair.
@ -76,19 +83,51 @@ type Option struct {
Value any
}
// Options is a collection of Option items.
// The universal input type for Core operations.
// --- Options: Universal Input ---
// Options is the universal input type for Core operations.
// A structured collection of key-value pairs with typed accessors.
//
// opts := core.Options{{Key: "name", Value: "myapp"}}
// opts := core.NewOptions(
// core.Option{Key: "name", Value: "myapp"},
// core.Option{Key: "port", Value: 8080},
// )
// name := opts.String("name")
type Options []Option
type Options struct {
items []Option
}
// NewOptions creates an Options collection from key-value pairs.
//
// opts := core.NewOptions(
// core.Option{Key: "name", Value: "brain"},
// core.Option{Key: "path", Value: "prompts"},
// )
func NewOptions(items ...Option) Options {
cp := make([]Option, len(items))
copy(cp, items)
return Options{items: cp}
}
// Set adds or updates a key-value pair.
//
// opts.Set("port", 8080)
func (o *Options) Set(key string, value any) {
for i, opt := range o.items {
if opt.Key == key {
o.items[i].Value = value
return
}
}
o.items = append(o.items, Option{Key: key, Value: value})
}
// Get retrieves a value by key.
//
// r := opts.Get("name")
// if r.OK { name := r.Value.(string) }
func (o Options) Get(key string) Result {
for _, opt := range o {
for _, opt := range o.items {
if opt.Key == key {
return Result{opt.Value, true}
}
@ -138,3 +177,15 @@ func (o Options) Bool(key string) bool {
b, _ := r.Value.(bool)
return b
}
// Len returns the number of options.
func (o Options) Len() int {
return len(o.items)
}
// Items returns a copy of the underlying option slice.
func (o Options) Items() []Option {
cp := make([]Option, len(o.items))
copy(cp, o.items)
return cp
}

View file

@ -7,75 +7,121 @@ import (
"github.com/stretchr/testify/assert"
)
// --- Option / Options ---
// --- NewOptions ---
func TestNewOptions_Good(t *testing.T) {
opts := NewOptions(
Option{Key: "name", Value: "brain"},
Option{Key: "port", Value: 8080},
)
assert.Equal(t, 2, opts.Len())
}
func TestNewOptions_Empty_Good(t *testing.T) {
opts := NewOptions()
assert.Equal(t, 0, opts.Len())
assert.False(t, opts.Has("anything"))
}
// --- Options.Set ---
func TestOptions_Set_Good(t *testing.T) {
opts := NewOptions()
opts.Set("name", "brain")
assert.Equal(t, "brain", opts.String("name"))
}
func TestOptions_Set_Update_Good(t *testing.T) {
opts := NewOptions(Option{Key: "name", Value: "old"})
opts.Set("name", "new")
assert.Equal(t, "new", opts.String("name"))
assert.Equal(t, 1, opts.Len())
}
// --- Options.Get ---
func TestOptions_Get_Good(t *testing.T) {
opts := Options{
{Key: "name", Value: "brain"},
{Key: "port", Value: 8080},
}
opts := NewOptions(
Option{Key: "name", Value: "brain"},
Option{Key: "port", Value: 8080},
)
r := opts.Get("name")
assert.True(t, r.OK)
assert.Equal(t, "brain", r.Value)
}
func TestOptions_Get_Bad(t *testing.T) {
opts := Options{{Key: "name", Value: "brain"}}
opts := NewOptions(Option{Key: "name", Value: "brain"})
r := opts.Get("missing")
assert.False(t, r.OK)
assert.Nil(t, r.Value)
}
// --- Options.Has ---
func TestOptions_Has_Good(t *testing.T) {
opts := Options{{Key: "debug", Value: true}}
opts := NewOptions(Option{Key: "debug", Value: true})
assert.True(t, opts.Has("debug"))
assert.False(t, opts.Has("missing"))
}
// --- Options.String ---
func TestOptions_String_Good(t *testing.T) {
opts := Options{{Key: "name", Value: "brain"}}
opts := NewOptions(Option{Key: "name", Value: "brain"})
assert.Equal(t, "brain", opts.String("name"))
}
func TestOptions_String_Bad(t *testing.T) {
opts := Options{{Key: "port", Value: 8080}}
// Wrong type — returns empty string
opts := NewOptions(Option{Key: "port", Value: 8080})
assert.Equal(t, "", opts.String("port"))
// Missing key — returns empty string
assert.Equal(t, "", opts.String("missing"))
}
// --- Options.Int ---
func TestOptions_Int_Good(t *testing.T) {
opts := Options{{Key: "port", Value: 8080}}
opts := NewOptions(Option{Key: "port", Value: 8080})
assert.Equal(t, 8080, opts.Int("port"))
}
func TestOptions_Int_Bad(t *testing.T) {
opts := Options{{Key: "name", Value: "brain"}}
opts := NewOptions(Option{Key: "name", Value: "brain"})
assert.Equal(t, 0, opts.Int("name"))
assert.Equal(t, 0, opts.Int("missing"))
}
// --- Options.Bool ---
func TestOptions_Bool_Good(t *testing.T) {
opts := Options{{Key: "debug", Value: true}}
opts := NewOptions(Option{Key: "debug", Value: true})
assert.True(t, opts.Bool("debug"))
}
func TestOptions_Bool_Bad(t *testing.T) {
opts := Options{{Key: "name", Value: "brain"}}
opts := NewOptions(Option{Key: "name", Value: "brain"})
assert.False(t, opts.Bool("name"))
assert.False(t, opts.Bool("missing"))
}
// --- Options.Items ---
func TestOptions_Items_Good(t *testing.T) {
opts := NewOptions(Option{Key: "a", Value: 1}, Option{Key: "b", Value: 2})
items := opts.Items()
assert.Len(t, items, 2)
}
// --- Options with typed struct ---
func TestOptions_TypedStruct_Good(t *testing.T) {
// Packages plug typed structs into Option.Value
type BrainConfig struct {
Name string
OllamaURL string
Collection string
}
cfg := BrainConfig{Name: "brain", OllamaURL: "http://localhost:11434", Collection: "openbrain"}
opts := Options{{Key: "config", Value: cfg}}
opts := NewOptions(Option{Key: "config", Value: cfg})
r := opts.Get("config")
assert.True(t, r.OK)
@ -85,10 +131,47 @@ func TestOptions_TypedStruct_Good(t *testing.T) {
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"))
// --- Result ---
func TestResult_New_Good(t *testing.T) {
r := Result{}.New("value")
assert.Equal(t, "value", r.Value)
}
func TestResult_New_Error_Bad(t *testing.T) {
err := E("test", "failed", nil)
r := Result{}.New(err)
assert.False(t, r.OK)
assert.Equal(t, err, r.Value)
}
func TestResult_Result_Good(t *testing.T) {
r := Result{Value: "hello", OK: true}
assert.Equal(t, r, r.Result())
}
func TestResult_Result_WithArgs_Good(t *testing.T) {
r := Result{}.Result("value")
assert.Equal(t, "value", r.Value)
}
func TestResult_Get_Good(t *testing.T) {
r := Result{Value: "hello", OK: true}
assert.True(t, r.Get().OK)
}
func TestResult_Get_Bad(t *testing.T) {
r := Result{Value: "err", OK: false}
assert.False(t, r.Get().OK)
}
// --- WithOption ---
func TestWithOption_Good(t *testing.T) {
c := New(
WithOption("name", "myapp"),
WithOption("port", 8080),
).Value.(*Core)
assert.Equal(t, "myapp", c.App().Name)
assert.Equal(t, 8080, c.Options().Int("port"))
}

View file

@ -106,7 +106,7 @@ type ServiceFactory func() Result
// NewWithFactories creates a Runtime with the provided service factories.
func NewWithFactories(app any, factories map[string]ServiceFactory) Result {
r := New(WithOptions(Options{{Key: "name", Value: "core"}}))
r := New(WithOptions(NewOptions(Option{Key: "name", Value: "core"})))
if !r.OK {
return r
}

View file

@ -2,9 +2,13 @@
// Service registry for the Core framework.
//
// Register a service:
// Register a service (DTO with lifecycle hooks):
//
// c.Service("auth", core.Service{})
// c.Service("auth", core.Service{OnStart: startFn})
//
// Register a service instance (auto-discovers Startable/Stoppable/HandleIPCEvents):
//
// c.RegisterService("display", displayInstance)
//
// Get a service:
//
@ -13,11 +17,10 @@
package core
// No imports needed — uses package-level string helpers.
// Service is a managed component with optional lifecycle.
type Service struct {
Name string
Instance any // the raw service instance (for interface discovery)
Options Options
OnStart func() Result
OnStop func() Result
@ -66,6 +69,89 @@ func (c *Core) Service(name string, service ...Service) Result {
return Result{OK: true}
}
// RegisterService registers a service instance by name.
// Auto-discovers Startable, Stoppable, and HandleIPCEvents interfaces
// on the instance and wires them into the lifecycle and IPC bus.
//
// c.RegisterService("display", displayInstance)
func (c *Core) RegisterService(name string, instance any) Result {
if name == "" {
return Result{E("core.RegisterService", "service name cannot be empty", nil), false}
}
c.Lock("srv").Mutex.Lock()
defer c.Lock("srv").Mutex.Unlock()
if c.services.locked {
return Result{E("core.RegisterService", Concat("service \"", name, "\" not permitted — registry locked"), nil), false}
}
if _, exists := c.services.services[name]; exists {
return Result{E("core.RegisterService", Join(" ", "service", name, "already registered"), nil), false}
}
srv := &Service{Name: name, Instance: instance}
// Auto-discover lifecycle interfaces
if s, ok := instance.(Startable); ok {
srv.OnStart = func() Result {
if err := s.OnStartup(c.context); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
}
if s, ok := instance.(Stoppable); ok {
srv.OnStop = func() Result {
if err := s.OnShutdown(c.context); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
}
c.services.services[name] = srv
// Auto-discover IPC handler
if handler, ok := instance.(interface {
HandleIPCEvents(*Core, Message) Result
}); ok {
c.ipc.ipcMu.Lock()
c.ipc.ipcHandlers = append(c.ipc.ipcHandlers, handler.HandleIPCEvents)
c.ipc.ipcMu.Unlock()
}
return Result{OK: true}
}
// ServiceFor retrieves a registered service by name and asserts its type.
//
// prep, ok := core.ServiceFor[*agentic.PrepSubsystem](c, "agentic")
func ServiceFor[T any](c *Core, name string) (T, bool) {
var zero T
r := c.Service(name)
if !r.OK {
return zero, false
}
svc := r.Value.(*Service)
if svc.Instance == nil {
return zero, false
}
typed, ok := svc.Instance.(T)
return typed, ok
}
// MustServiceFor retrieves a registered service by name and asserts its type.
// Panics if the service is not found or the type assertion fails.
//
// cli := core.MustServiceFor[*Cli](c, "cli")
func MustServiceFor[T any](c *Core, name string) T {
v, ok := ServiceFor[T](c, name)
if !ok {
panic(E("core.MustServiceFor", Sprintf("service %q not found or wrong type", name), nil))
}
return v
}
// Services returns all registered service names.
//
// names := c.Services()

View file

@ -47,9 +47,9 @@ func TestService_Names_Good(t *testing.T) {
c.Service("a", Service{})
c.Service("b", Service{})
names := c.Services()
assert.Len(t, names, 2)
assert.Contains(t, names, "a")
assert.Contains(t, names, "b")
assert.Contains(t, names, "cli") // auto-registered by CliRegister in New()
}
// --- Service Lifecycle ---