Merge pull request #2 from dAppCore/dev

feat: AX audit + Codex review — polish pass
This commit is contained in:
Snider 2026-03-20 18:52:43 +00:00 committed by GitHub
commit cee07f05dd
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
56 changed files with 5485 additions and 3761 deletions

View file

@ -33,20 +33,22 @@ type App struct {
}
// Find locates a program on PATH and returns a App for it.
// Returns nil if not found.
func Find(filename, name string) *App {
// Find locates a program on PATH and returns a Result containing the App.
//
// r := core.Find("node", "Node.js")
// if r.OK { app := r.Value.(*App) }
func Find(filename, name string) Result {
path, err := exec.LookPath(filename)
if err != nil {
return nil
return Result{err, false}
}
abs, err := filepath.Abs(path)
if err != nil {
return nil
return Result{err, false}
}
return &App{
return Result{&App{
Name: name,
Filename: filename,
Path: abs,
}
}, true}
}

View file

@ -40,14 +40,14 @@ func (s *Array[T]) Contains(val T) bool {
}
// Filter returns a new Array with elements matching the predicate.
func (s *Array[T]) Filter(fn func(T) bool) *Array[T] {
result := &Array[T]{}
func (s *Array[T]) Filter(fn func(T) bool) Result {
filtered := &Array[T]{}
for _, v := range s.items {
if fn(v) {
result.items = append(result.items, v)
filtered.items = append(filtered.items, v)
}
}
return result
return Result{filtered, true}
}
// Each runs a function on every element.
@ -90,7 +90,12 @@ func (s *Array[T]) Clear() {
s.items = nil
}
// AsSlice returns the underlying slice.
// AsSlice returns a copy of the underlying slice.
func (s *Array[T]) AsSlice() []T {
return s.items
if s.items == nil {
return nil
}
out := make([]T, len(s.items))
copy(out, s.items)
return out
}

View file

@ -1,200 +1,169 @@
// SPDX-License-Identifier: EUPL-1.2
// CLI command framework for the Core framework.
// Based on leaanthony/clir — zero-dependency command line interface.
// 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.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 (
"fmt"
"io"
"os"
)
// CliAction represents a function called when a command is invoked.
type CliAction func() error
// CliOpts configures a Cli.
type CliOpts struct {
Version string
Name string
Description string
}
// Cli is the CLI command framework.
// Cli is the CLI surface for the Core command tree.
type Cli struct {
opts *CliOpts
rootCommand *Command
defaultCommand *Command
preRunCommand func(*Cli) error
postRunCommand func(*Cli) error
bannerFunction func(*Cli) string
errorHandler func(string, error) error
core *Core
output io.Writer
banner func(*Cli) string
}
// defaultBannerFunction prints a banner for the application.
func defaultBannerFunction(c *Cli) string {
version := ""
if c.opts != nil && c.opts.Version != "" {
version = " " + c.opts.Version
}
name := ""
description := ""
if c.opts != nil {
name = c.opts.Name
description = c.opts.Description
}
if description != "" {
return fmt.Sprintf("%s%s - %s", name, version, description)
}
return fmt.Sprintf("%s%s", name, version)
// Print writes to the CLI output (defaults to os.Stdout).
//
// c.Cli().Print("hello %s", "world")
func (cl *Cli) Print(format string, args ...any) {
Print(cl.output, format, args...)
}
// Command returns the root command.
func (c *Cli) Command() *Command {
return c.rootCommand
// SetOutput sets the CLI output writer.
//
// c.Cli().SetOutput(os.Stderr)
func (cl *Cli) SetOutput(w io.Writer) {
cl.output = w
}
// Version returns the application version string.
func (c *Cli) Version() string {
if c.opts != nil {
return c.opts.Version
}
return ""
}
// Name returns the application name.
func (c *Cli) Name() string {
if c.opts != nil {
return c.opts.Name
}
return c.rootCommand.name
}
// ShortDescription returns the application short description.
func (c *Cli) ShortDescription() string {
if c.opts != nil {
return c.opts.Description
}
return c.rootCommand.shortdescription
}
// SetBannerFunction sets the function that generates the banner string.
func (c *Cli) SetBannerFunction(fn func(*Cli) string) {
c.bannerFunction = fn
}
// SetErrorFunction sets a custom error handler for undefined flags.
func (c *Cli) SetErrorFunction(fn func(string, error) error) {
c.errorHandler = fn
}
// AddCommand adds a command to the application.
func (c *Cli) AddCommand(command *Command) {
c.rootCommand.AddCommand(command)
}
// PrintBanner prints the application banner.
func (c *Cli) PrintBanner() {
fmt.Println(c.bannerFunction(c))
fmt.Println("")
}
// PrintHelp prints the application help.
func (c *Cli) PrintHelp() {
c.rootCommand.PrintHelp()
}
// Run runs the application with the given arguments.
func (c *Cli) Run(args ...string) error {
if c.preRunCommand != nil {
if err := c.preRunCommand(c); err != nil {
return err
}
}
// Run resolves os.Args to a command path and executes it.
//
// c.Cli().Run()
// c.Cli().Run("deploy", "to", "homelab")
func (cl *Cli) Run(args ...string) Result {
if len(args) == 0 {
args = os.Args[1:]
}
if err := c.rootCommand.run(args); err != nil {
return err
clean := FilterArgs(args)
if cl.core == nil || cl.core.commands == nil {
if cl.banner != nil {
cl.Print(cl.banner(cl))
}
if c.postRunCommand != nil {
if err := c.postRunCommand(c); err != nil {
return err
return Result{}
}
cl.core.commands.mu.RLock()
cmdCount := len(cl.core.commands.commands)
cl.core.commands.mu.RUnlock()
if cmdCount == 0 {
if cl.banner != nil {
cl.Print(cl.banner(cl))
}
return Result{}
}
// Resolve command path from args
var cmd *Command
var remaining []string
cl.core.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
remaining = clean[i:]
break
}
}
return nil
cl.core.commands.mu.RUnlock()
if cmd == nil {
if cl.banner != nil {
cl.Print(cl.banner(cl))
}
cl.PrintHelp()
return Result{}
}
// Build options from remaining args
opts := Options{}
for _, arg := range remaining {
key, val, valid := ParseFlag(arg)
if valid {
if Contains(arg, "=") {
opts = append(opts, Option{Key: key, Value: val})
} else {
opts = append(opts, Option{Key: key, Value: true})
}
} else if !IsFlag(arg) {
opts = append(opts, Option{Key: "_arg", Value: arg})
}
}
if cmd.Action != nil {
return cmd.Run(opts)
}
if cmd.Lifecycle != nil {
return cmd.Start(opts)
}
return Result{E("core.Cli.Run", Concat("command \"", cmd.Path, "\" is not executable"), nil), false}
}
// DefaultCommand sets the command to run when no other commands are given.
func (c *Cli) DefaultCommand(defaultCommand *Command) *Cli {
c.defaultCommand = defaultCommand
return c
// PrintHelp prints available commands.
//
// c.Cli().PrintHelp()
func (cl *Cli) PrintHelp() {
if cl.core == nil || cl.core.commands == nil {
return
}
name := ""
if cl.core.app != nil {
name = cl.core.app.Name
}
if name != "" {
cl.Print("%s commands:", name)
} else {
cl.Print("Commands:")
}
cl.core.commands.mu.RLock()
defer cl.core.commands.mu.RUnlock()
for path, cmd := range cl.core.commands.commands {
if cmd.Hidden || (cmd.Action == nil && cmd.Lifecycle == nil) {
continue
}
tr := cl.core.I18n().Translate(cmd.I18nKey())
desc, _ := tr.Value.(string)
if desc == "" || desc == cmd.I18nKey() {
cl.Print(" %s", path)
} else {
cl.Print(" %-30s %s", path, desc)
}
}
}
// NewChildCommand creates a new subcommand.
func (c *Cli) NewChildCommand(name string, description ...string) *Command {
return c.rootCommand.NewChildCommand(name, description...)
// SetBanner sets the banner function.
//
// c.Cli().SetBanner(func(_ *core.Cli) string { return "My App v1.0" })
func (cl *Cli) SetBanner(fn func(*Cli) string) {
cl.banner = fn
}
// NewChildCommandInheritFlags creates a new subcommand that inherits parent flags.
func (c *Cli) NewChildCommandInheritFlags(name string, description ...string) *Command {
return c.rootCommand.NewChildCommandInheritFlags(name, description...)
}
// PreRun sets a function to call before running the command.
func (c *Cli) PreRun(callback func(*Cli) error) {
c.preRunCommand = callback
}
// PostRun sets a function to call after running the command.
func (c *Cli) PostRun(callback func(*Cli) error) {
c.postRunCommand = callback
}
// BoolFlag adds a boolean flag to the root command.
func (c *Cli) BoolFlag(name, description string, variable *bool) *Cli {
c.rootCommand.BoolFlag(name, description, variable)
return c
}
// StringFlag adds a string flag to the root command.
func (c *Cli) StringFlag(name, description string, variable *string) *Cli {
c.rootCommand.StringFlag(name, description, variable)
return c
}
// IntFlag adds an int flag to the root command.
func (c *Cli) IntFlag(name, description string, variable *int) *Cli {
c.rootCommand.IntFlag(name, description, variable)
return c
}
// AddFlags adds struct-tagged flags to the root command.
func (c *Cli) AddFlags(flags any) *Cli {
c.rootCommand.AddFlags(flags)
return c
}
// Action defines an action for the root command.
func (c *Cli) Action(callback CliAction) *Cli {
c.rootCommand.Action(callback)
return c
}
// LongDescription sets the long description for the root command.
func (c *Cli) LongDescription(longdescription string) *Cli {
c.rootCommand.LongDescription(longdescription)
return c
}
// OtherArgs returns the non-flag arguments passed to the CLI.
func (c *Cli) OtherArgs() []string {
return c.rootCommand.flags.Args()
}
// NewChildCommandFunction creates a subcommand from a function with struct flags.
func (c *Cli) NewChildCommandFunction(name string, description string, fn any) *Cli {
c.rootCommand.NewChildCommandFunction(name, description, fn)
return c
// Banner returns the banner string.
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
}
return ""
}

File diff suppressed because it is too large Load diff

View file

@ -27,13 +27,13 @@ func NewConfigVar[T any](val T) ConfigVar[T] {
return ConfigVar[T]{val: val, set: true}
}
// ConfigOpts holds configuration data.
type ConfigOpts struct {
// ConfigOptions holds configuration data.
type ConfigOptions struct {
Settings map[string]any
Features map[string]bool
}
func (o *ConfigOpts) init() {
func (o *ConfigOptions) init() {
if o.Settings == nil {
o.Settings = make(map[string]any)
}
@ -44,27 +44,33 @@ func (o *ConfigOpts) init() {
// Config holds configuration settings and feature flags.
type Config struct {
*ConfigOpts
*ConfigOptions
mu sync.RWMutex
}
// Set stores a configuration value by key.
func (e *Config) Set(key string, val any) {
e.mu.Lock()
e.ConfigOpts.init()
if e.ConfigOptions == nil {
e.ConfigOptions = &ConfigOptions{}
}
e.ConfigOptions.init()
e.Settings[key] = val
e.mu.Unlock()
}
// Get retrieves a configuration value by key.
func (e *Config) Get(key string) (any, bool) {
func (e *Config) Get(key string) Result {
e.mu.RLock()
defer e.mu.RUnlock()
if e.ConfigOpts == nil || e.Settings == nil {
return nil, false
if e.ConfigOptions == nil || e.Settings == nil {
return Result{}
}
val, ok := e.Settings[key]
return val, ok
if !ok {
return Result{}
}
return Result{val, true}
}
func (e *Config) String(key string) string { return ConfigGet[string](e, key) }
@ -73,12 +79,12 @@ func (e *Config) Bool(key string) bool { return ConfigGet[bool](e, key) }
// ConfigGet retrieves a typed configuration value.
func ConfigGet[T any](e *Config, key string) T {
val, ok := e.Get(key)
if !ok {
r := e.Get(key)
if !r.OK {
var zero T
return zero
}
typed, _ := val.(T)
typed, _ := r.Value.(T)
return typed
}
@ -86,28 +92,39 @@ func ConfigGet[T any](e *Config, key string) T {
func (e *Config) Enable(feature string) {
e.mu.Lock()
e.ConfigOpts.init()
if e.ConfigOptions == nil {
e.ConfigOptions = &ConfigOptions{}
}
e.ConfigOptions.init()
e.Features[feature] = true
e.mu.Unlock()
}
func (e *Config) Disable(feature string) {
e.mu.Lock()
e.ConfigOpts.init()
if e.ConfigOptions == nil {
e.ConfigOptions = &ConfigOptions{}
}
e.ConfigOptions.init()
e.Features[feature] = false
e.mu.Unlock()
}
func (e *Config) Enabled(feature string) bool {
e.mu.RLock()
v := e.Features[feature]
e.mu.RUnlock()
return v
defer e.mu.RUnlock()
if e.ConfigOptions == nil || e.Features == nil {
return false
}
return e.Features[feature]
}
func (e *Config) EnabledFeatures() []string {
e.mu.RLock()
defer e.mu.RUnlock()
if e.ConfigOptions == nil || e.Features == nil {
return nil
}
var result []string
for k, v := range e.Features {
if v {

View file

@ -6,21 +6,8 @@ package core
import (
"context"
"embed"
"fmt"
"reflect"
"strings"
)
// Contract specifies operational guarantees for Core and its services.
type Contract struct {
DontPanic bool
DisableLogging bool
}
// Option is a function that configures the Core.
type Option func(*Core) error
// Message is the type for IPC broadcasts (fire-and-forget).
type Message any
@ -30,18 +17,18 @@ type Query any
// Task is the type for IPC requests that perform side effects.
type Task any
// TaskWithID is an optional interface for tasks that need to know their assigned ID.
type TaskWithID interface {
// TaskWithIdentifier is an optional interface for tasks that need to know their assigned identifier.
type TaskWithIdentifier interface {
Task
SetTaskID(id string)
GetTaskID() string
SetTaskIdentifier(id string)
GetTaskIdentifier() string
}
// QueryHandler handles Query requests. Returns (result, handled, error).
type QueryHandler func(*Core, Query) (any, bool, error)
// QueryHandler handles Query requests. Returns Result{Value, OK}.
type QueryHandler func(*Core, Query) Result
// TaskHandler handles Task requests. Returns (result, handled, error).
type TaskHandler func(*Core, Task) (any, bool, error)
// TaskHandler handles Task requests. Returns Result{Value, OK}.
type TaskHandler func(*Core, Task) Result
// Startable is implemented by services that need startup initialisation.
type Startable interface {
@ -53,31 +40,25 @@ type Stoppable interface {
OnShutdown(ctx context.Context) error
}
// ConfigService provides access to application configuration.
type ConfigService interface {
Get(key string, out any) error
Set(key string, v any) error
}
// --- Action Messages ---
type ActionServiceStartup struct{}
type ActionServiceShutdown struct{}
type ActionTaskStarted struct {
TaskID string
TaskIdentifier string
Task Task
}
type ActionTaskProgress struct {
TaskID string
TaskIdentifier string
Task Task
Progress float64
Message string
}
type ActionTaskCompleted struct {
TaskID string
TaskIdentifier string
Task Task
Result any
Error error
@ -85,114 +66,40 @@ type ActionTaskCompleted struct {
// --- Constructor ---
// New creates a Core instance with the provided options.
func New(opts ...Option) (*Core, error) {
// New creates a Core instance.
//
// c := core.New(core.Options{
// {Key: "name", Value: "myapp"},
// })
func New(opts ...Options) *Core {
c := &Core{
app: &App{},
data: &Data{},
drive: &Drive{},
fs: &Fs{root: "/"},
cfg: &Config{ConfigOpts: &ConfigOpts{}},
err: &ErrPan{},
log: &ErrLog{&ErrOpts{Log: defaultLog}},
cli: &Cli{opts: &CliOpts{}},
srv: &Service{},
config: &Config{ConfigOptions: &ConfigOptions{}},
error: &ErrorPanic{},
log: &ErrorLog{log: Default()},
lock: &Lock{},
ipc: &Ipc{},
i18n: &I18n{},
services: &serviceRegistry{services: make(map[string]*Service)},
commands: &commandRegistry{commands: make(map[string]*Command)},
}
c.context, c.cancel = context.WithCancel(context.Background())
for _, o := range opts {
if err := o(c); err != nil {
return nil, err
if len(opts) > 0 {
cp := make(Options, len(opts[0]))
copy(cp, opts[0])
c.options = &cp
name := cp.String("name")
if name != "" {
c.app.Name = name
}
}
c.LockApply()
return c, nil
}
// --- With* Options ---
// WithService registers a service with auto-discovered name and IPC handler.
func WithService(factory func(*Core) (any, error)) Option {
return func(c *Core) error {
serviceInstance, err := factory(c)
if err != nil {
return E("core.WithService", "failed to create service", err)
}
if serviceInstance == nil {
return E("core.WithService", "service factory returned nil instance", nil)
}
typeOfService := reflect.TypeOf(serviceInstance)
if typeOfService.Kind() == reflect.Ptr {
typeOfService = typeOfService.Elem()
}
pkgPath := typeOfService.PkgPath()
parts := strings.Split(pkgPath, "/")
name := strings.ToLower(parts[len(parts)-1])
if name == "" {
return E("core.WithService", fmt.Sprintf("service name could not be discovered for type %T (PkgPath is empty)", serviceInstance), nil)
}
instanceValue := reflect.ValueOf(serviceInstance)
handlerMethod := instanceValue.MethodByName("HandleIPCEvents")
if handlerMethod.IsValid() {
if handler, ok := handlerMethod.Interface().(func(*Core, Message) error); ok {
c.RegisterAction(handler)
} else {
return E("core.WithService", fmt.Sprintf("service %q has HandleIPCEvents but wrong signature; expected func(*Core, Message) error", name), nil)
}
}
result := c.Service(name, serviceInstance)
if err, ok := result.(error); ok {
return err
}
return nil
}
}
// WithName registers a service with an explicit name.
func WithName(name string, factory func(*Core) (any, error)) Option {
return func(c *Core) error {
serviceInstance, err := factory(c)
if err != nil {
return E("core.WithName", fmt.Sprintf("failed to create service %q", name), err)
}
result := c.Service(name, serviceInstance)
if err, ok := result.(error); ok {
return err
}
return nil
}
}
// WithApp injects the GUI runtime (e.g., Wails App).
func WithApp(runtime any) Option {
return func(c *Core) error {
c.app.Runtime = runtime
return nil
}
}
// WithAssets mounts embedded assets.
func WithAssets(efs embed.FS) Option {
return func(c *Core) error {
sub, err := Mount(efs, ".")
if err != nil {
return E("core.WithAssets", "failed to mount assets", err)
}
c.emb = sub
return nil
}
}
// WithServiceLock prevents service registration after initialisation.
// Order-independent — lock is applied after all options are processed.
func WithServiceLock() Option {
return func(c *Core) error {
c.LockEnable()
return nil
}
// Init Cli surface with Core reference
c.cli = &Cli{core: c}
return c
}

View file

@ -6,6 +6,7 @@
package core
import (
"context"
"sync"
"sync/atomic"
)
@ -14,52 +15,61 @@ 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
emb *Embed // c.Embed() — Mounted embedded assets (read-only)
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)
cfg *Config // c.Config() — Configuration, settings, feature flags
err *ErrPan // c.Error() — Panic recovery and crash reporting
log *ErrLog // c.Log() — Structured logging + error wrapping
cli *Cli // c.Cli() — CLI command framework
srv *Service // c.Service("name") — Service registry and lifecycle
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
commands *commandRegistry // c.Command("path") — Command tree
services *serviceRegistry // c.Service("name") — Service registry
lock *Lock // c.Lock("name") — Named mutexes
ipc *Ipc // c.IPC() — Message bus for IPC
i18n *I18n // c.I18n() — Internationalisation and locale collection
context context.Context
cancel context.CancelFunc
taskIDCounter atomic.Uint64
wg sync.WaitGroup
waitGroup sync.WaitGroup
shutdown atomic.Bool
}
// --- Accessors ---
func (c *Core) Options() *Options { return c.options }
func (c *Core) App() *App { return c.app }
func (c *Core) Embed() *Embed { return c.emb }
func (c *Core) Data() *Data { return c.data }
func (c *Core) Drive() *Drive { return c.drive }
func (c *Core) Embed() Result { return c.data.Get("app") } // legacy — use Data()
func (c *Core) Fs() *Fs { return c.fs }
func (c *Core) Config() *Config { return c.cfg }
func (c *Core) Error() *ErrPan { return c.err }
func (c *Core) Log() *ErrLog { return c.log }
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) IPC() *Ipc { return c.ipc }
func (c *Core) I18n() *I18n { return c.i18n }
func (c *Core) Context() context.Context { return c.context }
func (c *Core) Core() *Core { return c }
// --- IPC (uppercase aliases) ---
func (c *Core) ACTION(msg Message) error { return c.Action(msg) }
func (c *Core) QUERY(q Query) (any, bool, error) { return c.Query(q) }
func (c *Core) QUERYALL(q Query) ([]any, error) { return c.QueryAll(q) }
func (c *Core) PERFORM(t Task) (any, bool, error) { return c.Perform(t) }
func (c *Core) ACTION(msg Message) Result { return c.Action(msg) }
func (c *Core) QUERY(q Query) Result { return c.Query(q) }
func (c *Core) QUERYALL(q Query) Result { return c.QueryAll(q) }
func (c *Core) PERFORM(t Task) Result { return c.Perform(t) }
// --- Error+Log ---
// LogError logs an error and returns a wrapped error.
func (c *Core) LogError(err error, op, msg string) error {
// LogError logs an error and returns the Result from ErrorLog.
func (c *Core) LogError(err error, op, msg string) Result {
return c.log.Error(err, op, msg)
}
// LogWarn logs a warning and returns a wrapped error.
func (c *Core) LogWarn(err error, op, msg string) error {
// LogWarn logs a warning and returns the Result from ErrorLog.
func (c *Core) LogWarn(err error, op, msg string) Result {
return c.log.Warn(err, op, msg)
}

202
pkg/core/data.go Normal file
View file

@ -0,0 +1,202 @@
// SPDX-License-Identifier: EUPL-1.2
// Data is the embedded/stored content system for core packages.
// Packages mount their embedded content here and other packages
// read from it by path.
//
// Mount a package's assets:
//
// c.Data().New(core.Options{
// {Key: "name", Value: "brain"},
// {Key: "source", Value: brainFS},
// {Key: "path", Value: "prompts"},
// })
//
// Read from any mounted path:
//
// content := c.Data().ReadString("brain/coding.md")
// entries := c.Data().List("agent/flow")
//
// Extract a template directory:
//
// c.Data().Extract("agent/workspace/default", "/tmp/ws", data)
package core
import (
"io/fs"
"path/filepath"
"sync"
)
// Data manages mounted embedded filesystems from core packages.
type Data struct {
mounts map[string]*Embed
mu sync.RWMutex
}
// 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"},
// })
func (d *Data) New(opts Options) Result {
name := opts.String("name")
if name == "" {
return Result{}
}
r := opts.Get("source")
if !r.OK {
return r
}
fsys, ok := r.Value.(fs.FS)
if !ok {
return Result{E("data.New", "source is not fs.FS", nil), false}
}
path := opts.String("path")
if path == "" {
path = "."
}
d.mu.Lock()
defer d.mu.Unlock()
if d.mounts == nil {
d.mounts = make(map[string]*Embed)
}
mr := Mount(fsys, path)
if !mr.OK {
return mr
}
emb := mr.Value.(*Embed)
d.mounts[name] = emb
return Result{emb, true}
}
// Get returns the Embed for a named mount point.
//
// r := c.Data().Get("brain")
// if r.OK { emb := r.Value.(*Embed) }
func (d *Data) Get(name string) Result {
d.mu.RLock()
defer d.mu.RUnlock()
if d.mounts == nil {
return Result{}
}
emb, ok := d.mounts[name]
if !ok {
return Result{}
}
return Result{emb, true}
}
// resolve splits a path like "brain/coding.md" into mount name + relative path.
func (d *Data) resolve(path string) (*Embed, string) {
d.mu.RLock()
defer d.mu.RUnlock()
parts := SplitN(path, "/", 2)
if len(parts) < 2 {
return nil, ""
}
if d.mounts == nil {
return nil, ""
}
emb := d.mounts[parts[0]]
return emb, parts[1]
}
// ReadFile reads a file by full path.
//
// r := c.Data().ReadFile("brain/prompts/coding.md")
// if r.OK { data := r.Value.([]byte) }
func (d *Data) ReadFile(path string) Result {
emb, rel := d.resolve(path)
if emb == nil {
return Result{}
}
return emb.ReadFile(rel)
}
// ReadString reads a file as a string.
//
// r := c.Data().ReadString("agent/flow/deploy/to/homelab.yaml")
// if r.OK { content := r.Value.(string) }
func (d *Data) ReadString(path string) Result {
r := d.ReadFile(path)
if !r.OK {
return r
}
return Result{string(r.Value.([]byte)), true}
}
// List returns directory entries at a path.
//
// r := c.Data().List("agent/persona/code")
// if r.OK { entries := r.Value.([]fs.DirEntry) }
func (d *Data) List(path string) Result {
emb, rel := d.resolve(path)
if emb == nil {
return Result{}
}
r := emb.ReadDir(rel)
if !r.OK {
return r
}
return Result{r.Value, true}
}
// ListNames returns filenames (without extensions) at a path.
//
// r := c.Data().ListNames("agent/flow")
// if r.OK { names := r.Value.([]string) }
func (d *Data) ListNames(path string) Result {
r := d.List(path)
if !r.OK {
return r
}
entries := r.Value.([]fs.DirEntry)
var names []string
for _, e := range entries {
name := e.Name()
if !e.IsDir() {
name = TrimSuffix(name, filepath.Ext(name))
}
names = append(names, name)
}
return Result{names, true}
}
// Extract copies a template directory to targetDir.
//
// r := c.Data().Extract("agent/workspace/default", "/tmp/ws", templateData)
func (d *Data) Extract(path, targetDir string, templateData any) Result {
emb, rel := d.resolve(path)
if emb == nil {
return Result{}
}
r := emb.Sub(rel)
if !r.OK {
return r
}
return Extract(r.Value.(*Embed).FS(), targetDir, templateData)
}
// Mounts returns the names of all mounted content.
//
// names := c.Data().Mounts()
func (d *Data) Mounts() []string {
d.mu.RLock()
defer d.mu.RUnlock()
var names []string
for k := range d.mounts {
names = append(names, k)
}
return names
}

112
pkg/core/drive.go Normal file
View file

@ -0,0 +1,112 @@
// SPDX-License-Identifier: EUPL-1.2
// Drive is the resource handle registry for transport connections.
// Packages register their transport handles (API, MCP, SSH, VPN)
// and other packages access them by name.
//
// 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"},
// })
//
// Retrieve a handle:
//
// api := c.Drive().Get("api")
package core
import (
"sync"
)
// DriveHandle holds a named transport resource.
type DriveHandle struct {
Name string
Transport string
Options Options
}
// Drive manages named transport handles.
type Drive struct {
handles map[string]*DriveHandle
mu sync.RWMutex
}
// New registers a transport handle.
//
// c.Drive().New(core.Options{
// {Key: "name", Value: "api"},
// {Key: "transport", Value: "https://api.lthn.ai"},
// })
func (d *Drive) New(opts Options) Result {
name := opts.String("name")
if name == "" {
return Result{}
}
transport := opts.String("transport")
d.mu.Lock()
defer d.mu.Unlock()
if d.handles == nil {
d.handles = make(map[string]*DriveHandle)
}
cp := make(Options, len(opts))
copy(cp, opts)
handle := &DriveHandle{
Name: name,
Transport: transport,
Options: cp,
}
d.handles[name] = handle
return Result{handle, true}
}
// Get returns a handle by name.
//
// r := c.Drive().Get("api")
// if r.OK { handle := r.Value.(*DriveHandle) }
func (d *Drive) Get(name string) Result {
d.mu.RLock()
defer d.mu.RUnlock()
if d.handles == nil {
return Result{}
}
h, ok := d.handles[name]
if !ok {
return Result{}
}
return Result{h, true}
}
// Has returns true if a handle is registered.
//
// if c.Drive().Has("ssh") { ... }
func (d *Drive) Has(name string) bool {
return d.Get(name).OK
}
// Names returns all registered handle names.
//
// names := c.Drive().Names()
func (d *Drive) Names() []string {
d.mu.RLock()
defer d.mu.RUnlock()
var names []string
for k := range d.handles {
names = append(names, k)
}
return names
}

View file

@ -34,7 +34,6 @@ import (
"io/fs"
"os"
"path/filepath"
"strings"
"sync"
"text/template"
)
@ -65,24 +64,38 @@ func AddAsset(group, name, data string) {
}
// GetAsset retrieves and decompresses a packed asset.
func GetAsset(group, name string) (string, error) {
//
// r := core.GetAsset("mygroup", "greeting")
// if r.OK { content := r.Value.(string) }
func GetAsset(group, name string) Result {
assetGroupsMu.RLock()
g, ok := assetGroups[group]
assetGroupsMu.RUnlock()
if !ok {
return "", E("core.GetAsset", fmt.Sprintf("asset group %q not found", group), nil)
assetGroupsMu.RUnlock()
return Result{}
}
data, ok := g.assets[name]
assetGroupsMu.RUnlock()
if !ok {
return "", E("core.GetAsset", fmt.Sprintf("asset %q not found in group %q", name, group), nil)
return Result{}
}
return decompress(data)
s, err := decompress(data)
if err != nil {
return Result{err, false}
}
return Result{s, true}
}
// GetAssetBytes retrieves a packed asset as bytes.
func GetAssetBytes(group, name string) ([]byte, error) {
s, err := GetAsset(group, name)
return []byte(s), err
//
// r := core.GetAssetBytes("mygroup", "file")
// if r.OK { data := r.Value.([]byte) }
func GetAssetBytes(group, name string) Result {
r := GetAsset(group, name)
if !r.OK {
return r
}
return Result{[]byte(r.Value.(string)), true}
}
// --- Build-time: AST Scanner ---
@ -98,14 +111,14 @@ type AssetRef struct {
// ScannedPackage holds all asset references from a set of source files.
type ScannedPackage struct {
PackageName string
BaseDir string
BaseDirectory string
Groups []string
Assets []AssetRef
}
// ScanAssets parses Go source files and finds asset references.
// Looks for calls to: core.GetAsset("group", "name"), core.AddAsset, etc.
func ScanAssets(filenames []string) ([]ScannedPackage, error) {
func ScanAssets(filenames []string) Result {
packageMap := make(map[string]*ScannedPackage)
var scanErr error
@ -113,13 +126,13 @@ func ScanAssets(filenames []string) ([]ScannedPackage, error) {
fset := token.NewFileSet()
node, err := parser.ParseFile(fset, filename, nil, parser.AllErrors)
if err != nil {
return nil, err
return Result{err, false}
}
baseDir := filepath.Dir(filename)
pkg, ok := packageMap[baseDir]
if !ok {
pkg = &ScannedPackage{BaseDir: baseDir}
pkg = &ScannedPackage{BaseDirectory: baseDir}
packageMap[baseDir] = pkg
}
pkg.PackageName = node.Name.Name
@ -149,16 +162,16 @@ func ScanAssets(filenames []string) ([]ScannedPackage, error) {
case "GetAsset", "GetAssetBytes", "String", "MustString", "Bytes", "MustBytes":
if len(call.Args) >= 1 {
if lit, ok := call.Args[len(call.Args)-1].(*ast.BasicLit); ok {
path := strings.Trim(lit.Value, "\"")
path := TrimPrefix(TrimSuffix(lit.Value, "\""), "\"")
group := "."
if len(call.Args) >= 2 {
if glit, ok := call.Args[0].(*ast.BasicLit); ok {
group = strings.Trim(glit.Value, "\"")
group = TrimPrefix(TrimSuffix(glit.Value, "\""), "\"")
}
}
fullPath, err := filepath.Abs(filepath.Join(baseDir, group, path))
if err != nil {
scanErr = Wrap(err, "core.ScanAssets", fmt.Sprintf("could not determine absolute path for asset %q in group %q", path, group))
scanErr = Wrap(err, "core.ScanAssets", Join(" ", "could not determine absolute path for asset", path, "in group", group))
return false
}
pkg.Assets = append(pkg.Assets, AssetRef{
@ -173,10 +186,10 @@ func ScanAssets(filenames []string) ([]ScannedPackage, error) {
// Variable assignment: g := core.Group("./assets")
if len(call.Args) == 1 {
if lit, ok := call.Args[0].(*ast.BasicLit); ok {
path := strings.Trim(lit.Value, "\"")
path := TrimPrefix(TrimSuffix(lit.Value, "\""), "\"")
fullPath, err := filepath.Abs(filepath.Join(baseDir, path))
if err != nil {
scanErr = Wrap(err, "core.ScanAssets", fmt.Sprintf("could not determine absolute path for group %q", path))
scanErr = Wrap(err, "core.ScanAssets", Join(" ", "could not determine absolute path for group", path))
return false
}
pkg.Groups = append(pkg.Groups, fullPath)
@ -189,7 +202,7 @@ func ScanAssets(filenames []string) ([]ScannedPackage, error) {
return true
})
if scanErr != nil {
return nil, scanErr
return Result{scanErr, false}
}
}
@ -197,18 +210,18 @@ func ScanAssets(filenames []string) ([]ScannedPackage, error) {
for _, pkg := range packageMap {
result = append(result, *pkg)
}
return result, nil
return Result{result, true}
}
// GeneratePack creates Go source code that embeds the scanned assets.
func GeneratePack(pkg ScannedPackage) (string, error) {
var b strings.Builder
func GeneratePack(pkg ScannedPackage) Result {
b := NewBuilder()
b.WriteString(fmt.Sprintf("package %s\n\n", pkg.PackageName))
b.WriteString("// Code generated by core pack. DO NOT EDIT.\n\n")
if len(pkg.Assets) == 0 && len(pkg.Groups) == 0 {
return b.String(), nil
return Result{b.String(), true}
}
b.WriteString("import \"forge.lthn.ai/core/go/pkg/core\"\n\n")
@ -219,7 +232,7 @@ func GeneratePack(pkg ScannedPackage) (string, error) {
for _, groupPath := range pkg.Groups {
files, err := getAllFiles(groupPath)
if err != nil {
return "", Wrap(err, "core.GeneratePack", fmt.Sprintf("failed to scan asset group %q", groupPath))
return Result{err, false}
}
for _, file := range files {
if packed[file] {
@ -227,12 +240,12 @@ func GeneratePack(pkg ScannedPackage) (string, error) {
}
data, err := compressFile(file)
if err != nil {
return "", Wrap(err, "core.GeneratePack", fmt.Sprintf("failed to compress asset %q in group %q", file, groupPath))
return Result{err, false}
}
localPath := strings.TrimPrefix(file, groupPath+"/")
relGroup, err := filepath.Rel(pkg.BaseDir, groupPath)
localPath := TrimPrefix(file, groupPath+"/")
relGroup, err := filepath.Rel(pkg.BaseDirectory, groupPath)
if err != nil {
return "", Wrap(err, "core.GeneratePack", fmt.Sprintf("could not determine relative path for group %q (base %q)", groupPath, pkg.BaseDir))
return Result{err, false}
}
b.WriteString(fmt.Sprintf("\tcore.AddAsset(%q, %q, %q)\n", relGroup, localPath, data))
packed[file] = true
@ -246,14 +259,14 @@ func GeneratePack(pkg ScannedPackage) (string, error) {
}
data, err := compressFile(asset.FullPath)
if err != nil {
return "", Wrap(err, "core.GeneratePack", fmt.Sprintf("failed to compress asset %q", asset.FullPath))
return Result{err, false}
}
b.WriteString(fmt.Sprintf("\tcore.AddAsset(%q, %q, %q)\n", asset.Group, asset.Name, data))
packed[asset.FullPath] = true
}
b.WriteString("}\n")
return b.String(), nil
return Result{b.String(), true}
}
// --- Compression ---
@ -289,7 +302,7 @@ func compress(input string) (string, error) {
}
func decompress(input string) (string, error) {
b64 := base64.NewDecoder(base64.StdEncoding, strings.NewReader(input))
b64 := base64.NewDecoder(base64.StdEncoding, NewReader(input))
gz, err := gzip.NewReader(b64)
if err != nil {
return "", err
@ -330,62 +343,104 @@ type Embed struct {
}
// Mount creates a scoped view of an fs.FS anchored at basedir.
// Works with embed.FS, os.DirFS, or any fs.FS implementation.
func Mount(fsys fs.FS, basedir string) (*Embed, error) {
//
// r := core.Mount(myFS, "lib/prompts")
// if r.OK { emb := r.Value.(*Embed) }
func Mount(fsys fs.FS, basedir string) Result {
s := &Embed{fsys: fsys, basedir: basedir}
// If it's an embed.FS, keep a reference for EmbedFS()
if efs, ok := fsys.(embed.FS); ok {
s.embedFS = &efs
}
// Verify the basedir exists
if _, err := s.ReadDir("."); err != nil {
return nil, err
if r := s.ReadDir("."); !r.OK {
return r
}
return s, nil
return Result{s, true}
}
// MountEmbed creates a scoped view of an embed.FS.
func MountEmbed(efs embed.FS, basedir string) (*Embed, error) {
//
// r := core.MountEmbed(myFS, "testdata")
func MountEmbed(efs embed.FS, basedir string) Result {
return Mount(efs, basedir)
}
func (s *Embed) path(name string) string {
return filepath.ToSlash(filepath.Join(s.basedir, name))
func (s *Embed) path(name string) Result {
joined := filepath.ToSlash(filepath.Join(s.basedir, name))
if HasPrefix(joined, "..") || Contains(joined, "/../") || HasSuffix(joined, "/..") {
return Result{E("embed.path", Concat("path traversal rejected: ", name), nil), false}
}
return Result{joined, true}
}
// Open opens the named file for reading.
func (s *Embed) Open(name string) (fs.File, error) {
return s.fsys.Open(s.path(name))
//
// r := emb.Open("test.txt")
// if r.OK { file := r.Value.(fs.File) }
func (s *Embed) Open(name string) Result {
r := s.path(name)
if !r.OK {
return r
}
f, err := s.fsys.Open(r.Value.(string))
if err != nil {
return Result{err, false}
}
return Result{f, true}
}
// ReadDir reads the named directory.
func (s *Embed) ReadDir(name string) ([]fs.DirEntry, error) {
return fs.ReadDir(s.fsys, s.path(name))
func (s *Embed) ReadDir(name string) Result {
r := s.path(name)
if !r.OK {
return r
}
return Result{}.Result(fs.ReadDir(s.fsys, r.Value.(string)))
}
// ReadFile reads the named file.
func (s *Embed) ReadFile(name string) ([]byte, error) {
return fs.ReadFile(s.fsys, s.path(name))
//
// r := emb.ReadFile("test.txt")
// if r.OK { data := r.Value.([]byte) }
func (s *Embed) ReadFile(name string) Result {
r := s.path(name)
if !r.OK {
return r
}
data, err := fs.ReadFile(s.fsys, r.Value.(string))
if err != nil {
return Result{err, false}
}
return Result{data, true}
}
// ReadString reads the named file as a string.
func (s *Embed) ReadString(name string) (string, error) {
data, err := s.ReadFile(name)
if err != nil {
return "", err
//
// r := emb.ReadString("test.txt")
// if r.OK { content := r.Value.(string) }
func (s *Embed) ReadString(name string) Result {
r := s.ReadFile(name)
if !r.OK {
return r
}
return string(data), nil
return Result{string(r.Value.([]byte)), true}
}
// Sub returns a new Embed anchored at a subdirectory within this mount.
func (s *Embed) Sub(subDir string) (*Embed, error) {
sub, err := fs.Sub(s.fsys, s.path(subDir))
if err != nil {
return nil, err
//
// r := emb.Sub("testdata")
// if r.OK { sub := r.Value.(*Embed) }
func (s *Embed) Sub(subDir string) Result {
r := s.path(subDir)
if !r.OK {
return r
}
return &Embed{fsys: sub, basedir: "."}, nil
sub, err := fs.Sub(s.fsys, r.Value.(string))
if err != nil {
return Result{err, false}
}
return Result{&Embed{fsys: sub, basedir: "."}, true}
}
// FS returns the underlying fs.FS.
@ -402,8 +457,8 @@ func (s *Embed) EmbedFS() embed.FS {
return embed.FS{}
}
// BaseDir returns the basedir this Embed is anchored at.
func (s *Embed) BaseDir() string {
// BaseDirectory returns the base directory this Embed is anchored at.
func (s *Embed) BaseDirectory() string {
return s.basedir
}
@ -433,7 +488,7 @@ type ExtractOptions struct {
// {{.Name}}/main.go → myproject/main.go
//
// Data can be any struct or map[string]string for template substitution.
func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) error {
func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) Result {
opt := ExtractOptions{
TemplateFilters: []string{".tmpl"},
IgnoreFiles: make(map[string]struct{}),
@ -454,10 +509,10 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err
// Ensure target directory exists
targetDir, err := filepath.Abs(targetDir)
if err != nil {
return err
return Result{err, false}
}
if err := os.MkdirAll(targetDir, 0755); err != nil {
return err
return Result{err, false}
}
// Categorise files
@ -488,14 +543,29 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err
return nil
})
if err != nil {
return err
return Result{err, false}
}
// safePath ensures a rendered path stays under targetDir.
safePath := func(rendered string) (string, error) {
abs, err := filepath.Abs(rendered)
if err != nil {
return "", err
}
if !HasPrefix(abs, targetDir+string(filepath.Separator)) && abs != targetDir {
return "", E("embed.Extract", Concat("path escapes target: ", abs), nil)
}
return abs, nil
}
// Create directories (names may contain templates)
for _, dir := range dirs {
target := renderPath(filepath.Join(targetDir, dir), data)
target, err := safePath(renderPath(filepath.Join(targetDir, dir), data))
if err != nil {
return Result{err, false}
}
if err := os.MkdirAll(target, 0755); err != nil {
return err
return Result{err, false}
}
}
@ -503,7 +573,7 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err
for _, path := range templateFiles {
tmpl, err := template.ParseFS(fsys, path)
if err != nil {
return err
return Result{err, false}
}
targetFile := renderPath(filepath.Join(targetDir, path), data)
@ -512,20 +582,23 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err
dir := filepath.Dir(targetFile)
name := filepath.Base(targetFile)
for _, filter := range opt.TemplateFilters {
name = strings.ReplaceAll(name, filter, "")
name = Replace(name, filter, "")
}
if renamed := opt.RenameFiles[name]; renamed != "" {
name = renamed
}
targetFile = filepath.Join(dir, name)
targetFile, err = safePath(filepath.Join(dir, name))
if err != nil {
return Result{err, false}
}
f, err := os.Create(targetFile)
if err != nil {
return err
return Result{err, false}
}
if err := tmpl.Execute(f, data); err != nil {
f.Close()
return err
return Result{err, false}
}
f.Close()
}
@ -537,18 +610,21 @@ func Extract(fsys fs.FS, targetDir string, data any, opts ...ExtractOptions) err
if renamed := opt.RenameFiles[name]; renamed != "" {
targetPath = filepath.Join(filepath.Dir(path), renamed)
}
target := renderPath(filepath.Join(targetDir, targetPath), data)
target, err := safePath(renderPath(filepath.Join(targetDir, targetPath), data))
if err != nil {
return Result{err, false}
}
if err := copyFile(fsys, path, target); err != nil {
return err
return Result{err, false}
}
}
return nil
return Result{OK: true}
}
func isTemplate(filename string, filters []string) bool {
for _, f := range filters {
if strings.Contains(filename, f) {
if Contains(filename, f) {
return true
}
}

View file

@ -9,57 +9,55 @@ package core
import (
"encoding/json"
"errors"
"fmt"
"iter"
"maps"
"os"
"path/filepath"
"runtime"
"runtime/debug"
"strings"
"sync"
"time"
)
// ErrSink is the shared interface for error reporting.
// Implemented by ErrLog (structured logging) and ErrPan (panic recovery).
type ErrSink interface {
// ErrorSink is the shared interface for error reporting.
// Implemented by ErrorLog (structured logging) and ErrorPanic (panic recovery).
type ErrorSink interface {
Error(msg string, keyvals ...any)
Warn(msg string, keyvals ...any)
}
var _ ErrSink = (*Log)(nil)
var _ ErrorSink = (*Log)(nil)
// Err represents a structured error with operational context.
// It implements the error interface and supports unwrapping.
type Err struct {
Op string // Operation being performed (e.g., "user.Save")
Msg string // Human-readable message
Err error // Underlying error (optional)
Operation string // Operation being performed (e.g., "user.Save")
Message string // Human-readable message
Cause error // Underlying error (optional)
Code string // Error code (optional, e.g., "VALIDATION_FAILED")
}
// Error implements the error interface.
func (e *Err) Error() string {
var prefix string
if e.Op != "" {
prefix = e.Op + ": "
if e.Operation != "" {
prefix = e.Operation + ": "
}
if e.Err != nil {
if e.Cause != nil {
if e.Code != "" {
return fmt.Sprintf("%s%s [%s]: %v", prefix, e.Msg, e.Code, e.Err)
return Concat(prefix, e.Message, " [", e.Code, "]: ", e.Cause.Error())
}
return fmt.Sprintf("%s%s: %v", prefix, e.Msg, e.Err)
return Concat(prefix, e.Message, ": ", e.Cause.Error())
}
if e.Code != "" {
return fmt.Sprintf("%s%s [%s]", prefix, e.Msg, e.Code)
return Concat(prefix, e.Message, " [", e.Code, "]")
}
return fmt.Sprintf("%s%s", prefix, e.Msg)
return Concat(prefix, e.Message)
}
// Unwrap returns the underlying error for use with errors.Is and errors.As.
func (e *Err) Unwrap() error {
return e.Err
return e.Cause
}
// --- Error Creation Functions ---
@ -72,7 +70,7 @@ func (e *Err) Unwrap() error {
// return log.E("user.Save", "failed to save user", err)
// return log.E("api.Call", "rate limited", nil) // No underlying cause
func E(op, msg string, err error) error {
return &Err{Op: op, Msg: msg, Err: err}
return &Err{Operation: op, Message: msg, Cause: err}
}
// Wrap wraps an error with operation context.
@ -89,9 +87,9 @@ func Wrap(err error, op, msg string) error {
// Preserve Code from wrapped *Err
var logErr *Err
if As(err, &logErr) && logErr.Code != "" {
return &Err{Op: op, Msg: msg, Err: err, Code: logErr.Code}
return &Err{Operation: op, Message: msg, Cause: err, Code: logErr.Code}
}
return &Err{Op: op, Msg: msg, Err: err}
return &Err{Operation: op, Message: msg, Cause: err}
}
// WrapCode wraps an error with operation context and error code.
@ -105,7 +103,7 @@ func WrapCode(err error, code, op, msg string) error {
if err == nil && code == "" {
return nil
}
return &Err{Op: op, Msg: msg, Err: err, Code: code}
return &Err{Operation: op, Message: msg, Cause: err, Code: code}
}
// NewCode creates an error with just code and message (no underlying error).
@ -115,7 +113,7 @@ func WrapCode(err error, code, op, msg string) error {
//
// var ErrNotFound = log.NewCode("NOT_FOUND", "resource not found")
func NewCode(code, msg string) error {
return &Err{Msg: msg, Code: code}
return &Err{Message: msg, Code: code}
}
// --- Standard Library Wrappers ---
@ -138,27 +136,28 @@ func NewError(text string) error {
return errors.New(text)
}
// Join combines multiple errors into one.
// Wrapper around errors.Join for convenience.
func Join(errs ...error) error {
// ErrorJoin combines multiple errors into one.
//
// core.ErrorJoin(err1, err2, err3)
func ErrorJoin(errs ...error) error {
return errors.Join(errs...)
}
// --- Error Introspection Helpers ---
// Op extracts the operation name from an error.
// Operation extracts the operation name from an error.
// Returns empty string if the error is not an *Err.
func Op(err error) string {
func Operation(err error) string {
var e *Err
if As(err, &e) {
return e.Op
return e.Operation
}
return ""
}
// ErrCode extracts the error code from an error.
// ErrorCode extracts the error code from an error.
// Returns empty string if the error is not an *Err or has no code.
func ErrCode(err error) string {
func ErrorCode(err error) string {
var e *Err
if As(err, &e) {
return e.Code
@ -174,7 +173,7 @@ func ErrorMessage(err error) string {
}
var e *Err
if As(err, &e) {
return e.Msg
return e.Message
}
return err.Error()
}
@ -194,14 +193,14 @@ func Root(err error) error {
}
}
// AllOps returns an iterator over all operational contexts in the error chain.
// AllOperations returns an iterator over all operational contexts in the error chain.
// It traverses the error tree using errors.Unwrap.
func AllOps(err error) iter.Seq[string] {
func AllOperations(err error) iter.Seq[string] {
return func(yield func(string) bool) {
for err != nil {
if e, ok := err.(*Err); ok {
if e.Op != "" {
if !yield(e.Op) {
if e.Operation != "" {
if !yield(e.Operation) {
return
}
}
@ -215,7 +214,7 @@ func AllOps(err error) iter.Seq[string] {
// It returns an empty slice if no operational context is found.
func StackTrace(err error) []string {
var stack []string
for op := range AllOps(err) {
for op := range AllOperations(err) {
stack = append(stack, op)
}
return stack
@ -224,64 +223,54 @@ func StackTrace(err error) []string {
// FormatStackTrace returns a pretty-printed logical stack trace.
func FormatStackTrace(err error) string {
var ops []string
for op := range AllOps(err) {
for op := range AllOperations(err) {
ops = append(ops, op)
}
if len(ops) == 0 {
return ""
}
return strings.Join(ops, " -> ")
return Join(" -> ", ops...)
}
// --- ErrLog: Log-and-Return Error Helpers ---
// --- ErrorLog: Log-and-Return Error Helpers ---
// ErrOpts holds shared options for error subsystems.
type ErrOpts struct {
Log *Log
}
// ErrLog combines error creation with logging.
// ErrorLog combines error creation with logging.
// Primary action: return an error. Secondary: log it.
type ErrLog struct {
*ErrOpts
type ErrorLog struct {
log *Log
}
// NewErrLog creates an ErrLog (consumer convenience).
func NewErrLog(opts *ErrOpts) *ErrLog {
return &ErrLog{opts}
}
func (el *ErrLog) log() *Log {
if el.ErrOpts != nil && el.Log != nil {
return el.Log
func (el *ErrorLog) logger() *Log {
if el.log != nil {
return el.log
}
return defaultLog
return Default()
}
// Error logs at Error level and returns a wrapped error.
func (el *ErrLog) Error(err error, op, msg string) error {
// Error logs at Error level and returns a Result with the wrapped error.
func (el *ErrorLog) Error(err error, op, msg string) Result {
if err == nil {
return nil
return Result{OK: true}
}
wrapped := Wrap(err, op, msg)
el.log().Error(msg, "op", op, "err", err)
return wrapped
el.logger().Error(msg, "op", op, "err", err)
return Result{wrapped, false}
}
// Warn logs at Warn level and returns a wrapped error.
func (el *ErrLog) Warn(err error, op, msg string) error {
// Warn logs at Warn level and returns a Result with the wrapped error.
func (el *ErrorLog) Warn(err error, op, msg string) Result {
if err == nil {
return nil
return Result{OK: true}
}
wrapped := Wrap(err, op, msg)
el.log().Warn(msg, "op", op, "err", err)
return wrapped
el.logger().Warn(msg, "op", op, "err", err)
return Result{wrapped, false}
}
// Must logs and panics if err is not nil.
func (el *ErrLog) Must(err error, op, msg string) {
func (el *ErrorLog) Must(err error, op, msg string) {
if err != nil {
el.log().Error(msg, "op", op, "err", err)
el.logger().Error(msg, "op", op, "err", err)
panic(Wrap(err, op, msg))
}
}
@ -299,45 +288,21 @@ type CrashReport struct {
// CrashSystem holds system information at crash time.
type CrashSystem struct {
OS string `json:"os"`
Arch string `json:"arch"`
OperatingSystem string `json:"operatingsystem"`
Architecture string `json:"architecture"`
Version string `json:"go_version"`
}
// ErrPan manages panic recovery and crash reporting.
type ErrPan struct {
// ErrorPanic manages panic recovery and crash reporting.
type ErrorPanic struct {
filePath string
meta map[string]string
onCrash func(CrashReport)
}
// PanOpts configures an ErrPan.
type PanOpts struct {
// FilePath is the crash report JSON output path. Empty disables file output.
FilePath string
// Meta is metadata included in every crash report.
Meta map[string]string
// OnCrash is a callback invoked on every crash.
OnCrash func(CrashReport)
}
// NewErrPan creates an ErrPan (consumer convenience).
func NewErrPan(opts ...PanOpts) *ErrPan {
h := &ErrPan{}
if len(opts) > 0 {
o := opts[0]
h.filePath = o.FilePath
if o.Meta != nil {
h.meta = maps.Clone(o.Meta)
}
h.onCrash = o.OnCrash
}
return h
}
// Recover captures a panic and creates a crash report.
// Use as: defer c.Error().Recover()
func (h *ErrPan) Recover() {
func (h *ErrorPanic) Recover() {
if h == nil {
return
}
@ -348,7 +313,7 @@ func (h *ErrPan) Recover() {
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
err = NewError(Sprint("panic: ", r))
}
report := CrashReport{
@ -356,8 +321,8 @@ func (h *ErrPan) Recover() {
Error: err.Error(),
Stack: string(debug.Stack()),
System: CrashSystem{
OS: runtime.GOOS,
Arch: runtime.GOARCH,
OperatingSystem: runtime.GOOS,
Architecture: runtime.GOARCH,
Version: runtime.Version(),
},
Meta: maps.Clone(h.meta),
@ -373,7 +338,7 @@ func (h *ErrPan) Recover() {
}
// SafeGo runs a function in a goroutine with panic recovery.
func (h *ErrPan) SafeGo(fn func()) {
func (h *ErrorPanic) SafeGo(fn func()) {
go func() {
defer h.Recover()
fn()
@ -381,29 +346,29 @@ func (h *ErrPan) SafeGo(fn func()) {
}
// Reports returns the last n crash reports from the file.
func (h *ErrPan) Reports(n int) ([]CrashReport, error) {
func (h *ErrorPanic) Reports(n int) Result {
if h.filePath == "" {
return nil, nil
return Result{}
}
crashMu.Lock()
defer crashMu.Unlock()
data, err := os.ReadFile(h.filePath)
if err != nil {
return nil, err
return Result{err, false}
}
var reports []CrashReport
if err := json.Unmarshal(data, &reports); err != nil {
return nil, err
return Result{err, false}
}
if n <= 0 || len(reports) <= n {
return reports, nil
return Result{reports, true}
}
return reports[len(reports)-n:], nil
return Result{reports[len(reports)-n:], true}
}
var crashMu sync.Mutex
func (h *ErrPan) appendReport(report CrashReport) {
func (h *ErrorPanic) appendReport(report CrashReport) {
crashMu.Lock()
defer crashMu.Unlock()
@ -415,8 +380,16 @@ func (h *ErrPan) appendReport(report CrashReport) {
}
reports = append(reports, report)
if data, err := json.MarshalIndent(reports, "", " "); err == nil {
_ = os.MkdirAll(filepath.Dir(h.filePath), 0755)
_ = os.WriteFile(h.filePath, data, 0600)
data, err := json.MarshalIndent(reports, "", " ")
if err != nil {
Default().Error(Concat("crash report marshal failed: ", err.Error()))
return
}
if err := os.MkdirAll(filepath.Dir(h.filePath), 0755); err != nil {
Default().Error(Concat("crash report dir failed: ", err.Error()))
return
}
if err := os.WriteFile(h.filePath, data, 0600); err != nil {
Default().Error(Concat("crash report write failed: ", err.Error()))
}
}

View file

@ -2,13 +2,9 @@
package core
import (
"fmt"
"io"
"io/fs"
"os"
"os/user"
"path/filepath"
"strings"
"time"
)
@ -17,7 +13,6 @@ type Fs struct {
root string
}
// path sanitises and returns the full path.
// Absolute paths are sandboxed under root (unless root is "/").
func (m *Fs) path(p string) string {
@ -47,13 +42,13 @@ func (m *Fs) path(p string) string {
}
// validatePath ensures the path is within the sandbox, following symlinks if they exist.
func (m *Fs) validatePath(p string) (string, error) {
func (m *Fs) validatePath(p string) Result {
if m.root == "/" {
return m.path(p), nil
return Result{m.path(p), true}
}
// Split the cleaned path into components
parts := strings.Split(filepath.Clean("/"+p), string(os.PathSeparator))
parts := Split(filepath.Clean("/"+p), string(os.PathSeparator))
current := m.root
for _, part := range parts {
@ -71,67 +66,77 @@ func (m *Fs) validatePath(p string) (string, error) {
current = next
continue
}
return "", err
return Result{err, false}
}
// Verify the resolved part is still within the root
rel, err := filepath.Rel(m.root, realNext)
if err != nil || strings.HasPrefix(rel, "..") {
if err != nil || HasPrefix(rel, "..") {
// Security event: sandbox escape attempt
username := "unknown"
if u, err := user.Current(); err == nil {
username = u.Username
}
fmt.Fprintf(os.Stderr, "[%s] SECURITY sandbox escape detected root=%s path=%s attempted=%s user=%s\n",
Print(os.Stderr, "[%s] SECURITY sandbox escape detected root=%s path=%s attempted=%s user=%s",
time.Now().Format(time.RFC3339), m.root, p, realNext, username)
return "", os.ErrPermission // Path escapes sandbox
if err == nil {
err = E("fs.validatePath", Concat("sandbox escape: ", p, " resolves outside ", m.root), nil)
}
return Result{err, false}
}
current = realNext
}
return current, nil
return Result{current, true}
}
// Read returns file contents as string.
func (m *Fs) Read(p string) (string, error) {
full, err := m.validatePath(p)
if err != nil {
return "", err
func (m *Fs) Read(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
data, err := os.ReadFile(full)
data, err := os.ReadFile(vp.Value.(string))
if err != nil {
return "", err
return Result{err, false}
}
return string(data), nil
return Result{string(data), true}
}
// Write saves content to file, creating parent directories as needed.
// Files are created with mode 0644. For sensitive files (keys, secrets),
// use WriteMode with 0600.
func (m *Fs) Write(p, content string) error {
func (m *Fs) Write(p, content string) Result {
return m.WriteMode(p, content, 0644)
}
// WriteMode saves content to file with explicit permissions.
// Use 0600 for sensitive files (encryption output, private keys, auth hashes).
func (m *Fs) WriteMode(p, content string, mode os.FileMode) error {
full, err := m.validatePath(p)
if err != nil {
return err
func (m *Fs) WriteMode(p, content string, mode os.FileMode) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
full := vp.Value.(string)
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
return err
return Result{err, false}
}
return os.WriteFile(full, []byte(content), mode)
if err := os.WriteFile(full, []byte(content), mode); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
// EnsureDir creates directory if it doesn't exist.
func (m *Fs) EnsureDir(p string) error {
full, err := m.validatePath(p)
if err != nil {
return err
func (m *Fs) EnsureDir(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
return os.MkdirAll(full, 0755)
if err := os.MkdirAll(vp.Value.(string), 0755); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
// IsDir returns true if path is a directory.
@ -139,11 +144,11 @@ func (m *Fs) IsDir(p string) bool {
if p == "" {
return false
}
full, err := m.validatePath(p)
if err != nil {
vp := m.validatePath(p)
if !vp.OK {
return false
}
info, err := os.Stat(full)
info, err := os.Stat(vp.Value.(string))
return err == nil && info.IsDir()
}
@ -152,118 +157,131 @@ func (m *Fs) IsFile(p string) bool {
if p == "" {
return false
}
full, err := m.validatePath(p)
if err != nil {
vp := m.validatePath(p)
if !vp.OK {
return false
}
info, err := os.Stat(full)
info, err := os.Stat(vp.Value.(string))
return err == nil && info.Mode().IsRegular()
}
// Exists returns true if path exists.
func (m *Fs) Exists(p string) bool {
full, err := m.validatePath(p)
if err != nil {
vp := m.validatePath(p)
if !vp.OK {
return false
}
_, err = os.Stat(full)
_, err := os.Stat(vp.Value.(string))
return err == nil
}
// List returns directory entries.
func (m *Fs) List(p string) ([]fs.DirEntry, error) {
full, err := m.validatePath(p)
if err != nil {
return nil, err
func (m *Fs) List(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
return os.ReadDir(full)
return Result{}.Result(os.ReadDir(vp.Value.(string)))
}
// Stat returns file info.
func (m *Fs) Stat(p string) (fs.FileInfo, error) {
full, err := m.validatePath(p)
if err != nil {
return nil, err
func (m *Fs) Stat(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
return os.Stat(full)
return Result{}.Result(os.Stat(vp.Value.(string)))
}
// Open opens the named file for reading.
func (m *Fs) Open(p string) (fs.File, error) {
full, err := m.validatePath(p)
if err != nil {
return nil, err
func (m *Fs) Open(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
return os.Open(full)
return Result{}.Result(os.Open(vp.Value.(string)))
}
// Create creates or truncates the named file.
func (m *Fs) Create(p string) (io.WriteCloser, error) {
full, err := m.validatePath(p)
if err != nil {
return nil, err
func (m *Fs) Create(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
full := vp.Value.(string)
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
return nil, err
return Result{err, false}
}
return os.Create(full)
return Result{}.Result(os.Create(full))
}
// Append opens the named file for appending, creating it if it doesn't exist.
func (m *Fs) Append(p string) (io.WriteCloser, error) {
full, err := m.validatePath(p)
if err != nil {
return nil, err
func (m *Fs) Append(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
full := vp.Value.(string)
if err := os.MkdirAll(filepath.Dir(full), 0755); err != nil {
return nil, err
return Result{err, false}
}
return os.OpenFile(full, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644)
return Result{}.Result(os.OpenFile(full, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0644))
}
// ReadStream returns a reader for the file content.
func (m *Fs) ReadStream(path string) (io.ReadCloser, error) {
func (m *Fs) ReadStream(path string) Result {
return m.Open(path)
}
// WriteStream returns a writer for the file content.
func (m *Fs) WriteStream(path string) (io.WriteCloser, error) {
func (m *Fs) WriteStream(path string) Result {
return m.Create(path)
}
// Delete removes a file or empty directory.
func (m *Fs) Delete(p string) error {
full, err := m.validatePath(p)
if err != nil {
return err
func (m *Fs) Delete(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
full := vp.Value.(string)
if full == "/" || full == os.Getenv("HOME") {
return E("core.Delete", "refusing to delete protected path: "+full, nil)
return Result{E("fs.Delete", Concat("refusing to delete protected path: ", full), nil), false}
}
return os.Remove(full)
if err := os.Remove(full); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
// DeleteAll removes a file or directory recursively.
func (m *Fs) DeleteAll(p string) error {
full, err := m.validatePath(p)
if err != nil {
return err
func (m *Fs) DeleteAll(p string) Result {
vp := m.validatePath(p)
if !vp.OK {
return vp
}
full := vp.Value.(string)
if full == "/" || full == os.Getenv("HOME") {
return E("core.DeleteAll", "refusing to delete protected path: "+full, nil)
return Result{E("fs.DeleteAll", Concat("refusing to delete protected path: ", full), nil), false}
}
return os.RemoveAll(full)
if err := os.RemoveAll(full); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
// Rename moves a file or directory.
func (m *Fs) Rename(oldPath, newPath string) error {
oldFull, err := m.validatePath(oldPath)
if err != nil {
return err
func (m *Fs) Rename(oldPath, newPath string) Result {
oldVp := m.validatePath(oldPath)
if !oldVp.OK {
return oldVp
}
newFull, err := m.validatePath(newPath)
if err != nil {
return err
newVp := m.validatePath(newPath)
if !newVp.OK {
return newVp
}
return os.Rename(oldFull, newFull)
if err := os.Rename(oldVp.Value.(string), newVp.Value.(string)); err != nil {
return Result{err, false}
}
return Result{OK: true}
}

View file

@ -13,8 +13,8 @@ import (
// Translator defines the interface for translation services.
// Implemented by go-i18n's Srv.
type Translator interface {
// T translates a message by its ID with optional arguments.
T(messageID string, args ...any) string
// Translate translates a message by its ID with optional arguments.
Translate(messageID string, args ...any) Result
// SetLanguage sets the active language (BCP47 tag, e.g., "en-GB", "de").
SetLanguage(lang string) error
// Language returns the current language code.
@ -44,10 +44,10 @@ type LocaleProvider interface {
type I18n struct {
mu sync.RWMutex
locales []*Embed // collected from LocaleProvider services
locale string
translator Translator // registered implementation (nil until set)
}
// AddLocales adds locale mounts (called during service registration).
func (i *I18n) AddLocales(mounts ...*Embed) {
i.mu.Lock()
@ -56,12 +56,12 @@ func (i *I18n) AddLocales(mounts ...*Embed) {
}
// Locales returns all collected locale mounts.
func (i *I18n) Locales() []*Embed {
func (i *I18n) Locales() Result {
i.mu.RLock()
out := make([]*Embed, len(i.locales))
copy(out, i.locales)
i.mu.RUnlock()
return out
return Result{out, true}
}
// SetTranslator registers the translation implementation.
@ -69,46 +69,59 @@ func (i *I18n) Locales() []*Embed {
func (i *I18n) SetTranslator(t Translator) {
i.mu.Lock()
i.translator = t
locale := i.locale
i.mu.Unlock()
if t != nil && locale != "" {
_ = t.SetLanguage(locale)
}
}
// Translator returns the registered translation implementation, or nil.
func (i *I18n) Translator() Translator {
func (i *I18n) Translator() Result {
i.mu.RLock()
t := i.translator
i.mu.RUnlock()
return t
if t == nil {
return Result{}
}
return Result{t, true}
}
// T translates a message. Returns the key as-is if no translator is registered.
func (i *I18n) T(messageID string, args ...any) string {
// Translate translates a message. Returns the key as-is if no translator is registered.
func (i *I18n) Translate(messageID string, args ...any) Result {
i.mu.RLock()
t := i.translator
i.mu.RUnlock()
if t != nil {
return t.T(messageID, args...)
return t.Translate(messageID, args...)
}
return messageID
return Result{messageID, true}
}
// SetLanguage sets the active language. No-op if no translator is registered.
func (i *I18n) SetLanguage(lang string) error {
i.mu.RLock()
// SetLanguage sets the active language and forwards to the translator if registered.
func (i *I18n) SetLanguage(lang string) Result {
if lang == "" {
return Result{OK: true}
}
i.mu.Lock()
i.locale = lang
t := i.translator
i.mu.RUnlock()
i.mu.Unlock()
if t != nil {
return t.SetLanguage(lang)
if err := t.SetLanguage(lang); err != nil {
return Result{err, false}
}
return nil
}
return Result{OK: true}
}
// Language returns the current language code, or "en" if no translator.
// Language returns the current language code, or "en" if not set.
func (i *I18n) Language() string {
i.mu.RLock()
t := i.translator
locale := i.locale
i.mu.RUnlock()
if t != nil {
return t.Language()
if locale != "" {
return locale
}
return "en"
}

View file

@ -7,7 +7,6 @@
package core
import (
"errors"
"slices"
"sync"
)
@ -15,7 +14,7 @@ import (
// Ipc holds IPC dispatch data.
type Ipc struct {
ipcMu sync.RWMutex
ipcHandlers []func(*Core, Message) error
ipcHandlers []func(*Core, Message) Result
queryMu sync.RWMutex
queryHandlers []QueryHandler
@ -24,51 +23,46 @@ type Ipc struct {
taskHandlers []TaskHandler
}
func (c *Core) Action(msg Message) error {
func (c *Core) Action(msg Message) Result {
c.ipc.ipcMu.RLock()
handlers := slices.Clone(c.ipc.ipcHandlers)
c.ipc.ipcMu.RUnlock()
var agg error
for _, h := range handlers {
if err := h(c, msg); err != nil {
agg = errors.Join(agg, err)
if r := h(c, msg); !r.OK {
return r
}
}
return agg
return Result{OK: true}
}
func (c *Core) Query(q Query) (any, bool, error) {
func (c *Core) Query(q Query) Result {
c.ipc.queryMu.RLock()
handlers := slices.Clone(c.ipc.queryHandlers)
c.ipc.queryMu.RUnlock()
for _, h := range handlers {
result, handled, err := h(c, q)
if handled {
return result, true, err
r := h(c, q)
if r.OK {
return r
}
}
return nil, false, nil
return Result{}
}
func (c *Core) QueryAll(q Query) ([]any, error) {
func (c *Core) QueryAll(q Query) Result {
c.ipc.queryMu.RLock()
handlers := slices.Clone(c.ipc.queryHandlers)
c.ipc.queryMu.RUnlock()
var results []any
var agg error
for _, h := range handlers {
result, handled, err := h(c, q)
if err != nil {
agg = errors.Join(agg, err)
}
if handled && result != nil {
results = append(results, result)
r := h(c, q)
if r.OK && r.Value != nil {
results = append(results, r.Value)
}
}
return results, agg
return Result{results, true}
}
func (c *Core) RegisterQuery(handler QueryHandler) {

View file

@ -5,7 +5,6 @@
package core
import (
"slices"
"sync"
)
@ -18,7 +17,7 @@ var (
// Lock is the DTO for a named mutex.
type Lock struct {
Name string
Mu *sync.RWMutex
Mutex *sync.RWMutex
}
// Lock returns a named Lock, creating the mutex if needed.
@ -30,7 +29,7 @@ func (c *Core) Lock(name string) *Lock {
lockMap[name] = m
}
lockMu.Unlock()
return &Lock{Name: name, Mu: m}
return &Lock{Name: name, Mutex: m}
}
// LockEnable marks that the service lock should be applied after initialisation.
@ -39,9 +38,9 @@ func (c *Core) LockEnable(name ...string) {
if len(name) > 0 {
n = name[0]
}
c.Lock(n).Mu.Lock()
defer c.Lock(n).Mu.Unlock()
c.srv.lockEnabled = true
c.Lock(n).Mutex.Lock()
defer c.Lock(n).Mutex.Unlock()
c.services.lockEnabled = true
}
// LockApply activates the service lock if it was enabled.
@ -50,25 +49,41 @@ func (c *Core) LockApply(name ...string) {
if len(name) > 0 {
n = name[0]
}
c.Lock(n).Mu.Lock()
defer c.Lock(n).Mu.Unlock()
if c.srv.lockEnabled {
c.srv.locked = true
c.Lock(n).Mutex.Lock()
defer c.Lock(n).Mutex.Unlock()
if c.services.lockEnabled {
c.services.locked = true
}
}
// Startables returns a snapshot of services implementing Startable.
func (c *Core) Startables() []Startable {
c.Lock("srv").Mu.RLock()
out := slices.Clone(c.srv.startables)
c.Lock("srv").Mu.RUnlock()
return out
// Startables returns services that have an OnStart function.
func (c *Core) Startables() Result {
if c.services == nil {
return Result{}
}
c.Lock("srv").Mutex.RLock()
defer c.Lock("srv").Mutex.RUnlock()
var out []*Service
for _, svc := range c.services.services {
if svc.OnStart != nil {
out = append(out, svc)
}
}
return Result{out, true}
}
// Stoppables returns a snapshot of services implementing Stoppable.
func (c *Core) Stoppables() []Stoppable {
c.Lock("srv").Mu.RLock()
out := slices.Clone(c.srv.stoppables)
c.Lock("srv").Mu.RUnlock()
return out
// Stoppables returns services that have an OnStop function.
func (c *Core) Stoppables() Result {
if c.services == nil {
return Result{}
}
c.Lock("srv").Mutex.RLock()
defer c.Lock("srv").Mutex.RUnlock()
var out []*Service
for _, svc := range c.services.services {
if svc.OnStop != nil {
out = append(out, svc)
}
}
return Result{out, true}
}

View file

@ -6,12 +6,12 @@
package core
import (
"fmt"
goio "io"
"os"
"os/user"
"slices"
"sync"
"sync/atomic"
"time"
)
@ -68,8 +68,8 @@ type Log struct {
StyleSecurity func(string) string
}
// RotationLogOpts defines the log rotation and retention policy.
type RotationLogOpts struct {
// RotationLogOptions defines the log rotation and retention policy.
type RotationLogOptions struct {
// Filename is the log file path. If empty, rotation is disabled.
Filename string
@ -91,24 +91,24 @@ type RotationLogOpts struct {
Compress bool
}
// LogOpts configures a Log.
type LogOpts struct {
// LogOptions configures a Log.
type LogOptions struct {
Level Level
// Output is the destination for log messages. If Rotation is provided,
// Output is ignored and logs are written to the rotating file instead.
Output goio.Writer
// Rotation enables log rotation to file. If provided, Filename must be set.
Rotation *RotationLogOpts
Rotation *RotationLogOptions
// RedactKeys is a list of keys whose values should be masked in logs.
RedactKeys []string
}
// RotationWriterFactory creates a rotating writer from options.
// Set this to enable log rotation (provided by core/go-io integration).
var RotationWriterFactory func(RotationLogOpts) goio.WriteCloser
var RotationWriterFactory func(RotationLogOptions) goio.WriteCloser
// New creates a new Log with the given options.
func NewLog(opts LogOpts) *Log {
func NewLog(opts LogOptions) *Log {
output := opts.Output
if opts.Rotation != nil && opts.Rotation.Filename != "" && RotationWriterFactory != nil {
output = RotationWriterFactory(*opts.Rotation)
@ -183,7 +183,7 @@ func (l *Log) log(level Level, prefix, msg string, keyvals ...any) {
for i := 0; i < origLen; i += 2 {
if i+1 < origLen {
if err, ok := keyvals[i+1].(error); ok {
if op := Op(err); op != "" {
if op := Operation(err); op != "" {
// Check if op is already in keyvals
hasOp := false
for j := 0; j < len(keyvals); j += 2 {
@ -228,21 +228,21 @@ func (l *Log) log(level Level, prefix, msg string, keyvals ...any) {
}
// Redaction logic
keyStr := fmt.Sprintf("%v", key)
keyStr := Sprint(key)
if slices.Contains(redactKeys, keyStr) {
val = "[REDACTED]"
}
// Secure formatting to prevent log injection
if s, ok := val.(string); ok {
kvStr += fmt.Sprintf("%v=%q", key, s)
kvStr += Sprintf("%v=%q", key, s)
} else {
kvStr += fmt.Sprintf("%v=%v", key, val)
kvStr += Sprintf("%v=%v", key, val)
}
}
}
_, _ = fmt.Fprintf(output, "%s %s %s%s\n", timestamp, prefix, msg, kvStr)
Print(output, "%s %s %s%s", timestamp, prefix, msg, kvStr)
}
// Debug logs a debug message with optional key-value pairs.
@ -297,51 +297,56 @@ func Username() string {
// --- Default logger ---
var defaultLog = NewLog(LogOpts{Level: LevelInfo})
var defaultLogPtr atomic.Pointer[Log]
func init() {
l := NewLog(LogOptions{Level: LevelInfo})
defaultLogPtr.Store(l)
}
// Default returns the default logger.
func Default() *Log {
return defaultLog
return defaultLogPtr.Load()
}
// SetDefault sets the default logger.
func SetDefault(l *Log) {
defaultLog = l
defaultLogPtr.Store(l)
}
// SetLevel sets the default logger's level.
func SetLevel(level Level) {
defaultLog.SetLevel(level)
Default().SetLevel(level)
}
// SetRedactKeys sets the default logger's redaction keys.
func SetRedactKeys(keys ...string) {
defaultLog.SetRedactKeys(keys...)
Default().SetRedactKeys(keys...)
}
// Debug logs to the default logger.
func Debug(msg string, keyvals ...any) {
defaultLog.Debug(msg, keyvals...)
Default().Debug(msg, keyvals...)
}
// Info logs to the default logger.
func Info(msg string, keyvals ...any) {
defaultLog.Info(msg, keyvals...)
Default().Info(msg, keyvals...)
}
// Warn logs to the default logger.
func Warn(msg string, keyvals ...any) {
defaultLog.Warn(msg, keyvals...)
Default().Warn(msg, keyvals...)
}
// Error logs to the default logger.
func Error(msg string, keyvals ...any) {
defaultLog.Error(msg, keyvals...)
Default().Error(msg, keyvals...)
}
// Security logs to the default logger.
func Security(msg string, keyvals ...any) {
defaultLog.Security(msg, keyvals...)
Default().Security(msg, keyvals...)
}
// --- LogErr: Error-Aware Logger ---
@ -362,36 +367,36 @@ func (le *LogErr) Log(err error) {
if err == nil {
return
}
le.log.Error(ErrorMessage(err), "op", Op(err), "code", ErrCode(err), "stack", FormatStackTrace(err))
le.log.Error(ErrorMessage(err), "op", Operation(err), "code", ErrorCode(err), "stack", FormatStackTrace(err))
}
// --- LogPan: Panic-Aware Logger ---
// --- LogPanic: Panic-Aware Logger ---
// LogPan logs panic context without crash file management.
// LogPanic logs panic context without crash file management.
// Primary action: log. Secondary: recover panics.
type LogPan struct {
type LogPanic struct {
log *Log
}
// NewLogPan creates a LogPan bound to the given logger.
func NewLogPan(log *Log) *LogPan {
return &LogPan{log: log}
// NewLogPanic creates a LogPanic bound to the given logger.
func NewLogPanic(log *Log) *LogPanic {
return &LogPanic{log: log}
}
// Recover captures a panic and logs it. Does not write crash files.
// Use as: defer core.NewLogPan(logger).Recover()
func (lp *LogPan) Recover() {
// Use as: defer core.NewLogPanic(logger).Recover()
func (lp *LogPanic) Recover() {
r := recover()
if r == nil {
return
}
err, ok := r.(error)
if !ok {
err = fmt.Errorf("%v", r)
err = NewError(Sprint("panic: ", r))
}
lp.log.Error("panic recovered",
"err", err,
"op", Op(err),
"op", Operation(err),
"stack", FormatStackTrace(err),
)
}

140
pkg/core/options.go Normal file
View file

@ -0,0 +1,140 @@
// SPDX-License-Identifier: EUPL-1.2
// Core primitives: Option, Options, Result.
//
// Option is a single key-value pair. Options is a collection.
// Any function that returns Result can accept Options.
//
// 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"},
// })
package core
// 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()) }
type Result struct {
Value any
OK bool
}
// Result gets or sets the value. Zero args returns Value. With args, maps
// Go (value, error) pairs to Result and returns self.
//
// r.Result(file, err) // OK = err == nil, Value = file
// r.Result(value) // OK = true, Value = value
// r.Result() // after set — returns the value
func (r Result) Result(args ...any) Result {
if len(args) == 0 {
return r
}
if len(args) == 1 {
return Result{args[0], true}
}
if err, ok := args[len(args)-1].(error); ok {
if err != nil {
return Result{err, false}
}
return Result{args[0], true}
}
return Result{args[0], true}
}
// Option is a single key-value configuration pair.
//
// core.Option{Key: "name", Value: "brain"}
// core.Option{Key: "port", Value: 8080}
type Option struct {
Key string
Value any
}
// Options is a collection of Option items.
// The universal input type for Core operations.
//
// opts := core.Options{{Key: "name", Value: "myapp"}}
// name := opts.String("name")
type Options []Option
// 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 {
if opt.Key == key {
return Result{opt.Value, true}
}
}
return Result{}
}
// Has returns true if a key exists.
//
// if opts.Has("debug") { ... }
func (o Options) Has(key string) bool {
return o.Get(key).OK
}
// String retrieves a string value, empty string if missing.
//
// name := opts.String("name")
func (o Options) String(key string) string {
r := o.Get(key)
if !r.OK {
return ""
}
s, _ := r.Value.(string)
return s
}
// Int retrieves an int value, 0 if missing.
//
// port := opts.Int("port")
func (o Options) Int(key string) int {
r := o.Get(key)
if !r.OK {
return 0
}
i, _ := r.Value.(int)
return i
}
// Bool retrieves a bool value, false if missing.
//
// debug := opts.Bool("debug")
func (o Options) Bool(key string) bool {
r := o.Get(key)
if !r.OK {
return false
}
b, _ := r.Value.(bool)
return b
}

View file

@ -8,8 +8,6 @@ package core
import (
"context"
"errors"
"fmt"
"maps"
"slices"
)
@ -28,57 +26,71 @@ func NewServiceRuntime[T any](c *Core, opts T) *ServiceRuntime[T] {
}
func (r *ServiceRuntime[T]) Core() *Core { return r.core }
func (r *ServiceRuntime[T]) Opts() T { return r.opts }
func (r *ServiceRuntime[T]) Options() T { return r.opts }
func (r *ServiceRuntime[T]) Config() *Config { return r.core.Config() }
// --- Lifecycle ---
// ServiceStartup runs the startup lifecycle for all registered services.
func (c *Core) ServiceStartup(ctx context.Context, options any) error {
// ServiceStartup runs OnStart for all registered services that have one.
func (c *Core) ServiceStartup(ctx context.Context, options any) Result {
c.shutdown.Store(false)
c.context, c.cancel = context.WithCancel(ctx)
startables := c.Startables()
var agg error
for _, s := range startables {
if startables.OK {
for _, s := range startables.Value.([]*Service) {
if err := ctx.Err(); err != nil {
return errors.Join(agg, err)
return Result{err, false}
}
if err := s.OnStartup(ctx); err != nil {
agg = errors.Join(agg, err)
r := s.OnStart()
if !r.OK {
return r
}
}
if err := c.ACTION(ActionServiceStartup{}); err != nil {
agg = errors.Join(agg, err)
}
return agg
c.ACTION(ActionServiceStartup{})
return Result{OK: true}
}
// ServiceShutdown runs the shutdown lifecycle for all registered services.
func (c *Core) ServiceShutdown(ctx context.Context) error {
// ServiceShutdown drains background tasks, then stops all registered services.
func (c *Core) ServiceShutdown(ctx context.Context) Result {
c.shutdown.Store(true)
var agg error
if err := c.ACTION(ActionServiceShutdown{}); err != nil {
agg = errors.Join(agg, err)
}
stoppables := c.Stoppables()
for _, s := range slices.Backward(stoppables) {
if err := ctx.Err(); err != nil {
agg = errors.Join(agg, err)
break
}
if err := s.OnShutdown(ctx); err != nil {
agg = errors.Join(agg, err)
}
}
c.cancel() // signal all context-aware tasks to stop
c.ACTION(ActionServiceShutdown{})
// Drain background tasks before stopping services.
done := make(chan struct{})
go func() {
c.wg.Wait()
c.waitGroup.Wait()
close(done)
}()
select {
case <-done:
case <-ctx.Done():
agg = errors.Join(agg, ctx.Err())
return Result{ctx.Err(), false}
}
return agg
// Stop services
var firstErr error
stoppables := c.Stoppables()
if stoppables.OK {
for _, s := range stoppables.Value.([]*Service) {
if err := ctx.Err(); err != nil {
return Result{err, false}
}
r := s.OnStop()
if !r.OK && firstErr == nil {
if e, ok := r.Value.(error); ok {
firstErr = e
} else {
firstErr = E("core.ServiceShutdown", Sprint("service OnStop failed: ", r.Value), nil)
}
}
}
}
if firstErr != nil {
return Result{firstErr, false}
}
return Result{OK: true}
}
// --- Runtime DTO (GUI binding) ---
@ -89,44 +101,49 @@ type Runtime struct {
Core *Core
}
// ServiceFactory defines a function that creates a service instance.
type ServiceFactory func() (any, error)
// ServiceFactory defines a function that creates a Service.
type ServiceFactory func() Result
// NewWithFactories creates a Runtime with the provided service factories.
func NewWithFactories(app any, factories map[string]ServiceFactory) (*Runtime, error) {
coreOpts := []Option{WithApp(app)}
func NewWithFactories(app any, factories map[string]ServiceFactory) Result {
c := New(Options{{Key: "name", Value: "core"}})
c.app.Runtime = app
names := slices.Sorted(maps.Keys(factories))
for _, name := range names {
factory := factories[name]
if factory == nil {
return nil, E("core.NewWithFactories", fmt.Sprintf("factory is nil for service %q", name), nil)
continue
}
svc, err := factory()
if err != nil {
return nil, E("core.NewWithFactories", fmt.Sprintf("failed to create service %q", name), err)
r := factory()
if !r.OK {
cause, _ := r.Value.(error)
return Result{E("core.NewWithFactories", Concat("factory \"", name, "\" failed"), cause), false}
}
svcCopy := svc
coreOpts = append(coreOpts, WithName(name, func(c *Core) (any, error) { return svcCopy, nil }))
svc, ok := r.Value.(Service)
if !ok {
return Result{E("core.NewWithFactories", Concat("factory \"", name, "\" returned non-Service type"), nil), false}
}
coreInstance, err := New(coreOpts...)
if err != nil {
return nil, err
sr := c.Service(name, svc)
if !sr.OK {
return sr
}
return &Runtime{app: app, Core: coreInstance}, nil
}
return Result{&Runtime{app: app, Core: c}, true}
}
// NewRuntime creates a Runtime with no custom services.
func NewRuntime(app any) (*Runtime, error) {
func NewRuntime(app any) Result {
return NewWithFactories(app, map[string]ServiceFactory{})
}
func (r *Runtime) ServiceName() string { return "Core" }
func (r *Runtime) ServiceStartup(ctx context.Context, options any) error {
func (r *Runtime) ServiceStartup(ctx context.Context, options any) Result {
return r.Core.ServiceStartup(ctx, options)
}
func (r *Runtime) ServiceShutdown(ctx context.Context) error {
func (r *Runtime) ServiceShutdown(ctx context.Context) Result {
if r.Core != nil {
return r.Core.ServiceShutdown(ctx)
}
return nil
return Result{OK: true}
}

View file

@ -1,71 +1,83 @@
// SPDX-License-Identifier: EUPL-1.2
// Service registry, lifecycle tracking, and runtime helpers for the Core framework.
// Service registry for the Core framework.
//
// Register a service:
//
// c.Service("auth", core.Service{})
//
// Get a service:
//
// r := c.Service("auth")
// if r.OK { svc := r.Value }
package core
import "fmt"
// No imports needed — uses package-level string helpers.
// --- Service Registry DTO ---
// Service holds service registry data.
// Service is a managed component with optional lifecycle.
type Service struct {
Services map[string]any
startables []Startable
stoppables []Stoppable
Name string
Options Options
OnStart func() Result
OnStop func() Result
OnReload func() Result
}
// serviceRegistry holds registered services.
type serviceRegistry struct {
services map[string]*Service
lockEnabled bool
locked bool
}
// --- Core service methods ---
// Service gets or registers a service.
// Service gets or registers a service by name.
//
// c.Service() // returns *Service
// c.Service("auth") // returns the "auth" service
// c.Service("auth", myService) // registers "auth"
func (c *Core) Service(args ...any) any {
switch len(args) {
case 0:
return c.srv
case 1:
name, _ := args[0].(string)
c.Lock("srv").Mu.RLock()
v, ok := c.srv.Services[name]
c.Lock("srv").Mu.RUnlock()
if !ok {
return nil
// c.Service("auth", core.Service{OnStart: startFn})
// r := c.Service("auth")
func (c *Core) Service(name string, service ...Service) Result {
if len(service) == 0 {
c.Lock("srv").Mutex.RLock()
v, ok := c.services.services[name]
c.Lock("srv").Mutex.RUnlock()
return Result{v, ok}
}
return v
default:
name, _ := args[0].(string)
if name == "" {
return E("core.Service", "service name cannot be empty", nil)
return Result{E("core.Service", "service name cannot be empty", nil), false}
}
c.Lock("srv").Mu.Lock()
defer c.Lock("srv").Mu.Unlock()
if c.srv.locked {
return E("core.Service", fmt.Sprintf("service %q is not permitted by the serviceLock setting", name), nil)
c.Lock("srv").Mutex.Lock()
defer c.Lock("srv").Mutex.Unlock()
if c.services.locked {
return Result{E("core.Service", Concat("service \"", name, "\" not permitted — registry locked"), nil), false}
}
if _, exists := c.srv.Services[name]; exists {
return E("core.Service", fmt.Sprintf("service %q already registered", name), nil)
}
svc := args[1]
if c.srv.Services == nil {
c.srv.Services = make(map[string]any)
}
c.srv.Services[name] = svc
if st, ok := svc.(Startable); ok {
c.srv.startables = append(c.srv.startables, st)
}
if st, ok := svc.(Stoppable); ok {
c.srv.stoppables = append(c.srv.stoppables, st)
}
if lp, ok := svc.(LocaleProvider); ok {
c.i18n.AddLocales(lp.Locales())
}
return nil
if _, exists := c.services.services[name]; exists {
return Result{E("core.Service", Join(" ", "service", name, "already registered"), nil), false}
}
srv := &service[0]
srv.Name = name
c.services.services[name] = srv
return Result{OK: true}
}
// Services returns all registered service names.
//
// names := c.Services()
func (c *Core) Services() []string {
if c.services == nil {
return nil
}
c.Lock("srv").Mutex.RLock()
defer c.Lock("srv").Mutex.RUnlock()
var names []string
for k := range c.services.services {
names = append(names, k)
}
return names
}

157
pkg/core/string.go Normal file
View file

@ -0,0 +1,157 @@
// SPDX-License-Identifier: EUPL-1.2
// String operations for the Core framework.
// Provides safe, predictable string helpers that downstream packages
// use directly — same pattern as Array[T] for slices.
package core
import (
"fmt"
"strings"
"unicode/utf8"
)
// HasPrefix returns true if s starts with prefix.
//
// core.HasPrefix("--verbose", "--") // true
func HasPrefix(s, prefix string) bool {
return strings.HasPrefix(s, prefix)
}
// HasSuffix returns true if s ends with suffix.
//
// core.HasSuffix("test.go", ".go") // true
func HasSuffix(s, suffix string) bool {
return strings.HasSuffix(s, suffix)
}
// TrimPrefix removes prefix from s.
//
// core.TrimPrefix("--verbose", "--") // "verbose"
func TrimPrefix(s, prefix string) string {
return strings.TrimPrefix(s, prefix)
}
// TrimSuffix removes suffix from s.
//
// core.TrimSuffix("test.go", ".go") // "test"
func TrimSuffix(s, suffix string) string {
return strings.TrimSuffix(s, suffix)
}
// Contains returns true if s contains substr.
//
// core.Contains("hello world", "world") // true
func Contains(s, substr string) bool {
return strings.Contains(s, substr)
}
// Split splits s by separator.
//
// core.Split("a/b/c", "/") // ["a", "b", "c"]
func Split(s, sep string) []string {
return strings.Split(s, sep)
}
// SplitN splits s by separator into at most n parts.
//
// core.SplitN("key=value=extra", "=", 2) // ["key", "value=extra"]
func SplitN(s, sep string, n int) []string {
return strings.SplitN(s, sep, n)
}
// Join joins parts with a separator, building via Concat.
//
// core.Join("/", "deploy", "to", "homelab") // "deploy/to/homelab"
// core.Join(".", "cmd", "deploy", "description") // "cmd.deploy.description"
func Join(sep string, parts ...string) string {
if len(parts) == 0 {
return ""
}
result := parts[0]
for _, p := range parts[1:] {
result = Concat(result, sep, p)
}
return result
}
// Replace replaces all occurrences of old with new in s.
//
// core.Replace("deploy/to/homelab", "/", ".") // "deploy.to.homelab"
func Replace(s, old, new string) string {
return strings.ReplaceAll(s, old, new)
}
// Lower returns s in lowercase.
//
// core.Lower("HELLO") // "hello"
func Lower(s string) string {
return strings.ToLower(s)
}
// Upper returns s in uppercase.
//
// core.Upper("hello") // "HELLO"
func Upper(s string) string {
return strings.ToUpper(s)
}
// Trim removes leading and trailing whitespace.
//
// core.Trim(" hello ") // "hello"
func Trim(s string) string {
return strings.TrimSpace(s)
}
// RuneCount returns the number of runes (unicode characters) in s.
//
// core.RuneCount("hello") // 5
// core.RuneCount("🔥") // 1
func RuneCount(s string) int {
return utf8.RuneCountInString(s)
}
// NewBuilder returns a new strings.Builder.
//
// b := core.NewBuilder()
// b.WriteString("hello")
// b.String() // "hello"
func NewBuilder() *strings.Builder {
return &strings.Builder{}
}
// NewReader returns a strings.NewReader for the given string.
//
// r := core.NewReader("hello world")
func NewReader(s string) *strings.Reader {
return strings.NewReader(s)
}
// Sprint converts any value to its string representation.
//
// core.Sprint(42) // "42"
// core.Sprint(err) // "connection refused"
func Sprint(args ...any) string {
return fmt.Sprint(args...)
}
// Sprintf formats a string with the given arguments.
//
// core.Sprintf("%v=%q", "key", "value") // `key="value"`
func Sprintf(format string, args ...any) string {
return fmt.Sprintf(format, args...)
}
// Concat joins variadic string parts into one string.
// Hook point for validation, sanitisation, and security checks.
//
// core.Concat("cmd.", "deploy.to.homelab", ".description")
// core.Concat("https://", host, "/api/v1")
func Concat(parts ...string) string {
b := NewBuilder()
for _, p := range parts {
b.WriteString(p)
}
return b.String()
}

View file

@ -5,64 +5,81 @@
package core
import (
"fmt"
"reflect"
"slices"
"strconv"
)
// TaskState holds background task state.
type TaskState struct {
ID string
Identifier string
Task Task
Result any
Error error
}
// PerformAsync dispatches a task in a background goroutine.
func (c *Core) PerformAsync(t Task) string {
func (c *Core) PerformAsync(t Task) Result {
if c.shutdown.Load() {
return ""
return Result{}
}
taskID := fmt.Sprintf("task-%d", c.taskIDCounter.Add(1))
if tid, ok := t.(TaskWithID); ok {
tid.SetTaskID(taskID)
taskID := Concat("task-", strconv.FormatUint(c.taskIDCounter.Add(1), 10))
if tid, ok := t.(TaskWithIdentifier); ok {
tid.SetTaskIdentifier(taskID)
}
_ = c.ACTION(ActionTaskStarted{TaskID: taskID, Task: t})
c.wg.Go(func() {
result, handled, err := c.PERFORM(t)
if !handled && err == nil {
err = E("core.PerformAsync", fmt.Sprintf("no handler found for task type %T", t), nil)
c.ACTION(ActionTaskStarted{TaskIdentifier: taskID, Task: t})
c.waitGroup.Go(func() {
defer func() {
if rec := recover(); rec != nil {
err := E("core.PerformAsync", Sprint("panic: ", rec), nil)
c.ACTION(ActionTaskCompleted{TaskIdentifier: taskID, Task: t, Result: nil, Error: err})
}
_ = c.ACTION(ActionTaskCompleted{TaskID: taskID, Task: t, Result: result, Error: err})
}()
r := c.PERFORM(t)
var err error
if !r.OK {
if e, ok := r.Value.(error); ok {
err = e
} else {
taskType := reflect.TypeOf(t)
typeName := "<nil>"
if taskType != nil {
typeName = taskType.String()
}
err = E("core.PerformAsync", Join(" ", "no handler found for task type", typeName), nil)
}
}
c.ACTION(ActionTaskCompleted{TaskIdentifier: taskID, Task: t, Result: r.Value, Error: err})
})
return taskID
return Result{taskID, true}
}
// Progress broadcasts a progress update for a background task.
func (c *Core) Progress(taskID string, progress float64, message string, t Task) {
_ = c.ACTION(ActionTaskProgress{TaskID: taskID, Task: t, Progress: progress, Message: message})
c.ACTION(ActionTaskProgress{TaskIdentifier: taskID, Task: t, Progress: progress, Message: message})
}
func (c *Core) Perform(t Task) (any, bool, error) {
func (c *Core) Perform(t Task) Result {
c.ipc.taskMu.RLock()
handlers := slices.Clone(c.ipc.taskHandlers)
c.ipc.taskMu.RUnlock()
for _, h := range handlers {
result, handled, err := h(c, t)
if handled {
return result, true, err
r := h(c, t)
if r.OK {
return r
}
}
return nil, false, nil
return Result{}
}
func (c *Core) RegisterAction(handler func(*Core, Message) error) {
func (c *Core) RegisterAction(handler func(*Core, Message) Result) {
c.ipc.ipcMu.Lock()
c.ipc.ipcHandlers = append(c.ipc.ipcHandlers, handler)
c.ipc.ipcMu.Unlock()
}
func (c *Core) RegisterActions(handlers ...func(*Core, Message) error) {
func (c *Core) RegisterActions(handlers ...func(*Core, Message) Result) {
c.ipc.ipcMu.Lock()
c.ipc.ipcHandlers = append(c.ipc.ipcHandlers, handlers...)
c.ipc.ipcMu.Unlock()

159
pkg/core/utils.go Normal file
View file

@ -0,0 +1,159 @@
// SPDX-License-Identifier: EUPL-1.2
// Utility functions for the Core framework.
// Built on core string.go primitives.
package core
import (
"fmt"
"io"
"os"
)
// Print writes a formatted line to a writer, defaulting to os.Stdout.
//
// core.Print(nil, "hello %s", "world") // → stdout
// core.Print(w, "port: %d", 8080) // → w
func Print(w io.Writer, format string, args ...any) {
if w == nil {
w = os.Stdout
}
fmt.Fprintf(w, format+"\n", args...)
}
// JoinPath joins string segments into a path with "/" separator.
//
// core.JoinPath("deploy", "to", "homelab") // → "deploy/to/homelab"
func JoinPath(segments ...string) string {
return Join("/", segments...)
}
// IsFlag returns true if the argument starts with a dash.
//
// core.IsFlag("--verbose") // true
// core.IsFlag("-v") // true
// core.IsFlag("deploy") // false
func IsFlag(arg string) bool {
return HasPrefix(arg, "-")
}
// Arg extracts a value from variadic args at the given index.
// Type-checks and delegates to the appropriate typed extractor.
// Returns Result — OK is false if index is out of bounds.
//
// r := core.Arg(0, args...)
// if r.OK { path = r.Value.(string) }
func Arg(index int, args ...any) Result {
if index >= len(args) {
return Result{}
}
v := args[index]
switch v.(type) {
case string:
return Result{ArgString(index, args...), true}
case int:
return Result{ArgInt(index, args...), true}
case bool:
return Result{ArgBool(index, args...), true}
default:
return Result{v, true}
}
}
// ArgString extracts a string at the given index.
//
// name := core.ArgString(0, args...)
func ArgString(index int, args ...any) string {
if index >= len(args) {
return ""
}
s, ok := args[index].(string)
if !ok {
return ""
}
return s
}
// ArgInt extracts an int at the given index.
//
// port := core.ArgInt(1, args...)
func ArgInt(index int, args ...any) int {
if index >= len(args) {
return 0
}
i, ok := args[index].(int)
if !ok {
return 0
}
return i
}
// ArgBool extracts a bool at the given index.
//
// debug := core.ArgBool(2, args...)
func ArgBool(index int, args ...any) bool {
if index >= len(args) {
return false
}
b, ok := args[index].(bool)
if !ok {
return false
}
return b
}
// FilterArgs removes empty strings and Go test runner flags from an argument list.
//
// clean := core.FilterArgs(os.Args[1:])
func FilterArgs(args []string) []string {
var clean []string
for _, a := range args {
if a == "" || HasPrefix(a, "-test.") {
continue
}
clean = append(clean, a)
}
return clean
}
// ParseFlag parses a single flag argument into key, value, and validity.
// Single dash (-) requires exactly 1 character (letter, emoji, unicode).
// Double dash (--) requires 2+ characters.
//
// "-v" → "v", "", true
// "-🔥" → "🔥", "", true
// "--verbose" → "verbose", "", true
// "--port=8080" → "port", "8080", true
// "-verbose" → "", "", false (single dash, 2+ chars)
// "--v" → "", "", false (double dash, 1 char)
// "hello" → "", "", false (not a flag)
func ParseFlag(arg string) (key, value string, valid bool) {
if HasPrefix(arg, "--") {
rest := TrimPrefix(arg, "--")
parts := SplitN(rest, "=", 2)
name := parts[0]
if RuneCount(name) < 2 {
return "", "", false
}
if len(parts) == 2 {
return name, parts[1], true
}
return name, "", true
}
if HasPrefix(arg, "-") {
rest := TrimPrefix(arg, "-")
parts := SplitN(rest, "=", 2)
name := parts[0]
if RuneCount(name) != 1 {
return "", "", false
}
if len(parts) == 2 {
return name, parts[1], true
}
return name, "", true
}
return "", "", false
}

39
tests/app_test.go Normal file
View file

@ -0,0 +1,39 @@
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{{Key: "name", Value: "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) {
r := 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")
assert.False(t, r.OK)
}

90
tests/array_test.go Normal file
View file

@ -0,0 +1,90 @@
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)
r := a.Filter(func(n int) bool { return n%2 == 0 })
assert.True(t, r.OK)
evens := r.Value.(*Array[int])
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())
}

View file

@ -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())
}

View file

@ -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
View file

@ -0,0 +1,76 @@
package core_test
import (
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- Cli Surface ---
func TestCli_Good(t *testing.T) {
c := New()
assert.NotNil(t, c.Cli())
}
func TestCli_Banner_Good(t *testing.T) {
c := New(Options{{Key: "name", Value: "myapp"}})
assert.Equal(t, "myapp", c.Cli().Banner())
}
func TestCli_SetBanner_Good(t *testing.T) {
c := New()
c.Cli().SetBanner(func(_ *Cli) string { return "Custom Banner" })
assert.Equal(t, "Custom Banner", c.Cli().Banner())
}
func TestCli_Run_Good(t *testing.T) {
c := New()
executed := false
c.Command("hello", Command{Action: func(_ Options) Result {
executed = true
return Result{Value: "world", OK: true}
}})
r := c.Cli().Run("hello")
assert.True(t, r.OK)
assert.Equal(t, "world", r.Value)
assert.True(t, executed)
}
func TestCli_Run_Nested_Good(t *testing.T) {
c := New()
executed := false
c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result {
executed = true
return Result{OK: true}
}})
r := c.Cli().Run("deploy", "to", "homelab")
assert.True(t, r.OK)
assert.True(t, executed)
}
func TestCli_Run_WithFlags_Good(t *testing.T) {
c := New()
var received Options
c.Command("serve", Command{Action: func(opts Options) Result {
received = opts
return Result{OK: true}
}})
c.Cli().Run("serve", "--port=8080", "--debug")
assert.Equal(t, "8080", received.String("port"))
assert.True(t, received.Bool("debug"))
}
func TestCli_Run_NoCommand_Good(t *testing.T) {
c := New()
r := c.Cli().Run()
assert.False(t, r.OK)
}
func TestCli_PrintHelp_Good(t *testing.T) {
c := New(Options{{Key: "name", Value: "myapp"}})
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()
}

130
tests/command_test.go Normal file
View file

@ -0,0 +1,130 @@
package core_test
import (
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- Command DTO ---
func TestCommand_Register_Good(t *testing.T) {
c := New()
r := c.Command("deploy", Command{Action: func(_ Options) Result {
return Result{Value: "deployed", OK: true}
}})
assert.True(t, r.OK)
}
func TestCommand_Get_Good(t *testing.T) {
c := New()
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
r := c.Command("deploy")
assert.True(t, r.OK)
assert.NotNil(t, r.Value)
}
func TestCommand_Get_Bad(t *testing.T) {
c := New()
r := c.Command("nonexistent")
assert.False(t, r.OK)
}
func TestCommand_Run_Good(t *testing.T) {
c := New()
c.Command("greet", Command{Action: func(opts Options) Result {
return Result{Value: Concat("hello ", opts.String("name")), OK: true}
}})
cmd := c.Command("greet").Value.(*Command)
r := cmd.Run(Options{{Key: "name", Value: "world"}})
assert.True(t, r.OK)
assert.Equal(t, "hello world", r.Value)
}
func TestCommand_Run_NoAction_Good(t *testing.T) {
c := New()
c.Command("empty", Command{Description: "no action"})
cmd := c.Command("empty").Value.(*Command)
r := cmd.Run(Options{})
assert.False(t, r.OK)
}
// --- Nested Commands ---
func TestCommand_Nested_Good(t *testing.T) {
c := New()
c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result {
return Result{Value: "deployed to homelab", OK: true}
}})
r := c.Command("deploy/to/homelab")
assert.True(t, r.OK)
// Parent auto-created
assert.True(t, c.Command("deploy").OK)
assert.True(t, c.Command("deploy/to").OK)
}
func TestCommand_Paths_Good(t *testing.T) {
c := New()
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.Command("deploy/to/homelab", Command{Action: func(_ Options) Result { return Result{OK: true} }})
paths := c.Commands()
assert.Contains(t, paths, "deploy")
assert.Contains(t, paths, "serve")
assert.Contains(t, paths, "deploy/to/homelab")
assert.Contains(t, paths, "deploy/to")
}
// --- I18n Key Derivation ---
func TestCommand_I18nKey_Good(t *testing.T) {
c := New()
c.Command("deploy/to/homelab", Command{})
cmd := c.Command("deploy/to/homelab").Value.(*Command)
assert.Equal(t, "cmd.deploy.to.homelab.description", cmd.I18nKey())
}
func TestCommand_I18nKey_Custom_Good(t *testing.T) {
c := New()
c.Command("deploy", Command{Description: "custom.deploy.key"})
cmd := c.Command("deploy").Value.(*Command)
assert.Equal(t, "custom.deploy.key", cmd.I18nKey())
}
func TestCommand_I18nKey_Simple_Good(t *testing.T) {
c := New()
c.Command("serve", Command{})
cmd := c.Command("serve").Value.(*Command)
assert.Equal(t, "cmd.serve.description", cmd.I18nKey())
}
// --- Lifecycle ---
func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) {
c := New()
c.Command("serve", Command{Action: func(_ Options) Result {
return Result{Value: "running", OK: true}
}})
cmd := c.Command("serve").Value.(*Command)
r := cmd.Start(Options{})
assert.True(t, r.OK)
assert.Equal(t, "running", r.Value)
assert.False(t, cmd.Stop().OK)
assert.False(t, cmd.Restart().OK)
assert.False(t, cmd.Reload().OK)
assert.False(t, cmd.Signal("HUP").OK)
}
// --- Empty path ---
func TestCommand_EmptyPath_Bad(t *testing.T) {
c := New()
r := c.Command("", Command{})
assert.False(t, r.OK)
}

102
tests/config_test.go Normal file
View 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)
r := c.Config().Get("api_url")
assert.True(t, r.OK)
assert.Equal(t, "https://api.lthn.ai", r.Value)
}
func TestConfig_Get_Bad(t *testing.T) {
c := New()
r := c.Config().Get("missing")
assert.False(t, r.OK)
assert.Nil(t, r.Value)
}
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())
}

View file

@ -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())
}

View file

@ -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")
}

View file

@ -1,346 +1,97 @@
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{{Key: "name", Value: "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")
func TestOptions_Accessor_Good(t *testing.T) {
c := New(Options{
{Key: "name", Value: "testapp"},
{Key: "port", Value: 8080},
{Key: "debug", Value: 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"))
}
err = c.RegisterService("test", "not a service")
assert.NoError(t, err)
func TestOptions_Accessor_Nil(t *testing.T) {
c := New()
// No options passed — Options() returns nil
assert.Nil(t, c.Options())
}
// MustServiceFor panics on type mismatch
// --- Core Error/Log Helpers ---
func TestCore_LogError_Good(t *testing.T) {
c := New()
cause := assert.AnError
r := c.LogError(cause, "test.Operation", "something broke")
assert.False(t, r.OK)
err, ok := r.Value.(error)
assert.True(t, ok)
assert.ErrorIs(t, err, cause)
}
func TestCore_LogWarn_Good(t *testing.T) {
c := New()
r := c.LogWarn(assert.AnError, "test.Operation", "heads up")
assert.False(t, r.OK)
_, ok := r.Value.(error)
assert.True(t, ok)
}
func TestCore_Must_Ugly(t *testing.T) {
c := New()
assert.Panics(t, func() {
MustServiceFor[*MockService](c, "test")
c.Must(assert.AnError, "test.Operation", "fatal")
})
}
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 TestCore_Must_Nil_Good(t *testing.T) {
c := New()
assert.NotPanics(t, func() {
c.Must(nil, "test.Operation", "no error")
})
}

130
tests/data_test.go Normal file
View file

@ -0,0 +1,130 @@
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{
{Key: "name", Value: "test"},
{Key: "source", Value: testFS},
{Key: "path", Value: "testdata"},
})
assert.True(t, r.OK)
assert.NotNil(t, r.Value)
}
func TestData_New_Bad(t *testing.T) {
c := New()
r := c.Data().New(Options{{Key: "source", Value: testFS}})
assert.False(t, r.OK)
r = c.Data().New(Options{{Key: "name", Value: "test"}})
assert.False(t, r.OK)
r = c.Data().New(Options{{Key: "name", Value: "test"}, {Key: "source", Value: "not-an-fs"}})
assert.False(t, r.OK)
}
func TestData_ReadString_Good(t *testing.T) {
c := New()
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
r := c.Data().ReadString("app/test.txt")
assert.True(t, r.OK)
assert.Equal(t, "hello from testdata\n", r.Value.(string))
}
func TestData_ReadString_Bad(t *testing.T) {
c := New()
r := c.Data().ReadString("nonexistent/file.txt")
assert.False(t, r.OK)
}
func TestData_ReadFile_Good(t *testing.T) {
c := New()
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
r := c.Data().ReadFile("app/test.txt")
assert.True(t, r.OK)
assert.Equal(t, "hello from testdata\n", string(r.Value.([]byte)))
}
func TestData_Get_Good(t *testing.T) {
c := New()
c.Data().New(Options{{Key: "name", Value: "brain"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
gr := c.Data().Get("brain")
assert.True(t, gr.OK)
emb := gr.Value.(*Embed)
r := emb.Open("test.txt")
assert.True(t, r.OK)
file := r.Value.(io.ReadCloser)
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()
r := c.Data().Get("nonexistent")
assert.False(t, r.OK)
}
func TestData_Mounts_Good(t *testing.T) {
c := New()
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"}})
mounts := c.Data().Mounts()
assert.Len(t, mounts, 2)
}
func TestEmbed_Legacy_Good(t *testing.T) {
c := New()
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}})
assert.NotNil(t, c.Embed())
}
func TestData_List_Good(t *testing.T) {
c := New()
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
r := c.Data().List("app/testdata")
assert.True(t, r.OK)
}
func TestData_List_Bad(t *testing.T) {
c := New()
r := c.Data().List("nonexistent/path")
assert.False(t, r.OK)
}
func TestData_ListNames_Good(t *testing.T) {
c := New()
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
r := c.Data().ListNames("app/testdata")
assert.True(t, r.OK)
assert.Contains(t, r.Value.([]string), "test")
}
func TestData_Extract_Good(t *testing.T) {
c := New()
c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}})
r := c.Data().Extract("app/testdata", t.TempDir(), nil)
assert.True(t, r.OK)
}
func TestData_Extract_Bad(t *testing.T) {
c := New()
r := c.Data().Extract("nonexistent/path", t.TempDir(), nil)
assert.False(t, r.OK)
}

80
tests/drive_test.go Normal file
View file

@ -0,0 +1,80 @@
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{
{Key: "name", Value: "api"},
{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)
}
func TestDrive_New_Bad(t *testing.T) {
c := New()
// Missing name
r := c.Drive().New(Options{
{Key: "transport", Value: "https://api.lthn.ai"},
})
assert.False(t, r.OK)
}
func TestDrive_Get_Good(t *testing.T) {
c := New()
c.Drive().New(Options{
{Key: "name", Value: "ssh"},
{Key: "transport", Value: "ssh://claude@10.69.69.165"},
})
r := c.Drive().Get("ssh")
assert.True(t, r.OK)
handle := r.Value.(*DriveHandle)
assert.Equal(t, "ssh://claude@10.69.69.165", handle.Transport)
}
func TestDrive_Get_Bad(t *testing.T) {
c := New()
r := c.Drive().Get("nonexistent")
assert.False(t, r.OK)
}
func TestDrive_Has_Good(t *testing.T) {
c := New()
c.Drive().New(Options{{Key: "name", Value: "mcp"}, {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()
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"}})
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{
{Key: "name", Value: "api"},
{Key: "transport", Value: "https://api.lthn.ai"},
{Key: "timeout", Value: 30},
})
r := c.Drive().Get("api")
assert.True(t, r.OK)
handle := r.Value.(*DriveHandle)
assert.Equal(t, 30, handle.Options.Int("timeout"))
}

View file

@ -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)
}

168
tests/embed_test.go Normal file
View file

@ -0,0 +1,168 @@
package core_test
import (
"bytes"
"compress/gzip"
"encoding/base64"
"os"
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- Mount ---
func TestMount_Good(t *testing.T) {
r := Mount(testFS, "testdata")
assert.True(t, r.OK)
}
func TestMount_Bad(t *testing.T) {
r := Mount(testFS, "nonexistent")
assert.False(t, r.OK)
}
// --- Embed methods ---
func TestEmbed_ReadFile_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
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)
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)
r := emb.Open("test.txt")
assert.True(t, r.OK)
}
func TestEmbed_ReadDir_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
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)
r := emb.Sub("testdata")
assert.True(t, r.OK)
sub := r.Value.(*Embed)
r2 := sub.ReadFile("test.txt")
assert.True(t, r2.OK)
}
func TestEmbed_BaseDir_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
assert.Equal(t, "testdata", emb.BaseDirectory())
}
func TestEmbed_FS_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
assert.NotNil(t, emb.FS())
}
func TestEmbed_EmbedFS_Good(t *testing.T) {
emb := Mount(testFS, "testdata").Value.(*Embed)
efs := emb.EmbedFS()
_, err := efs.ReadFile("testdata/test.txt")
assert.NoError(t, err)
}
// --- Extract ---
func TestExtract_Good(t *testing.T) {
dir := t.TempDir()
r := Extract(testFS, dir, nil)
assert.True(t, r.OK)
content, err := os.ReadFile(dir + "/testdata/test.txt")
assert.NoError(t, err)
assert.Equal(t, "hello from testdata\n", string(content))
}
// --- Asset Pack ---
func TestAddGetAsset_Good(t *testing.T) {
AddAsset("test-group", "greeting", mustCompress("hello world"))
r := GetAsset("test-group", "greeting")
assert.True(t, r.OK)
assert.Equal(t, "hello world", r.Value.(string))
}
func TestGetAsset_Bad(t *testing.T) {
r := GetAsset("missing-group", "missing")
assert.False(t, r.OK)
}
func TestGetAssetBytes_Good(t *testing.T) {
AddAsset("bytes-group", "file", mustCompress("binary content"))
r := GetAssetBytes("bytes-group", "file")
assert.True(t, r.OK)
assert.Equal(t, []byte("binary content"), r.Value.([]byte))
}
func TestMountEmbed_Good(t *testing.T) {
r := MountEmbed(testFS, "testdata")
assert.True(t, r.OK)
}
// --- ScanAssets ---
func TestScanAssets_Good(t *testing.T) {
r := ScanAssets([]string{"testdata/scantest/sample.go"})
assert.True(t, r.OK)
pkgs := r.Value.([]ScannedPackage)
assert.Len(t, pkgs, 1)
assert.Equal(t, "scantest", pkgs[0].PackageName)
}
func TestScanAssets_Bad(t *testing.T) {
r := ScanAssets([]string{"nonexistent.go"})
assert.False(t, r.OK)
}
func TestGeneratePack_Empty_Good(t *testing.T) {
pkg := ScannedPackage{PackageName: "empty"}
r := GeneratePack(pkg)
assert.True(t, r.OK)
assert.Contains(t, r.Value.(string), "package empty")
}
func TestGeneratePack_WithFiles_Good(t *testing.T) {
dir := t.TempDir()
assetDir := dir + "/mygroup"
os.MkdirAll(assetDir, 0755)
os.WriteFile(assetDir+"/hello.txt", []byte("hello world"), 0644)
source := "package test\nimport \"forge.lthn.ai/core/go/pkg/core\"\nfunc example() {\n\t_, _ = core.GetAsset(\"mygroup\", \"hello.txt\")\n}\n"
goFile := dir + "/test.go"
os.WriteFile(goFile, []byte(source), 0644)
sr := ScanAssets([]string{goFile})
assert.True(t, sr.OK)
pkgs := sr.Value.([]ScannedPackage)
r := GeneratePack(pkgs[0])
assert.True(t, r.OK)
assert.Contains(t, r.Value.(string), "core.AddAsset")
}
func mustCompress(input string) string {
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()
}

229
tests/error_test.go Normal file
View file

@ -0,0 +1,229 @@
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", ErrorCode(err))
}
func TestNewCode_Good(t *testing.T) {
err := NewCode("NOT_FOUND", "resource not found")
assert.Error(t, err)
assert.Equal(t, "NOT_FOUND", ErrorCode(err))
}
// --- Error Introspection ---
func TestOperation_Good(t *testing.T) {
err := E("brain.Recall", "search failed", nil)
assert.Equal(t, "brain.Recall", Operation(err))
}
func TestOperation_Bad(t *testing.T) {
err := errors.New("plain error")
assert.Equal(t, "", Operation(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")
r := c.Log().Error(cause, "test.Operation", "something broke")
assert.False(t, r.OK)
assert.ErrorIs(t, r.Value.(error), cause)
}
func TestErrorLog_Nil_Good(t *testing.T) {
c := New()
r := c.Log().Error(nil, "test.Operation", "no error")
assert.True(t, r.OK)
}
func TestErrorLog_Warn_Good(t *testing.T) {
c := New()
cause := errors.New("warning")
r := c.Log().Warn(cause, "test.Operation", "heads up")
assert.False(t, r.OK)
}
func TestErrorLog_Must_Ugly(t *testing.T) {
c := New()
assert.Panics(t, func() {
c.Log().Must(errors.New("fatal"), "test.Operation", "must fail")
})
}
func TestErrorLog_Must_Nil_Good(t *testing.T) {
c := New()
assert.NotPanics(t, func() {
c.Log().Must(nil, "test.Operation", "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.Operation)
}
func TestNewError_Good(t *testing.T) {
err := NewError("simple error")
assert.Equal(t, "simple error", err.Error())
}
func TestErrorJoin_Good(t *testing.T) {
e1 := errors.New("first")
e2 := errors.New("second")
joined := ErrorJoin(e1, e2)
assert.ErrorIs(t, joined, e1)
assert.ErrorIs(t, joined, e2)
}
// --- ErrorPanic Crash Reports ---
func TestErrorPanic_Reports_Good(t *testing.T) {
dir := t.TempDir()
path := dir + "/crashes.json"
// Create ErrorPanic with file output
c := New()
// Access internals via a crash that writes to file
// Since ErrorPanic fields are unexported, we test via Recover
_ = c
_ = path
// Crash reporting needs ErrorPanic configured with filePath — tested indirectly
}
// --- ErrorPanic Crash File ---
func TestErrorPanic_CrashFile_Good(t *testing.T) {
dir := t.TempDir()
path := dir + "/crashes.json"
// Create Core, trigger a panic through SafeGo, check crash file
// ErrorPanic.filePath is unexported — but we can test via the package-level
// error handling that writes crash reports
// For now, test that Reports handles missing file gracefully
c := New()
r := c.Error().Reports(5)
assert.False(t, r.OK)
assert.Nil(t, r.Value)
_ = path
}

186
tests/fs_test.go Normal file
View file

@ -0,0 +1,186 @@
package core_test
import (
"io"
"io/fs"
"os"
"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")
assert.True(t, c.Fs().Write(path, "hello core").OK)
r := c.Fs().Read(path)
assert.True(t, r.OK)
assert.Equal(t, "hello core", r.Value.(string))
}
func TestFs_Read_Bad(t *testing.T) {
c := New()
r := c.Fs().Read("/nonexistent/path/to/file.txt")
assert.False(t, r.OK)
}
func TestFs_EnsureDir_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := filepath.Join(dir, "sub", "dir")
assert.True(t, c.Fs().EnsureDir(path).OK)
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))
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")
r := c.Fs().List(dir)
assert.True(t, r.OK)
assert.Len(t, r.Value.([]fs.DirEntry), 2)
}
func TestFs_Stat_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := filepath.Join(dir, "stat.txt")
c.Fs().Write(path, "data")
r := c.Fs().Stat(path)
assert.True(t, r.OK)
assert.Equal(t, "stat.txt", r.Value.(os.FileInfo).Name())
}
func TestFs_Open_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := filepath.Join(dir, "open.txt")
c.Fs().Write(path, "content")
r := c.Fs().Open(path)
assert.True(t, r.OK)
r.Value.(io.Closer).Close()
}
func TestFs_Create_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := filepath.Join(dir, "sub", "created.txt")
r := c.Fs().Create(path)
assert.True(t, r.OK)
w := r.Value.(io.WriteCloser)
w.Write([]byte("hello"))
w.Close()
rr := c.Fs().Read(path)
assert.Equal(t, "hello", rr.Value.(string))
}
func TestFs_Append_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := filepath.Join(dir, "append.txt")
c.Fs().Write(path, "first")
r := c.Fs().Append(path)
assert.True(t, r.OK)
w := r.Value.(io.WriteCloser)
w.Write([]byte(" second"))
w.Close()
rr := c.Fs().Read(path)
assert.Equal(t, "first second", rr.Value.(string))
}
func TestFs_ReadStream_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := filepath.Join(dir, "stream.txt")
c.Fs().Write(path, "streamed")
r := c.Fs().ReadStream(path)
assert.True(t, r.OK)
r.Value.(io.Closer).Close()
}
func TestFs_WriteStream_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := filepath.Join(dir, "sub", "ws.txt")
r := c.Fs().WriteStream(path)
assert.True(t, r.OK)
w := r.Value.(io.WriteCloser)
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")
assert.True(t, c.Fs().Delete(path).OK)
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")
assert.True(t, c.Fs().DeleteAll(filepath.Join(dir, "deep")).OK)
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")
nw := filepath.Join(dir, "new.txt")
c.Fs().Write(old, "data")
assert.True(t, c.Fs().Rename(old, nw).OK)
assert.False(t, c.Fs().Exists(old))
assert.True(t, c.Fs().Exists(nw))
}
func TestFs_WriteMode_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := filepath.Join(dir, "secret.txt")
assert.True(t, c.Fs().WriteMode(path, "secret", 0600).OK)
r := c.Fs().Stat(path)
assert.True(t, r.OK)
assert.Equal(t, "secret.txt", r.Value.(os.FileInfo).Name())
}

View file

@ -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)
}
})
}

94
tests/i18n_test.go Normal file
View file

@ -0,0 +1,94 @@
package core_test
import (
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- I18n ---
func TestI18n_Good(t *testing.T) {
c := New()
assert.NotNil(t, c.I18n())
}
func TestI18n_AddLocales_Good(t *testing.T) {
c := New()
r := c.Data().New(Options{
{Key: "name", Value: "lang"},
{Key: "source", Value: testFS},
{Key: "path", Value: "testdata"},
})
if r.OK {
c.I18n().AddLocales(r.Value.(*Embed))
}
r2 := c.I18n().Locales()
assert.True(t, r2.OK)
assert.Len(t, r2.Value.([]*Embed), 1)
}
func TestI18n_Locales_Empty_Good(t *testing.T) {
c := New()
r := c.I18n().Locales()
assert.True(t, r.OK)
assert.Empty(t, r.Value.([]*Embed))
}
// --- Translator (no translator registered) ---
func TestI18n_Translate_NoTranslator_Good(t *testing.T) {
c := New()
// Without a translator, Translate returns the key as-is
r := c.I18n().Translate("greeting.hello")
assert.True(t, r.OK)
assert.Equal(t, "greeting.hello", r.Value)
}
func TestI18n_SetLanguage_NoTranslator_Good(t *testing.T) {
c := New()
r := c.I18n().SetLanguage("de")
assert.True(t, r.OK) // no-op without translator
}
func TestI18n_Language_NoTranslator_Good(t *testing.T) {
c := New()
assert.Equal(t, "en", c.I18n().Language())
}
func TestI18n_AvailableLanguages_NoTranslator_Good(t *testing.T) {
c := New()
langs := c.I18n().AvailableLanguages()
assert.Equal(t, []string{"en"}, langs)
}
func TestI18n_Translator_Nil_Good(t *testing.T) {
c := New()
assert.False(t, c.I18n().Translator().OK)
}
// --- Translator (with mock) ---
type mockTranslator struct {
lang string
}
func (m *mockTranslator) Translate(id string, args ...any) Result { return Result{"translated:" + id, true} }
func (m *mockTranslator) SetLanguage(lang string) error { m.lang = lang; return nil }
func (m *mockTranslator) Language() string { return m.lang }
func (m *mockTranslator) AvailableLanguages() []string { return []string{"en", "de", "fr"} }
func TestI18n_WithTranslator_Good(t *testing.T) {
c := New()
tr := &mockTranslator{lang: "en"}
c.I18n().SetTranslator(tr)
assert.Equal(t, tr, c.I18n().Translator().Value)
assert.Equal(t, "translated:hello", c.I18n().Translate("hello").Value)
assert.Equal(t, "en", c.I18n().Language())
assert.Equal(t, []string{"en", "de", "fr"}, c.I18n().AvailableLanguages())
c.I18n().SetLanguage("de")
assert.Equal(t, "de", c.I18n().Language())
}

View file

@ -1,121 +1,95 @@
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
func TestAction_Good(t *testing.T) {
c := New()
var received Message
c.RegisterAction(func(_ *Core, msg Message) Result {
received = msg
return Result{OK: true}
})
res, handled, err = c.QUERY(IPCTestQuery{Value: "test"})
assert.True(t, handled)
assert.Nil(t, err)
assert.Equal(t, "test-response", res)
r := c.ACTION(testMessage{payload: "hello"})
assert.True(t, r.OK)
assert.Equal(t, testMessage{payload: "hello"}, received)
}
func TestIPC_QueryAll(t *testing.T) {
c, _ := New()
func TestAction_Multiple_Good(t *testing.T) {
c := New()
count := 0
handler := func(_ *Core, _ Message) Result { count++; return Result{OK: true} }
c.RegisterActions(handler, handler, handler)
c.ACTION(nil)
assert.Equal(t, 3, count)
}
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
})
func TestAction_None_Good(t *testing.T) {
c := New()
// No handlers registered — should succeed
r := c.ACTION(nil)
assert.True(t, r.OK)
}
results, err := c.QUERYALL(IPCTestQuery{})
assert.Nil(t, err)
// --- IPC: Queries ---
func TestQuery_Good(t *testing.T) {
c := New()
c.RegisterQuery(func(_ *Core, q Query) Result {
if q == "ping" {
return Result{Value: "pong", OK: true}
}
return Result{}
})
r := c.QUERY("ping")
assert.True(t, r.OK)
assert.Equal(t, "pong", r.Value)
}
func TestQuery_Unhandled_Good(t *testing.T) {
c := New()
c.RegisterQuery(func(_ *Core, q Query) Result {
return Result{}
})
r := c.QUERY("unknown")
assert.False(t, r.OK)
}
func TestQueryAll_Good(t *testing.T) {
c := New()
c.RegisterQuery(func(_ *Core, _ Query) Result {
return Result{Value: "a", OK: true}
})
c.RegisterQuery(func(_ *Core, _ Query) Result {
return Result{Value: "b", OK: true}
})
r := c.QUERYALL("anything")
assert.True(t, r.OK)
results := r.Value.([]any)
assert.Len(t, results, 2)
assert.Contains(t, results, "h1")
assert.Contains(t, results, "h2")
assert.Contains(t, results, "a")
assert.Contains(t, results, "b")
}
func TestIPC_Perform(t *testing.T) {
c, _ := New()
// --- IPC: Tasks ---
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")
func TestPerform_Good(t *testing.T) {
c := New()
c.RegisterTask(func(_ *Core, t Task) Result {
if t == "compute" {
return Result{Value: 42, OK: true}
}
return "done", true, nil
}
return nil, false, nil
return Result{}
})
// 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,
}
}
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")
}
r := c.PERFORM("compute")
assert.True(t, r.OK)
assert.Equal(t, 42, r.Value)
}

55
tests/lock_test.go Normal file
View file

@ -0,0 +1,55 @@
package core_test
import (
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
func TestLock_Good(t *testing.T) {
c := New()
lock := c.Lock("test")
assert.NotNil(t, lock)
assert.NotNil(t, lock.Mutex)
}
func TestLock_SameName_Good(t *testing.T) {
c := New()
l1 := c.Lock("shared")
l2 := c.Lock("shared")
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 TestLockEnable_Good(t *testing.T) {
c := New()
c.Service("early", Service{})
c.LockEnable()
c.LockApply()
r := c.Service("late", Service{})
assert.False(t, r.OK)
}
func TestStartables_Good(t *testing.T) {
c := New()
c.Service("s", Service{OnStart: func() Result { return Result{OK: true} }})
r := c.Startables()
assert.True(t, r.OK)
assert.Len(t, r.Value.([]*Service), 1)
}
func TestStoppables_Good(t *testing.T) {
c := New()
c.Service("s", Service{OnStop: func() Result { return Result{OK: true} }})
r := c.Stoppables()
assert.True(t, r.OK)
assert.Len(t, r.Value.([]*Service), 1)
}

147
tests/log_test.go Normal file
View file

@ -0,0 +1,147 @@
package core_test
import (
"os"
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- Log ---
func TestLog_New_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
assert.NotNil(t, l)
}
func TestLog_AllLevels_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelDebug})
l.Debug("debug")
l.Info("info")
l.Warn("warn")
l.Error("error")
l.Security("security event")
}
func TestLog_LevelFiltering_Good(t *testing.T) {
// At Error level, Debug/Info/Warn should be suppressed (no panic)
l := NewLog(LogOptions{Level: LevelError})
l.Debug("suppressed")
l.Info("suppressed")
l.Warn("suppressed")
l.Error("visible")
}
func TestLog_SetLevel_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
l.SetLevel(LevelDebug)
assert.Equal(t, LevelDebug, l.Level())
}
func TestLog_SetRedactKeys_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
l.SetRedactKeys("password", "token")
// Redacted keys should mask values in output
l.Info("login", "password", "secret123", "user", "admin")
}
func TestLog_LevelString_Good(t *testing.T) {
assert.Equal(t, "debug", LevelDebug.String())
assert.Equal(t, "info", LevelInfo.String())
assert.Equal(t, "warn", LevelWarn.String())
assert.Equal(t, "error", LevelError.String())
}
func TestLog_CoreLog_Good(t *testing.T) {
c := New()
assert.NotNil(t, c.Log())
}
func TestLog_ErrorSink_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
var sink ErrorSink = l
sink.Error("test")
sink.Warn("test")
}
// --- Default Logger ---
func TestLog_Default_Good(t *testing.T) {
d := Default()
assert.NotNil(t, d)
}
func TestLog_SetDefault_Good(t *testing.T) {
original := Default()
defer SetDefault(original)
custom := NewLog(LogOptions{Level: LevelDebug})
SetDefault(custom)
assert.Equal(t, custom, Default())
}
func TestLog_PackageLevelFunctions_Good(t *testing.T) {
// Package-level log functions use the default logger
Debug("debug msg")
Info("info msg")
Warn("warn msg")
Error("error msg")
Security("security msg")
}
func TestLog_PackageSetLevel_Good(t *testing.T) {
original := Default()
defer SetDefault(original)
SetLevel(LevelDebug)
SetRedactKeys("secret")
}
func TestLog_Username_Good(t *testing.T) {
u := Username()
assert.NotEmpty(t, u)
}
// --- LogErr ---
func TestLogErr_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
le := NewLogErr(l)
assert.NotNil(t, le)
err := E("test.Operation", "something broke", nil)
le.Log(err)
}
func TestLogErr_Nil_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
le := NewLogErr(l)
le.Log(nil) // should not panic
}
// --- LogPanic ---
func TestLogPanic_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
lp := NewLogPanic(l)
assert.NotNil(t, lp)
}
func TestLogPanic_Recover_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
lp := NewLogPanic(l)
assert.NotPanics(t, func() {
defer lp.Recover()
panic("caught")
})
}
// --- SetOutput ---
func TestLog_SetOutput_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
l.SetOutput(os.Stderr)
// Should not panic — just changes where logs go
l.Info("redirected")
}

View file

@ -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
View 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{
{Key: "name", Value: "brain"},
{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"}}
r := opts.Get("missing")
assert.False(t, r.OK)
assert.Nil(t, r.Value)
}
func TestOptions_Has_Good(t *testing.T) {
opts := Options{{Key: "debug", Value: true}}
assert.True(t, opts.Has("debug"))
assert.False(t, opts.Has("missing"))
}
func TestOptions_String_Good(t *testing.T) {
opts := Options{{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
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{{Key: "port", Value: 8080}}
assert.Equal(t, 8080, opts.Int("port"))
}
func TestOptions_Int_Bad(t *testing.T) {
opts := Options{{Key: "name", Value: "brain"}}
assert.Equal(t, 0, opts.Int("name"))
assert.Equal(t, 0, opts.Int("missing"))
}
func TestOptions_Bool_Good(t *testing.T) {
opts := Options{{Key: "debug", Value: true}}
assert.True(t, opts.Bool("debug"))
}
func TestOptions_Bool_Bad(t *testing.T) {
opts := Options{{Key: "name", Value: "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.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}}
r := opts.Get("config")
assert.True(t, r.OK)
bc, ok := r.Value.(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"))
}

View file

@ -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)
}

View file

@ -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")
}

View file

@ -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()
})
}

76
tests/runtime_test.go Normal file
View file

@ -0,0 +1,76 @@
package core_test
import (
"context"
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- ServiceRuntime ---
type testOpts struct {
URL string
Timeout int
}
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.Options())
assert.Equal(t, "https://api.lthn.ai", rt.Options().URL)
assert.NotNil(t, rt.Config())
}
// --- NewWithFactories ---
func TestNewWithFactories_Good(t *testing.T) {
r := NewWithFactories(nil, map[string]ServiceFactory{
"svc1": func() Result { return Result{Value: Service{}, OK: true} },
"svc2": func() Result { return Result{Value: Service{}, OK: true} },
})
assert.True(t, r.OK)
rt := r.Value.(*Runtime)
assert.NotNil(t, rt.Core)
}
func TestNewWithFactories_NilFactory_Good(t *testing.T) {
r := NewWithFactories(nil, map[string]ServiceFactory{
"bad": nil,
})
assert.True(t, r.OK) // nil factories skipped
}
func TestNewRuntime_Good(t *testing.T) {
r := NewRuntime(nil)
assert.True(t, r.OK)
}
func TestRuntime_ServiceName_Good(t *testing.T) {
r := NewRuntime(nil)
rt := r.Value.(*Runtime)
assert.Equal(t, "Core", rt.ServiceName())
}
// --- Lifecycle via Runtime ---
func TestRuntime_Lifecycle_Good(t *testing.T) {
started := false
r := NewWithFactories(nil, map[string]ServiceFactory{
"test": func() Result {
return Result{Value: Service{
OnStart: func() Result { started = true; return Result{OK: true} },
}, OK: true}
},
})
assert.True(t, r.OK)
rt := r.Value.(*Runtime)
result := rt.ServiceStartup(context.Background(), nil)
assert.True(t, result.OK)
assert.True(t, started)
}

View file

@ -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)
}

79
tests/service_test.go Normal file
View file

@ -0,0 +1,79 @@
package core_test
import (
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- Service Registration ---
func TestService_Register_Good(t *testing.T) {
c := New()
r := c.Service("auth", Service{})
assert.True(t, r.OK)
}
func TestService_Register_Duplicate_Bad(t *testing.T) {
c := New()
c.Service("auth", Service{})
r := c.Service("auth", Service{})
assert.False(t, r.OK)
}
func TestService_Register_Empty_Bad(t *testing.T) {
c := New()
r := c.Service("", Service{})
assert.False(t, r.OK)
}
func TestService_Get_Good(t *testing.T) {
c := New()
c.Service("brain", Service{OnStart: func() Result { return Result{OK: true} }})
r := c.Service("brain")
assert.True(t, r.OK)
assert.NotNil(t, r.Value)
}
func TestService_Get_Bad(t *testing.T) {
c := New()
r := c.Service("nonexistent")
assert.False(t, r.OK)
}
func TestService_Names_Good(t *testing.T) {
c := New()
c.Service("a", Service{})
c.Service("b", Service{})
names := c.Services()
assert.Len(t, names, 2)
assert.Contains(t, names, "a")
assert.Contains(t, names, "b")
}
// --- Service Lifecycle ---
func TestService_Lifecycle_Good(t *testing.T) {
c := New()
started := false
stopped := false
c.Service("lifecycle", Service{
OnStart: func() Result { started = true; return Result{OK: true} },
OnStop: func() Result { stopped = true; return Result{OK: true} },
})
sr := c.Startables()
assert.True(t, sr.OK)
startables := sr.Value.([]*Service)
assert.Len(t, startables, 1)
startables[0].OnStart()
assert.True(t, started)
tr := c.Stoppables()
assert.True(t, tr.OK)
stoppables := tr.Value.([]*Service)
assert.Len(t, stoppables, 1)
stoppables[0].OnStop()
assert.True(t, stopped)
}

70
tests/string_test.go Normal file
View file

@ -0,0 +1,70 @@
package core_test
import (
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- String Operations ---
func TestHasPrefix_Good(t *testing.T) {
assert.True(t, HasPrefix("--verbose", "--"))
assert.True(t, HasPrefix("-v", "-"))
assert.False(t, HasPrefix("hello", "-"))
}
func TestHasSuffix_Good(t *testing.T) {
assert.True(t, HasSuffix("test.go", ".go"))
assert.False(t, HasSuffix("test.go", ".py"))
}
func TestTrimPrefix_Good(t *testing.T) {
assert.Equal(t, "verbose", TrimPrefix("--verbose", "--"))
assert.Equal(t, "hello", TrimPrefix("hello", "--"))
}
func TestTrimSuffix_Good(t *testing.T) {
assert.Equal(t, "test", TrimSuffix("test.go", ".go"))
assert.Equal(t, "test.go", TrimSuffix("test.go", ".py"))
}
func TestContains_Good(t *testing.T) {
assert.True(t, Contains("hello world", "world"))
assert.False(t, Contains("hello world", "mars"))
}
func TestSplit_Good(t *testing.T) {
assert.Equal(t, []string{"a", "b", "c"}, Split("a/b/c", "/"))
}
func TestSplitN_Good(t *testing.T) {
assert.Equal(t, []string{"key", "value=extra"}, SplitN("key=value=extra", "=", 2))
}
func TestJoin_Good(t *testing.T) {
assert.Equal(t, "a/b/c", Join("/", "a", "b", "c"))
}
func TestReplace_Good(t *testing.T) {
assert.Equal(t, "deploy.to.homelab", Replace("deploy/to/homelab", "/", "."))
}
func TestLower_Good(t *testing.T) {
assert.Equal(t, "hello", Lower("HELLO"))
}
func TestUpper_Good(t *testing.T) {
assert.Equal(t, "HELLO", Upper("hello"))
}
func TestTrim_Good(t *testing.T) {
assert.Equal(t, "hello", Trim(" hello "))
}
func TestRuneCount_Good(t *testing.T) {
assert.Equal(t, 5, RuneCount("hello"))
assert.Equal(t, 1, RuneCount("🔥"))
assert.Equal(t, 0, RuneCount(""))
}

69
tests/task_test.go Normal file
View file

@ -0,0 +1,69 @@
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) Result {
mu.Lock()
result = "done"
mu.Unlock()
return Result{"completed", true}
})
r := c.PerformAsync("work")
assert.True(t, r.OK)
taskID := r.Value.(string)
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) Result {
return Result{OK: true}
})
r := c.PerformAsync("work")
taskID := r.Value.(string)
c.Progress(taskID, 0.5, "halfway", "work")
}
// --- RegisterAction + RegisterActions ---
func TestRegisterAction_Good(t *testing.T) {
c := New()
called := false
c.RegisterAction(func(_ *Core, _ Message) Result {
called = true
return Result{OK: true}
})
c.Action(nil)
assert.True(t, called)
}
func TestRegisterActions_Good(t *testing.T) {
c := New()
count := 0
h := func(_ *Core, _ Message) Result { count++; return Result{OK: true} }
c.RegisterActions(h, h)
c.Action(nil)
assert.Equal(t, 2, count)
}

1339
tests/testdata/cli_clir.go.bak vendored Normal file

File diff suppressed because it is too large Load diff

7
tests/testdata/scantest/sample.go vendored Normal file
View file

@ -0,0 +1,7 @@
package scantest
import "forge.lthn.ai/core/go/pkg/core"
func example() {
_, _ = core.GetAsset("mygroup", "myfile.txt")
}

217
tests/utils_test.go Normal file
View file

@ -0,0 +1,217 @@
package core_test
import (
"errors"
"testing"
. "forge.lthn.ai/core/go/pkg/core"
"github.com/stretchr/testify/assert"
)
// --- FilterArgs ---
func TestFilterArgs_Good(t *testing.T) {
args := []string{"deploy", "", "to", "-test.v", "homelab", "-test.paniconexit0"}
clean := FilterArgs(args)
assert.Equal(t, []string{"deploy", "to", "homelab"}, clean)
}
func TestFilterArgs_Empty_Good(t *testing.T) {
clean := FilterArgs(nil)
assert.Nil(t, clean)
}
// --- ParseFlag ---
func TestParseFlag_ShortValid_Good(t *testing.T) {
// Single letter
k, v, ok := ParseFlag("-v")
assert.True(t, ok)
assert.Equal(t, "v", k)
assert.Equal(t, "", v)
// Single emoji
k, v, ok = ParseFlag("-🔥")
assert.True(t, ok)
assert.Equal(t, "🔥", k)
assert.Equal(t, "", v)
// Short with value
k, v, ok = ParseFlag("-p=8080")
assert.True(t, ok)
assert.Equal(t, "p", k)
assert.Equal(t, "8080", v)
}
func TestParseFlag_ShortInvalid_Bad(t *testing.T) {
// Multiple chars with single dash — invalid
_, _, ok := ParseFlag("-verbose")
assert.False(t, ok)
_, _, ok = ParseFlag("-port")
assert.False(t, ok)
}
func TestParseFlag_LongValid_Good(t *testing.T) {
k, v, ok := ParseFlag("--verbose")
assert.True(t, ok)
assert.Equal(t, "verbose", k)
assert.Equal(t, "", v)
k, v, ok = ParseFlag("--port=8080")
assert.True(t, ok)
assert.Equal(t, "port", k)
assert.Equal(t, "8080", v)
}
func TestParseFlag_LongInvalid_Bad(t *testing.T) {
// Single char with double dash — invalid
_, _, ok := ParseFlag("--v")
assert.False(t, ok)
}
func TestParseFlag_NotAFlag_Bad(t *testing.T) {
_, _, ok := ParseFlag("hello")
assert.False(t, ok)
_, _, ok = ParseFlag("")
assert.False(t, ok)
}
// --- IsFlag ---
func TestIsFlag_Good(t *testing.T) {
assert.True(t, IsFlag("-v"))
assert.True(t, IsFlag("--verbose"))
assert.True(t, IsFlag("-"))
}
func TestIsFlag_Bad(t *testing.T) {
assert.False(t, IsFlag("hello"))
assert.False(t, IsFlag(""))
}
// --- Arg ---
func TestArg_String_Good(t *testing.T) {
r := Arg(0, "hello", 42, true)
assert.True(t, r.OK)
assert.Equal(t, "hello", r.Value)
}
func TestArg_Int_Good(t *testing.T) {
r := Arg(1, "hello", 42, true)
assert.True(t, r.OK)
assert.Equal(t, 42, r.Value)
}
func TestArg_Bool_Good(t *testing.T) {
r := Arg(2, "hello", 42, true)
assert.True(t, r.OK)
assert.Equal(t, true, r.Value)
}
func TestArg_UnsupportedType_Good(t *testing.T) {
r := Arg(0, 3.14)
assert.True(t, r.OK)
assert.Equal(t, 3.14, r.Value)
}
func TestArg_OutOfBounds_Bad(t *testing.T) {
r := Arg(5, "only", "two")
assert.False(t, r.OK)
assert.Nil(t, r.Value)
}
func TestArg_NoArgs_Bad(t *testing.T) {
r := Arg(0)
assert.False(t, r.OK)
assert.Nil(t, r.Value)
}
func TestArg_ErrorDetection_Good(t *testing.T) {
err := errors.New("fail")
r := Arg(0, err)
assert.True(t, r.OK)
assert.Equal(t, err, r.Value)
}
// --- ArgString ---
func TestArgString_Good(t *testing.T) {
assert.Equal(t, "hello", ArgString(0, "hello", 42))
assert.Equal(t, "world", ArgString(1, "hello", "world"))
}
func TestArgString_WrongType_Bad(t *testing.T) {
assert.Equal(t, "", ArgString(0, 42))
}
func TestArgString_OutOfBounds_Bad(t *testing.T) {
assert.Equal(t, "", ArgString(3, "only"))
}
// --- ArgInt ---
func TestArgInt_Good(t *testing.T) {
assert.Equal(t, 42, ArgInt(0, 42, "hello"))
assert.Equal(t, 99, ArgInt(1, 0, 99))
}
func TestArgInt_WrongType_Bad(t *testing.T) {
assert.Equal(t, 0, ArgInt(0, "not an int"))
}
func TestArgInt_OutOfBounds_Bad(t *testing.T) {
assert.Equal(t, 0, ArgInt(5, 1, 2))
}
// --- ArgBool ---
func TestArgBool_Good(t *testing.T) {
assert.Equal(t, true, ArgBool(0, true, "hello"))
assert.Equal(t, false, ArgBool(1, true, false))
}
func TestArgBool_WrongType_Bad(t *testing.T) {
assert.Equal(t, false, ArgBool(0, "not a bool"))
}
func TestArgBool_OutOfBounds_Bad(t *testing.T) {
assert.Equal(t, false, ArgBool(5, true))
}
// --- Result.Result() ---
func TestResult_Result_SingleArg_Good(t *testing.T) {
r := Result{}.Result("value")
assert.True(t, r.OK)
assert.Equal(t, "value", r.Value)
}
func TestResult_Result_NilError_Good(t *testing.T) {
r := Result{}.Result("value", nil)
assert.True(t, r.OK)
assert.Equal(t, "value", r.Value)
}
func TestResult_Result_WithError_Bad(t *testing.T) {
err := errors.New("fail")
r := Result{}.Result("value", err)
assert.False(t, r.OK)
assert.Equal(t, err, r.Value)
}
func TestResult_Result_ZeroArgs_Good(t *testing.T) {
r := Result{"hello", true}
got := r.Result()
assert.Equal(t, "hello", got.Value)
assert.True(t, got.OK)
}
func TestResult_Result_ZeroArgs_Empty_Good(t *testing.T) {
r := Result{}
got := r.Result()
assert.Nil(t, got.Value)
assert.False(t, got.OK)
}