Compare commits

..

17 commits

Author SHA1 Message Date
Virgil
0a7bafd631 fix(core): repair embed mounts and result status
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 20:28:52 +00:00
Snider
5c435f4b92 wip: v0.3.3 parity — Tasks 1-7 complete, data/embed tests need fixing
WithService: full name discovery + IPC handler auto-registration via reflect
WithName: explicit service naming
RegisterService: Startable/Stoppable/HandleIPCEvents auto-discovery
MustServiceFor[T]: panics if not found
WithServiceLock: enable/apply split (v0.3.3 parity)
Cli: registered as service via CliRegister, accessed via ServiceFor

@TODO Codex: Fix data_test.go and embed_test.go — embed path resolution
after Options changed from []Option to struct. Mount paths need updating.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 20:15:34 +00:00
Snider
d10e7bba01 feat: MustServiceFor[T] + fix service names test for auto-registered cli
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 20:10:29 +00:00
Snider
6efa1591d6 fix: WithServiceLock enables, New() applies after all opts — v0.3.3 parity
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 20:09:06 +00:00
Snider
951fe9ce8c test: lifecycle + HandleIPCEvents end-to-end via WithService
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 20:07:45 +00:00
Snider
84110a3ac5 feat: WithName for explicit service naming
Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 20:06:56 +00:00
Snider
0342089b0e feat: WithService with v0.3.3 name discovery + IPC handler auto-registration
WithService now: calls factory, discovers service name from instance's
package path via reflect.TypeOf, discovers HandleIPCEvents method,
calls RegisterService. If factory returns nil Value, assumes self-registered.

Also fixes: Cli() accessor uses ServiceFor, test files updated for Options struct.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 20:05:36 +00:00
Snider
f1ed1f0ac5 feat: WithService with v0.3.3 name discovery + IPC handler auto-registration
- WithService now calls factory, discovers service name from package path via
  reflect/runtime (last path segment, _test suffix stripped, lowercased), and
  calls RegisterService — which handles Startable/Stoppable/HandleIPCEvents
- If factory returns nil Value (self-registered), WithService returns OK without
  a second registration
- Add contract_test.go with _Good/_Bad tests covering all three code paths
- Fix core.go Cli() accessor: use ServiceFor[*Cli](c, "cli") (was cli.New())
- Fix pre-existing })) → }}) syntax errors in command_test, service_test, lock_test
- Fix pre-existing Options{...} → NewOptions(...) in core_test, data_test,
  drive_test, i18n_test (Options is a struct, not a slice)

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 20:02:53 +00:00
Snider
a7ab83550a wip: checkpoint before v0.3.3 parity rewrite
Cli as service with ServiceRuntime, incomplete.
Need to properly port v0.3.3 service_manager, message_bus,
WithService with full name/IPC discovery.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 19:48:12 +00:00
Snider
b8ac786308 feat: Cli.New(c) constructor — Core uses it during construction
Cli{}.New(c) replaces &Cli{core: c} in contract.go.
9 tests passing.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 19:26:44 +00:00
Snider
416e975fbb fix: update Cli doc comment + tests for new Options contract
Cli struct unchanged — already conforms.
Tests use WithOption() convenience. 9 tests passing.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 19:24:17 +00:00
Snider
d8fb18a663 feat: App struct with New(Options) + Find() as method
App.New() creates from Options. App.Find() locates programs on PATH.
Both are struct methods — no package-level functions.
8 tests passing.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 19:18:59 +00:00
Snider
d5e388fbb2 feat: Options struct + Result methods + WithOption convenience
Options is now a proper struct with New(), Set(), Get(), typed accessors.
Result gains New(), Result(), Get() methods on the struct.
WithOption("key", value) convenience for core.New().

options_test.go: 22 tests passing against the new contract.
Other test files mechanically updated for compilation.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 19:17:12 +00:00
Snider
7ee897d12b feat: RegisterService with instance storage + interface discovery
Restores v0.3.3 service manager capabilities:
- RegisterService(name, instance) stores the raw instance
- Auto-discovers Startable/Stoppable interfaces → wires lifecycle
- Auto-discovers HandleIPCEvents → wires to IPC bus
- ServiceFor[T](c, name) for typed instance retrieval
- Service DTO gains Instance field for instance tracking

WithService is a simple factory call — no reflect, no magic.
discoverHandlers removed — RegisterService handles it inline.
No double-registration: IPC wired once at registration time.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 17:37:57 +00:00
Snider
98d078130e fix: move HandleIPCEvents discovery to New() post-construction
WithService is now a simple factory call — no reflect, no auto-registration.
New() calls discoverHandlers() after all opts run, scanning Config for
service instances that implement HandleIPCEvents.

This eliminates both double-registration and empty-placeholder issues:
- Factories wire their own lifecycle via c.Service()
- HandleIPCEvents discovered once, after all services are registered
- No tension between factory-registered and auto-discovered paths

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 17:24:50 +00:00
Snider
3a9ac82275 fix: prevent double IPC registration + empty service placeholder
- HandleIPCEvents only auto-registered for services the factory didn't
  register itself (prevents double handler registration)
- Auto-discovery only creates Service{} placeholder when factory didn't
  call c.Service() — factories that register themselves keep full lifecycle

Addresses Codex review findings 1 and 2 from third pass.

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 17:14:51 +00:00
Snider
b5dcdbb216 fix: address Codex review findings on PR #28
- WithOptions copies the Options slice (constructor isolation regression)
- WithService auto-discovers service name from package path via reflect
- WithService auto-registers HandleIPCEvents if present (v0.3.3 parity)
- Add test for failing option short-circuit in New()

Co-Authored-By: Virgil <virgil@lethean.io>
2026-03-24 16:59:33 +00:00
79 changed files with 1499 additions and 7290 deletions

View file

@ -4,15 +4,16 @@ Guidance for Claude Code and Codex when working with this repository.
## Module
`dappco.re/go/core` — dependency injection, service lifecycle, permission, and message-passing for Go.
`dappco.re/go/core` — dependency injection, service lifecycle, command routing, and message-passing for Go.
Source files and tests live at the module root. No `pkg/` nesting.
Source files live at the module root (not `pkg/core/`). Tests live in `tests/`.
## Build & Test
```bash
go test ./... -count=1 # run all tests (483 tests, 84.7% coverage)
go build ./... # verify compilation
go test ./tests/... # run all tests
go build . # verify compilation
GOWORK=off go test ./tests/ # test without workspace
```
Or via the Core CLI:
@ -24,61 +25,55 @@ core go qa # fmt + vet + lint + test
## API Shape
```go
c := core.New(
core.WithOption("name", "myapp"),
core.WithService(mypackage.Register),
core.WithServiceLock(),
)
c.Run() // or: if err := c.RunE(); err != nil { ... }
```
Service factory:
CoreGO uses the DTO/Options/Result pattern, not functional options:
```go
func Register(c *core.Core) core.Result {
svc := &MyService{ServiceRuntime: core.NewServiceRuntime(c, MyOpts{})}
return core.Result{Value: svc, OK: true}
}
c := core.New(core.Options{
{Key: "name", Value: "myapp"},
})
c.Service("cache", core.Service{
OnStart: func() core.Result { return core.Result{OK: true} },
OnStop: func() core.Result { return core.Result{OK: true} },
})
c.Command("deploy/to/homelab", core.Command{
Action: func(opts core.Options) core.Result {
return core.Result{Value: "deployed", OK: true}
},
})
r := c.Cli().Run("deploy", "to", "homelab")
```
**Do not use:** `WithService`, `WithName`, `WithApp`, `WithServiceLock`, `Must*`, `ServiceFor[T]` — these no longer exist.
## Subsystems
| Accessor | Returns | Purpose |
|----------|---------|---------|
| `c.Options()` | `*Options` | Input configuration |
| `c.App()` | `*App` | Application identity |
| `c.Config()` | `*Config` | Runtime settings, feature flags |
| `c.Data()` | `*Data` | Embedded assets (Registry[*Embed]) |
| `c.Drive()` | `*Drive` | Transport handles (Registry[*DriveHandle]) |
| `c.Fs()` | `*Fs` | Filesystem I/O (sandboxable) |
| `c.Cli()` | `*Cli` | CLI command framework |
| `c.IPC()` | `*Ipc` | Message bus internals |
| `c.Process()` | `*Process` | Managed execution (Action sugar) |
| `c.API()` | `*API` | Remote streams (protocol handlers) |
| `c.Action(name)` | `*Action` | Named callable (register/invoke) |
| `c.Task(name)` | `*Task` | Composed Action sequence |
| `c.Entitled(name)` | `Entitlement` | Permission check |
| `c.RegistryOf(n)` | `*Registry` | Cross-cutting queries |
| `c.I18n()` | `*I18n` | Internationalisation |
| `c.Data()` | `*Data` | Embedded filesystem mounts |
| `c.Drive()` | `*Drive` | Named transport handles |
| `c.Fs()` | `*Fs` | Local filesystem I/O |
| `c.Config()` | `*Config` | Runtime settings |
| `c.Cli()` | `*Cli` | CLI surface |
| `c.Command("path")` | `Result` | Command tree |
| `c.Service("name")` | `Result` | Service registry |
| `c.Lock("name")` | `*Lock` | Named mutexes |
| `c.IPC()` | `*Ipc` | Message bus |
| `c.I18n()` | `*I18n` | Locale + translation |
## Messaging
| Method | Pattern |
|--------|---------|
| `c.ACTION(msg)` | Broadcast to all handlers (panic recovery per handler) |
| `c.ACTION(msg)` | Broadcast to all handlers |
| `c.QUERY(q)` | First responder wins |
| `c.QUERYALL(q)` | Collect all responses |
| `c.PerformAsync(action, opts)` | Background goroutine with progress |
## Lifecycle
```go
type Startable interface { OnStartup(ctx context.Context) Result }
type Stoppable interface { OnShutdown(ctx context.Context) Result }
```
`RunE()` always calls `defer ServiceShutdown` — even on startup failure or panic.
| `c.PERFORM(task)` | First executor wins |
| `c.PerformAsync(task)` | Background goroutine |
## Error Handling
@ -88,15 +83,13 @@ Use `core.E()` for structured errors:
return core.E("service.Method", "what failed", underlyingErr)
```
**Never** use `fmt.Errorf`, `errors.New`, `os/exec`, or `unsafe.Pointer` on Core types.
## Test Naming
## Test Naming (AX-7)
`TestFile_Function_{Good,Bad,Ugly}` — 100% compliance.
`_Good` (happy path), `_Bad` (expected errors), `_Ugly` (panics/edge cases).
## Docs
Full API contract: `docs/RFC.md` (1476 lines, 21 sections).
Full documentation in `docs/`. Start with `docs/getting-started.md`.
## Go Workspace

View file

@ -1,38 +0,0 @@
# Specification Mismatches
## Scope
Findings are mismatches between current repository source behavior and existing docs/spec pages under `docs/`.
### 1) `docs/getting-started.md` uses deprecated constructor pattern
- Example and prose show `core.New(core.Options{...})` and say constructor reads only the first `core.Options`.
- Current code uses variadic `core.New(...CoreOption)` only; passing `core.Options` requires `core.WithOptions(core.NewOptions(...))`.
- References: `docs/getting-started.md:18`, `docs/getting-started.md:26`, `docs/getting-started.md:142`.
### 2) `docs/testing.md` and `docs/configuration.md` repeat outdated constructor usage
- Both files document `core.New(core.Options{...})` examples.
- Current constructor is variadic `CoreOption` values.
- References: `docs/testing.md:29`, `docs/configuration.md:16`.
### 3) `docs/lifecycle.md` claims registry order is map-backed and unstable
- File states `Startables()/Stoppables()` are built from a map-backed registry and therefore non-deterministic.
- Current `Registry` stores an explicit insertion-order slice and iterates in insertion order.
- References: `docs/lifecycle.md:64-67`.
### 4) `docs/services.md` stale ordering and lock-name behavior
- Claims registry is map-backed; actual behavior is insertion-order iteration.
- States default service lock name is `"srv"`, but `LockEnable`/`LockApply` do not expose/use a default namespace in implementation.
- References: `docs/services.md:53`, `docs/services.md:86-88`.
### 5) `docs/commands.md` documents removed managed lifecycle field
- Section “Lifecycle Commands” shows `Lifecycle` field with `Start/Stop/Restart/Reload/Signal` callbacks.
- Current `Command` struct has `Managed string` and no `Lifecycle` field.
- References: `docs/commands.md:155-159`.
### 6) `docs/subsystems.md` documents legacy options creation call for subsystem registration
- Uses `c.Data().New(core.Options{...})` and `c.Drive().New(core.Options{...})`.
- `Data.New` and `Drive.New` expect `core.Options` via varargs usage helpers (`core.NewOptions` in current docs/usage pattern).
- References: `docs/subsystems.md:44`, `docs/subsystems.md:75`, `docs/subsystems.md:80`.
### 7) `docs/index.md` RFC summary is stale
- Claims `docs/RFC.md` is 21 sections, 1476 lines, but current RFC content has expanded sections/size.
- Reference: `docs/index.md` table header note.

117
README.md
View file

@ -1,6 +1,8 @@
# CoreGO
Dependency injection, service lifecycle, permission, and message-passing for Go.
Dependency injection, service lifecycle, command routing, and message-passing for Go.
Import path:
```go
import "dappco.re/go/core"
@ -12,24 +14,75 @@ CoreGO is the foundation layer for the Core ecosystem. It gives you:
- one input shape: `Options`
- one output shape: `Result`
- one command tree: `Command`
- one message bus: `ACTION`, `QUERY` + named `Action` callables
- one permission gate: `Entitled`
- one collection primitive: `Registry[T]`
- one message bus: `ACTION`, `QUERY`, `PERFORM`
## Why It Exists
Most non-trivial Go systems end up needing the same small set of infrastructure:
- a place to keep runtime state and shared subsystems
- a predictable way to start and stop managed components
- a clean command surface for CLI-style workflows
- decoupled communication between components without tight imports
CoreGO keeps those pieces small and explicit.
## Quick Example
```go
package main
import "dappco.re/go/core"
import (
"context"
"fmt"
"dappco.re/go/core"
)
type flushCacheTask struct {
Name string
}
func main() {
c := core.New(
core.WithOption("name", "agent-workbench"),
core.WithService(cache.Register),
core.WithServiceLock(),
)
c.Run()
c := core.New(core.Options{
{Key: "name", Value: "agent-workbench"},
})
c.Service("cache", core.Service{
OnStart: func() core.Result {
core.Info("cache started", "app", c.App().Name)
return core.Result{OK: true}
},
OnStop: func() core.Result {
core.Info("cache stopped", "app", c.App().Name)
return core.Result{OK: true}
},
})
c.RegisterTask(func(_ *core.Core, task core.Task) core.Result {
switch t := task.(type) {
case flushCacheTask:
return core.Result{Value: "cache flushed for " + t.Name, OK: true}
}
return core.Result{}
})
c.Command("cache/flush", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(flushCacheTask{
Name: opts.String("name"),
})
},
})
if !c.ServiceStartup(context.Background(), nil).OK {
panic("startup failed")
}
r := c.Cli().Run("cache", "flush", "--name=session-store")
fmt.Println(r.Value)
_ = c.ServiceShutdown(context.Background())
}
```
@ -40,16 +93,22 @@ func main() {
| `Core` | Central container and access point |
| `Service` | Managed lifecycle component |
| `Command` | Path-based executable operation |
| `Action` | Named callable with panic recovery + entitlement |
| `Task` | Composed sequence of Actions |
| `Registry[T]` | Thread-safe named collection |
| `Process` | Managed execution (Action sugar) |
| `API` | Remote streams (protocol handlers) |
| `Entitlement` | Permission check result |
| `Cli` | CLI surface over the command tree |
| `Data` | Embedded filesystem mounts |
| `Drive` | Named transport handles |
| `Fs` | Local filesystem (sandboxable) |
| `Fs` | Local filesystem operations |
| `Config` | Runtime settings and feature flags |
| `I18n` | Locale collection and translation delegation |
| `E`, `Wrap`, `ErrorLog`, `ErrorPanic` | Structured failures and panic recovery |
## AX-Friendly Model
CoreGO follows the same design direction as the AX spec:
- predictable names over compressed names
- paths as documentation, such as `deploy/to/homelab`
- one repeated vocabulary across the framework
- examples that show how to call real APIs
## Install
@ -62,12 +121,30 @@ Requires Go 1.26 or later.
## Test
```bash
go test ./... # 483 tests, 84.7% coverage
core go test
```
Or with the standard toolchain:
```bash
go test ./...
```
## Docs
The authoritative API contract is `docs/RFC.md` (21 sections).
The full documentation set lives in `docs/`.
| Path | Covers |
|------|--------|
| `docs/getting-started.md` | First runnable CoreGO app |
| `docs/primitives.md` | `Options`, `Result`, `Service`, `Message`, `Query`, `Task` |
| `docs/services.md` | Service registry, runtime helpers, service locks |
| `docs/commands.md` | Path-based commands and CLI execution |
| `docs/messaging.md` | `ACTION`, `QUERY`, `QUERYALL`, `PERFORM`, `PerformAsync` |
| `docs/lifecycle.md` | Startup, shutdown, context, and task draining |
| `docs/subsystems.md` | `App`, `Data`, `Drive`, `Fs`, `I18n`, `Cli` |
| `docs/errors.md` | Structured errors, logging helpers, panic recovery |
| `docs/testing.md` | Test naming and framework testing patterns |
## License

233
action.go
View file

@ -1,233 +0,0 @@
// SPDX-License-Identifier: EUPL-1.2
// Named action system for the Core framework.
// Actions are the atomic unit of work — named, registered, invokable,
// and inspectable. The Action registry IS the capability map.
//
// Register a named action:
//
// c.Action("git.log", func(ctx context.Context, opts core.Options) core.Result {
// dir := opts.String("dir")
// return c.Process().RunIn(ctx, dir, "git", "log")
// })
//
// Invoke by name:
//
// r := c.Action("git.log").Run(ctx, core.NewOptions(
// core.Option{Key: "dir", Value: "/path/to/repo"},
// ))
//
// Check capability:
//
// if c.Action("process.run").Exists() { ... }
//
// List all:
//
// names := c.Actions() // ["process.run", "agentic.dispatch", ...]
package core
import "context"
// ActionHandler is the function signature for all named actions.
//
// func(ctx context.Context, opts core.Options) core.Result
type ActionHandler func(context.Context, Options) Result
// Action is a registered named action.
//
// action := c.Action("process.run")
// action.Description // "Execute a command"
// action.Schema // expected input keys
type Action struct {
Name string
Handler ActionHandler
Description string
Schema Options // declares expected input keys (optional)
enabled bool
core *Core // for entitlement checks during Run()
}
// Run executes the action with panic recovery.
// Returns Result{OK: false} if the action has no handler (not registered).
//
// r := c.Action("process.run").Run(ctx, opts)
func (a *Action) Run(ctx context.Context, opts Options) (result Result) {
if a == nil || a.Handler == nil {
return Result{E("action.Run", Concat("action not registered: ", a.safeName()), nil), false}
}
if !a.enabled {
return Result{E("action.Run", Concat("action disabled: ", a.Name), nil), false}
}
// Entitlement check — permission boundary
if a.core != nil {
if e := a.core.Entitled(a.Name); !e.Allowed {
return Result{E("action.Run", Concat("not entitled: ", a.Name, " — ", e.Reason), nil), false}
}
}
defer func() {
if r := recover(); r != nil {
result = Result{E("action.Run", Sprint("panic in action ", a.Name, ": ", r), nil), false}
}
}()
return a.Handler(ctx, opts)
}
// Exists returns true if this action has a registered handler.
//
// if c.Action("process.run").Exists() { ... }
func (a *Action) Exists() bool {
return a != nil && a.Handler != nil
}
func (a *Action) safeName() string {
if a == nil {
return "<nil>"
}
return a.Name
}
// --- Core accessor ---
// Action gets or registers a named action.
// With a handler argument: registers the action.
// Without: returns the action for invocation.
//
// c.Action("process.run", handler) // register
// c.Action("process.run").Run(ctx, opts) // invoke
// c.Action("process.run").Exists() // check
func (c *Core) Action(name string, handler ...ActionHandler) *Action {
if len(handler) > 0 {
def := &Action{Name: name, Handler: handler[0], enabled: true, core: c}
c.ipc.actions.Set(name, def)
return def
}
r := c.ipc.actions.Get(name)
if !r.OK {
return &Action{Name: name} // no handler — Exists() returns false
}
return r.Value.(*Action)
}
// Actions returns all registered named action names in registration order.
//
// names := c.Actions() // ["process.run", "agentic.dispatch"]
func (c *Core) Actions() []string {
return c.ipc.actions.Names()
}
// --- Task Composition ---
// Step is a single step in a Task — references an Action by name.
//
// core.Step{Action: "agentic.qa"}
// core.Step{Action: "agentic.poke", Async: true}
// core.Step{Action: "agentic.verify", Input: "previous"}
type Step struct {
Action string // name of the Action to invoke
With Options // static options (merged with runtime opts)
Async bool // run in background, don't block
Input string // "previous" = output of last step piped as input
}
// Task is a named sequence of Steps.
//
// c.Task("agent.completion", core.Task{
// Steps: []core.Step{
// {Action: "agentic.qa"},
// {Action: "agentic.auto-pr"},
// {Action: "agentic.verify"},
// {Action: "agentic.poke", Async: true},
// },
// })
type Task struct {
Name string
Description string
Steps []Step
}
// Run executes the task's steps in order. Sync steps run sequentially —
// if any fails, the chain stops. Async steps are dispatched and don't block.
// The "previous" input pipes the last sync step's output to the next step.
//
// r := c.Task("deploy").Run(ctx, opts)
func (t *Task) Run(ctx context.Context, c *Core, opts Options) Result {
if t == nil || len(t.Steps) == 0 {
return Result{E("task.Run", Concat("task has no steps: ", t.safeName()), nil), false}
}
var lastResult Result
for _, step := range t.Steps {
// Use step's own options, or runtime options if step has none
stepOpts := stepOptions(step)
if stepOpts.Len() == 0 {
stepOpts = opts
}
// Pipe previous result as input
if step.Input == "previous" && lastResult.OK {
stepOpts.Set("_input", lastResult.Value)
}
action := c.Action(step.Action)
if !action.Exists() {
return Result{E("task.Run", Concat("action not found: ", step.Action), nil), false}
}
if step.Async {
// Fire and forget — don't block the chain
go func(a *Action, o Options) {
defer func() {
if r := recover(); r != nil {
Error("async task step panicked", "action", a.Name, "panic", r)
}
}()
a.Run(ctx, o)
}(action, stepOpts)
continue
}
lastResult = action.Run(ctx, stepOpts)
if !lastResult.OK {
return lastResult
}
}
return lastResult
}
func (t *Task) safeName() string {
if t == nil {
return "<nil>"
}
return t.Name
}
// mergeStepOptions returns the step's With options — runtime opts are passed directly.
// Step.With provides static defaults that the step was registered with.
func stepOptions(step Step) Options {
return step.With
}
// Task gets or registers a named task.
// With a Task argument: registers the task.
// Without: returns the task for invocation.
//
// c.Task("deploy", core.Task{Steps: steps}) // register
// c.Task("deploy").Run(ctx, c, opts) // invoke
func (c *Core) Task(name string, def ...Task) *Task {
if len(def) > 0 {
d := def[0]
d.Name = name
c.ipc.tasks.Set(name, &d)
return &d
}
r := c.ipc.tasks.Get(name)
if !r.OK {
return &Task{Name: name}
}
return r.Value.(*Task)
}
// Tasks returns all registered task names.
func (c *Core) Tasks() []string {
return c.ipc.tasks.Names()
}

View file

@ -1,59 +0,0 @@
package core_test
import (
"context"
. "dappco.re/go/core"
)
func ExampleAction_Run() {
c := New()
c.Action("double", func(_ context.Context, opts Options) Result {
return Result{Value: opts.Int("n") * 2, OK: true}
})
r := c.Action("double").Run(context.Background(), NewOptions(
Option{Key: "n", Value: 21},
))
Println(r.Value)
// Output: 42
}
func ExampleAction_Exists() {
c := New()
Println(c.Action("missing").Exists())
c.Action("present", func(_ context.Context, _ Options) Result { return Result{OK: true} })
Println(c.Action("present").Exists())
// Output:
// false
// true
}
func ExampleAction_Run_panicRecovery() {
c := New()
c.Action("boom", func(_ context.Context, _ Options) Result {
panic("explosion")
})
r := c.Action("boom").Run(context.Background(), NewOptions())
Println(r.OK)
// Output: false
}
func ExampleAction_Run_entitlementDenied() {
c := New()
c.Action("premium", func(_ context.Context, _ Options) Result {
return Result{Value: "secret", OK: true}
})
c.SetEntitlementChecker(func(action string, _ int, _ context.Context) Entitlement {
if action == "premium" {
return Entitlement{Allowed: false, Reason: "upgrade"}
}
return Entitlement{Allowed: true, Unlimited: true}
})
r := c.Action("premium").Run(context.Background(), NewOptions())
Println(r.OK)
// Output: false
}

View file

@ -1,246 +0,0 @@
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- NamedAction Register ---
func TestAction_NamedAction_Good_Register(t *testing.T) {
c := New()
def := c.Action("process.run", func(_ context.Context, opts Options) Result {
return Result{Value: "output", OK: true}
})
assert.NotNil(t, def)
assert.Equal(t, "process.run", def.Name)
assert.True(t, def.Exists())
}
func TestAction_NamedAction_Good_Invoke(t *testing.T) {
c := New()
c.Action("git.log", func(_ context.Context, opts Options) Result {
dir := opts.String("dir")
return Result{Value: Concat("log from ", dir), OK: true}
})
r := c.Action("git.log").Run(context.Background(), NewOptions(
Option{Key: "dir", Value: "/repo"},
))
assert.True(t, r.OK)
assert.Equal(t, "log from /repo", r.Value)
}
func TestAction_NamedAction_Bad_NotRegistered(t *testing.T) {
c := New()
r := c.Action("missing.action").Run(context.Background(), NewOptions())
assert.False(t, r.OK, "invoking unregistered action must fail")
}
func TestAction_NamedAction_Good_Exists(t *testing.T) {
c := New()
c.Action("brain.recall", func(_ context.Context, _ Options) Result {
return Result{OK: true}
})
assert.True(t, c.Action("brain.recall").Exists())
assert.False(t, c.Action("brain.forget").Exists())
}
func TestAction_NamedAction_Ugly_PanicRecovery(t *testing.T) {
c := New()
c.Action("explode", func(_ context.Context, _ Options) Result {
panic("boom")
})
r := c.Action("explode").Run(context.Background(), NewOptions())
assert.False(t, r.OK, "panicking action must return !OK, not crash")
err, ok := r.Value.(error)
assert.True(t, ok)
assert.Contains(t, err.Error(), "panic")
}
func TestAction_NamedAction_Ugly_NilAction(t *testing.T) {
var def *Action
r := def.Run(context.Background(), NewOptions())
assert.False(t, r.OK)
assert.False(t, def.Exists())
}
// --- Actions listing ---
func TestAction_Actions_Good(t *testing.T) {
c := New()
c.Action("process.run", func(_ context.Context, _ Options) Result { return Result{OK: true} })
c.Action("process.kill", func(_ context.Context, _ Options) Result { return Result{OK: true} })
c.Action("agentic.dispatch", func(_ context.Context, _ Options) Result { return Result{OK: true} })
names := c.Actions()
assert.Len(t, names, 3)
assert.Equal(t, []string{"process.run", "process.kill", "agentic.dispatch"}, names)
}
func TestAction_Actions_Bad_Empty(t *testing.T) {
c := New()
assert.Empty(t, c.Actions())
}
// --- Action fields ---
func TestAction_NamedAction_Good_DescriptionAndSchema(t *testing.T) {
c := New()
def := c.Action("process.run", func(_ context.Context, _ Options) Result { return Result{OK: true} })
def.Description = "Execute a command synchronously"
def.Schema = NewOptions(
Option{Key: "command", Value: "string"},
Option{Key: "args", Value: "[]string"},
)
retrieved := c.Action("process.run")
assert.Equal(t, "Execute a command synchronously", retrieved.Description)
assert.True(t, retrieved.Schema.Has("command"))
}
// --- Permission by registration ---
func TestAction_NamedAction_Good_PermissionModel(t *testing.T) {
// Full Core — process registered
full := New()
full.Action("process.run", func(_ context.Context, _ Options) Result {
return Result{Value: "executed", OK: true}
})
// Sandboxed Core — no process
sandboxed := New()
// Full can execute
r := full.Action("process.run").Run(context.Background(), NewOptions())
assert.True(t, r.OK)
// Sandboxed returns not-registered
r = sandboxed.Action("process.run").Run(context.Background(), NewOptions())
assert.False(t, r.OK, "sandboxed Core must not have process capability")
}
// --- Action overwrite ---
func TestAction_NamedAction_Good_Overwrite(t *testing.T) {
c := New()
c.Action("hot.reload", func(_ context.Context, _ Options) Result {
return Result{Value: "v1", OK: true}
})
c.Action("hot.reload", func(_ context.Context, _ Options) Result {
return Result{Value: "v2", OK: true}
})
r := c.Action("hot.reload").Run(context.Background(), NewOptions())
assert.True(t, r.OK)
assert.Equal(t, "v2", r.Value, "latest handler wins")
}
// --- Task Composition ---
func TestAction_Task_Good_Sequential(t *testing.T) {
c := New()
var order []string
c.Action("step.a", func(_ context.Context, _ Options) Result {
order = append(order, "a")
return Result{Value: "output-a", OK: true}
})
c.Action("step.b", func(_ context.Context, _ Options) Result {
order = append(order, "b")
return Result{Value: "output-b", OK: true}
})
c.Task("pipeline", Task{
Steps: []Step{
{Action: "step.a"},
{Action: "step.b"},
},
})
r := c.Task("pipeline").Run(context.Background(), c, NewOptions())
assert.True(t, r.OK)
assert.Equal(t, []string{"a", "b"}, order, "steps must run in order")
assert.Equal(t, "output-b", r.Value, "last step's result is returned")
}
func TestAction_Task_Bad_StepFails(t *testing.T) {
c := New()
var order []string
c.Action("step.ok", func(_ context.Context, _ Options) Result {
order = append(order, "ok")
return Result{OK: true}
})
c.Action("step.fail", func(_ context.Context, _ Options) Result {
order = append(order, "fail")
return Result{Value: NewError("broke"), OK: false}
})
c.Action("step.never", func(_ context.Context, _ Options) Result {
order = append(order, "never")
return Result{OK: true}
})
c.Task("broken", Task{
Steps: []Step{
{Action: "step.ok"},
{Action: "step.fail"},
{Action: "step.never"},
},
})
r := c.Task("broken").Run(context.Background(), c, NewOptions())
assert.False(t, r.OK)
assert.Equal(t, []string{"ok", "fail"}, order, "chain stops on failure, step.never skipped")
}
func TestAction_Task_Bad_MissingAction(t *testing.T) {
c := New()
c.Task("missing", Task{
Steps: []Step{
{Action: "nonexistent"},
},
})
r := c.Task("missing").Run(context.Background(), c, NewOptions())
assert.False(t, r.OK)
}
func TestAction_Task_Good_PreviousInput(t *testing.T) {
c := New()
c.Action("produce", func(_ context.Context, _ Options) Result {
return Result{Value: "data-from-step-1", OK: true}
})
c.Action("consume", func(_ context.Context, opts Options) Result {
input := opts.Get("_input")
if !input.OK {
return Result{Value: "no input", OK: true}
}
return Result{Value: "got: " + input.Value.(string), OK: true}
})
c.Task("pipe", Task{
Steps: []Step{
{Action: "produce"},
{Action: "consume", Input: "previous"},
},
})
r := c.Task("pipe").Run(context.Background(), c, NewOptions())
assert.True(t, r.OK)
assert.Equal(t, "got: data-from-step-1", r.Value)
}
func TestAction_Task_Ugly_EmptySteps(t *testing.T) {
c := New()
c.Task("empty", Task{})
r := c.Task("empty").Run(context.Background(), c, NewOptions())
assert.False(t, r.OK)
}
func TestAction_Tasks_Good(t *testing.T) {
c := New()
c.Task("deploy", Task{Steps: []Step{{Action: "x"}}})
c.Task("review", Task{Steps: []Step{{Action: "y"}}})
assert.Equal(t, []string{"deploy", "review"}, c.Tasks())
}

157
api.go
View file

@ -1,157 +0,0 @@
// SPDX-License-Identifier: EUPL-1.2
// Remote communication primitive for the Core framework.
// API manages named streams to remote endpoints. The transport protocol
// (HTTP, WebSocket, SSE, MCP, TCP) is handled by protocol handlers
// registered by consumer packages.
//
// Drive is the phone book (WHERE to connect).
// API is the phone (HOW to connect).
//
// Usage:
//
// // Configure endpoint
// c.Drive().New(core.NewOptions(
// core.Option{Key: "name", Value: "charon"},
// core.Option{Key: "transport", Value: "http://10.69.69.165:9101/mcp"},
// ))
//
// // Open stream
// s := c.API().Stream("charon")
// if s.OK { stream := s.Value.(Stream) }
//
// // Remote Action dispatch
// r := c.API().Call("charon", "agentic.status", opts)
package core
import "context"
// Stream is a bidirectional connection to a remote endpoint.
// Consumers implement this for each transport protocol.
//
// type httpStream struct { ... }
// func (s *httpStream) Send(data []byte) error { ... }
// func (s *httpStream) Receive() ([]byte, error) { ... }
// func (s *httpStream) Close() error { ... }
type Stream interface {
Send(data []byte) error
Receive() ([]byte, error)
Close() error
}
// StreamFactory creates a Stream from a DriveHandle's transport config.
// Registered per-protocol by consumer packages.
type StreamFactory func(handle *DriveHandle) (Stream, error)
// API manages remote streams and protocol handlers.
type API struct {
core *Core
protocols *Registry[StreamFactory]
}
// API returns the remote communication primitive.
//
// c.API().Stream("charon")
func (c *Core) API() *API {
return c.api
}
// RegisterProtocol registers a stream factory for a URL scheme.
// Consumer packages call this during OnStartup.
//
// c.API().RegisterProtocol("http", httpStreamFactory)
// c.API().RegisterProtocol("https", httpStreamFactory)
// c.API().RegisterProtocol("mcp", mcpStreamFactory)
func (a *API) RegisterProtocol(scheme string, factory StreamFactory) {
a.protocols.Set(scheme, factory)
}
// Stream opens a connection to a named endpoint.
// Looks up the endpoint in Drive, extracts the protocol from the transport URL,
// and delegates to the registered protocol handler.
//
// r := c.API().Stream("charon")
// if r.OK { stream := r.Value.(Stream) }
func (a *API) Stream(name string) Result {
r := a.core.Drive().Get(name)
if !r.OK {
return Result{E("api.Stream", Concat("endpoint not found in Drive: ", name), nil), false}
}
handle := r.Value.(*DriveHandle)
scheme := extractScheme(handle.Transport)
fr := a.protocols.Get(scheme)
if !fr.OK {
return Result{E("api.Stream", Concat("no protocol handler for scheme: ", scheme), nil), false}
}
factory := fr.Value.(StreamFactory)
stream, err := factory(handle)
if err != nil {
return Result{err, false}
}
return Result{stream, true}
}
// Call invokes a named Action on a remote endpoint.
// This is the remote equivalent of c.Action("name").Run(ctx, opts).
//
// r := c.API().Call("charon", "agentic.status", opts)
func (a *API) Call(endpoint string, action string, opts Options) Result {
r := a.Stream(endpoint)
if !r.OK {
return r
}
stream := r.Value.(Stream)
defer stream.Close()
// Encode the action call as JSON-RPC (MCP compatible)
payload := Concat(`{"action":"`, action, `","options":`, JSONMarshalString(opts), `}`)
if err := stream.Send([]byte(payload)); err != nil {
return Result{err, false}
}
response, err := stream.Receive()
if err != nil {
return Result{err, false}
}
return Result{string(response), true}
}
// Protocols returns all registered protocol scheme names.
func (a *API) Protocols() []string {
return a.protocols.Names()
}
// extractScheme pulls the protocol from a transport URL.
// "http://host:port/path" → "http"
// "mcp://host:port" → "mcp"
func extractScheme(transport string) string {
for i, c := range transport {
if c == ':' {
return transport[:i]
}
}
return transport
}
// RemoteAction resolves "host:action.name" syntax for transparent remote dispatch.
// If the action name contains ":", the prefix is the endpoint and the suffix is the action.
//
// c.Action("charon:agentic.status") // → c.API().Call("charon", "agentic.status", opts)
func (c *Core) RemoteAction(name string, ctx context.Context, opts Options) Result {
for i, ch := range name {
if ch == ':' {
endpoint := name[:i]
action := name[i+1:]
return c.API().Call(endpoint, action, opts)
}
}
// No ":" — local action
return c.Action(name).Run(ctx, opts)
}

View file

@ -1,49 +0,0 @@
package core_test
import (
"context"
. "dappco.re/go/core"
)
func ExampleAPI_RegisterProtocol() {
c := New()
c.API().RegisterProtocol("http", func(h *DriveHandle) (Stream, error) {
return &mockStream{response: []byte("pong")}, nil
})
Println(c.API().Protocols())
// Output: [http]
}
func ExampleAPI_Stream() {
c := New()
c.API().RegisterProtocol("http", func(h *DriveHandle) (Stream, error) {
return &mockStream{response: []byte(Concat("connected to ", h.Name))}, nil
})
c.Drive().New(NewOptions(
Option{Key: "name", Value: "charon"},
Option{Key: "transport", Value: "http://10.69.69.165:9101"},
))
r := c.API().Stream("charon")
if r.OK {
stream := r.Value.(Stream)
resp, _ := stream.Receive()
Println(string(resp))
stream.Close()
}
// Output: connected to charon
}
func ExampleCore_RemoteAction() {
c := New()
// Local action
c.Action("status", func(_ context.Context, _ Options) Result {
return Result{Value: "running", OK: true}
})
// No colon — resolves locally
r := c.RemoteAction("status", context.Background(), NewOptions())
Println(r.Value)
// Output: running
}

View file

@ -1,156 +0,0 @@
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- mock stream for testing ---
type mockStream struct {
sent []byte
response []byte
closed bool
}
func (s *mockStream) Send(data []byte) error {
s.sent = data
return nil
}
func (s *mockStream) Receive() ([]byte, error) {
return s.response, nil
}
func (s *mockStream) Close() error {
s.closed = true
return nil
}
func mockFactory(response string) StreamFactory {
return func(handle *DriveHandle) (Stream, error) {
return &mockStream{response: []byte(response)}, nil
}
}
// --- API ---
func TestApi_API_Good_Accessor(t *testing.T) {
c := New()
assert.NotNil(t, c.API())
}
// --- RegisterProtocol ---
func TestApi_RegisterProtocol_Good(t *testing.T) {
c := New()
c.API().RegisterProtocol("http", mockFactory("ok"))
assert.Contains(t, c.API().Protocols(), "http")
}
// --- Stream ---
func TestApi_Stream_Good(t *testing.T) {
c := New()
c.API().RegisterProtocol("http", mockFactory("pong"))
c.Drive().New(NewOptions(
Option{Key: "name", Value: "charon"},
Option{Key: "transport", Value: "http://10.69.69.165:9101/mcp"},
))
r := c.API().Stream("charon")
assert.True(t, r.OK)
stream := r.Value.(Stream)
stream.Send([]byte("ping"))
resp, err := stream.Receive()
assert.NoError(t, err)
assert.Equal(t, "pong", string(resp))
stream.Close()
}
func TestApi_Stream_Bad_EndpointNotFound(t *testing.T) {
c := New()
r := c.API().Stream("nonexistent")
assert.False(t, r.OK)
}
func TestApi_Stream_Bad_NoProtocolHandler(t *testing.T) {
c := New()
c.Drive().New(NewOptions(
Option{Key: "name", Value: "unknown"},
Option{Key: "transport", Value: "grpc://host:port"},
))
r := c.API().Stream("unknown")
assert.False(t, r.OK)
}
// --- Call ---
func TestApi_Call_Good(t *testing.T) {
c := New()
c.API().RegisterProtocol("http", mockFactory(`{"status":"ok"}`))
c.Drive().New(NewOptions(
Option{Key: "name", Value: "charon"},
Option{Key: "transport", Value: "http://10.69.69.165:9101"},
))
r := c.API().Call("charon", "agentic.status", NewOptions())
assert.True(t, r.OK)
assert.Contains(t, r.Value.(string), "ok")
}
func TestApi_Call_Bad_EndpointNotFound(t *testing.T) {
c := New()
r := c.API().Call("missing", "action", NewOptions())
assert.False(t, r.OK)
}
// --- RemoteAction ---
func TestApi_RemoteAction_Good_Local(t *testing.T) {
c := New()
c.Action("local.action", func(_ context.Context, _ Options) Result {
return Result{Value: "local", OK: true}
})
r := c.RemoteAction("local.action", context.Background(), NewOptions())
assert.True(t, r.OK)
assert.Equal(t, "local", r.Value)
}
func TestApi_RemoteAction_Good_Remote(t *testing.T) {
c := New()
c.API().RegisterProtocol("http", mockFactory(`{"value":"remote"}`))
c.Drive().New(NewOptions(
Option{Key: "name", Value: "charon"},
Option{Key: "transport", Value: "http://10.69.69.165:9101"},
))
r := c.RemoteAction("charon:agentic.status", context.Background(), NewOptions())
assert.True(t, r.OK)
assert.Contains(t, r.Value.(string), "remote")
}
func TestApi_RemoteAction_Ugly_NoColon(t *testing.T) {
c := New()
// No colon — falls through to local action (which doesn't exist)
r := c.RemoteAction("nonexistent", context.Background(), NewOptions())
assert.False(t, r.OK, "non-existent local action should fail")
}
// --- extractScheme ---
func TestApi_Ugly_SchemeExtraction(t *testing.T) {
c := New()
// Verify scheme parsing works by registering different protocols
c.API().RegisterProtocol("http", mockFactory("http"))
c.API().RegisterProtocol("mcp", mockFactory("mcp"))
c.API().RegisterProtocol("ws", mockFactory("ws"))
assert.Equal(t, 3, len(c.API().Protocols()))
}

50
app.go
View file

@ -5,7 +5,7 @@
package core
import (
"os"
"os/exec"
"path/filepath"
)
@ -47,47 +47,21 @@ func (a App) New(opts Options) App {
}
// Find locates a program on PATH and returns a Result containing the App.
// Uses os.Stat to search PATH directories — no os/exec dependency.
//
// r := core.App{}.Find("node", "Node.js")
// if r.OK { app := r.Value.(*App) }
func (a App) Find(filename, name string) Result {
// If filename contains a separator, check it directly
if Contains(filename, string(os.PathSeparator)) {
abs, err := filepath.Abs(filename)
if err != nil {
return Result{err, false}
}
if isExecutable(abs) {
return Result{&App{Name: name, Filename: filename, Path: abs}, true}
}
return Result{E("app.Find", Concat(filename, " not found"), nil), false}
}
// Search PATH
pathEnv := os.Getenv("PATH")
if pathEnv == "" {
return Result{E("app.Find", "PATH is empty", nil), false}
}
for _, dir := range Split(pathEnv, string(os.PathListSeparator)) {
candidate := filepath.Join(dir, filename)
if isExecutable(candidate) {
abs, err := filepath.Abs(candidate)
if err != nil {
continue
}
return Result{&App{Name: name, Filename: filename, Path: abs}, true}
}
}
return Result{E("app.Find", Concat(filename, " not found on PATH"), nil), false}
}
// isExecutable checks if a path exists and is executable.
func isExecutable(path string) bool {
info, err := os.Stat(path)
path, err := exec.LookPath(filename)
if err != nil {
return false
return Result{err, false}
}
// Regular file with at least one execute bit
return !info.IsDir() && info.Mode()&0111 != 0
abs, err := filepath.Abs(path)
if err != nil {
return Result{err, false}
}
return Result{&App{
Name: name,
Filename: filename,
Path: abs,
}, true}
}

View file

@ -37,18 +37,18 @@ func TestApp_New_Partial_Good(t *testing.T) {
// --- App via Core ---
func TestApp_Core_Good(t *testing.T) {
c := New(WithOption("name", "myapp"))
c := New(WithOption("name", "myapp")).Value.(*Core)
assert.Equal(t, "myapp", c.App().Name)
}
func TestApp_Core_Empty_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.NotNil(t, c.App())
assert.Equal(t, "", c.App().Name)
}
func TestApp_Runtime_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.App().Runtime = &struct{ Name string }{Name: "wails"}
assert.NotNil(t, c.App().Runtime)
}

View file

@ -1,41 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleNewArray() {
a := NewArray[string]()
a.Add("alpha")
a.Add("bravo")
a.Add("charlie")
Println(a.Len())
Println(a.Contains("bravo"))
// Output:
// 3
// true
}
func ExampleArray_AddUnique() {
a := NewArray[string]()
a.AddUnique("alpha")
a.AddUnique("alpha") // no duplicate
a.AddUnique("bravo")
Println(a.Len())
// Output: 2
}
func ExampleArray_Filter() {
a := NewArray[int]()
a.Add(1)
a.Add(2)
a.Add(3)
a.Add(4)
r := a.Filter(func(n int) bool { return n%2 == 0 })
Println(r.OK)
// Output: true
}

26
cli.go
View file

@ -64,7 +64,11 @@ func (cl *Cli) Run(args ...string) Result {
return Result{}
}
if c.commands.Len() == 0 {
c.commands.mu.RLock()
cmdCount := len(c.commands.commands)
c.commands.mu.RUnlock()
if cmdCount == 0 {
if cl.banner != nil {
cl.Print(cl.banner(cl))
}
@ -75,14 +79,16 @@ func (cl *Cli) Run(args ...string) Result {
var cmd *Command
var remaining []string
c.commands.mu.RLock()
for i := len(clean); i > 0; i-- {
path := JoinPath(clean[:i]...)
if r := c.commands.Get(path); r.OK {
cmd = r.Value.(*Command)
if found, ok := c.commands.commands[path]; ok {
cmd = found
remaining = clean[i:]
break
}
}
c.commands.mu.RUnlock()
if cmd == nil {
if cl.banner != nil {
@ -110,6 +116,9 @@ func (cl *Cli) Run(args ...string) Result {
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}
}
@ -132,9 +141,12 @@ func (cl *Cli) PrintHelp() {
cl.Print("Commands:")
}
c.commands.Each(func(path string, cmd *Command) {
if cmd.Hidden || (cmd.Action == nil && !cmd.IsManaged()) {
return
c.commands.mu.RLock()
defer c.commands.mu.RUnlock()
for path, cmd := range c.commands.commands {
if cmd.Hidden || (cmd.Action == nil && cmd.Lifecycle == nil) {
continue
}
tr := c.I18n().Translate(cmd.I18nKey())
desc, _ := tr.Value.(string)
@ -143,7 +155,7 @@ func (cl *Cli) PrintHelp() {
} else {
cl.Print(" %-30s %s", path, desc)
}
})
}
}
// SetBanner sets the banner function.

View file

@ -11,23 +11,23 @@ import (
// --- Cli Surface ---
func TestCli_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.NotNil(t, c.Cli())
}
func TestCli_Banner_Good(t *testing.T) {
c := New(WithOption("name", "myapp"))
c := New(WithOption("name", "myapp")).Value.(*Core)
assert.Equal(t, "myapp", c.Cli().Banner())
}
func TestCli_SetBanner_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
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()
c := New().Value.(*Core)
executed := false
c.Command("hello", Command{Action: func(_ Options) Result {
executed = true
@ -40,7 +40,7 @@ func TestCli_Run_Good(t *testing.T) {
}
func TestCli_Run_Nested_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
executed := false
c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result {
executed = true
@ -52,7 +52,7 @@ func TestCli_Run_Nested_Good(t *testing.T) {
}
func TestCli_Run_WithFlags_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
var received Options
c.Command("serve", Command{Action: func(opts Options) Result {
received = opts
@ -64,20 +64,20 @@ func TestCli_Run_WithFlags_Good(t *testing.T) {
}
func TestCli_Run_NoCommand_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Cli().Run()
assert.False(t, r.OK)
}
func TestCli_PrintHelp_Good(t *testing.T) {
c := New(WithOption("name", "myapp"))
c := New(WithOption("name", "myapp")).Value.(*Core)
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("serve", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Cli().PrintHelp()
}
func TestCli_SetOutput_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
var buf bytes.Buffer
c.Cli().SetOutput(&buf)
c.Cli().Print("hello %s", "world")

View file

@ -20,31 +20,37 @@
// "deploy/to/homelab" → "cmd.deploy.to.homelab.description"
package core
import (
"sync"
)
// CommandAction is the function signature for command handlers.
//
// func(opts core.Options) core.Result
type CommandAction func(Options) Result
// CommandLifecycle is implemented by commands that support managed lifecycle.
// Basic commands only need an action. Daemon commands implement Start/Stop/Signal
// via go-process.
type CommandLifecycle interface {
Start(Options) Result
Stop() Result
Restart() Result
Reload() Result
Signal(string) Result
}
// Command is the DTO for an executable operation.
// Commands are declarative — they carry enough information for multiple consumers:
// - core.Cli() runs the Action
// - core/cli adds rich help, completion, man pages
// - go-process wraps Managed commands with lifecycle (PID, health, signals)
//
// c.Command("serve", core.Command{
// Action: handler,
// Managed: "process.daemon", // go-process provides start/stop/restart
// })
type Command struct {
Name string
Description string // i18n key — derived from path if empty
Path string // "deploy/to/homelab"
Action CommandAction // business logic
Managed string // "" = one-shot, "process.daemon" = managed lifecycle
Flags Options // declared flags
Description string // i18n key — derived from path if empty
Path string // "deploy/to/homelab"
Action CommandAction // business logic
Lifecycle CommandLifecycle // optional — provided by go-process
Flags Options // declared flags
Hidden bool
commands map[string]*Command // child commands (internal)
mu sync.RWMutex
}
// I18nKey returns the i18n key for this command's description.
@ -71,19 +77,52 @@ func (cmd *Command) Run(opts Options) Result {
return cmd.Action(opts)
}
// IsManaged returns true if this command has a managed lifecycle.
//
// if cmd.IsManaged() { /* go-process handles start/stop */ }
func (cmd *Command) IsManaged() bool {
return cmd.Managed != ""
// Start delegates to the lifecycle implementation if available.
func (cmd *Command) Start(opts Options) Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Start(opts)
}
return cmd.Run(opts)
}
// Stop delegates to the lifecycle implementation.
func (cmd *Command) Stop() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Stop()
}
return Result{}
}
// Restart delegates to the lifecycle implementation.
func (cmd *Command) Restart() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Restart()
}
return Result{}
}
// Reload delegates to the lifecycle implementation.
func (cmd *Command) Reload() Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Reload()
}
return Result{}
}
// Signal delegates to the lifecycle implementation.
func (cmd *Command) Signal(sig string) Result {
if cmd.Lifecycle != nil {
return cmd.Lifecycle.Signal(sig)
}
return Result{}
}
// --- Command Registry (on Core) ---
// CommandRegistry holds the command tree. Embeds Registry[*Command]
// for thread-safe named storage with insertion order.
type CommandRegistry struct {
*Registry[*Command]
// commandRegistry holds the command tree.
type commandRegistry struct {
commands map[string]*Command
mu sync.RWMutex
}
// Command gets or registers a command by path.
@ -92,19 +131,21 @@ type CommandRegistry struct {
// r := c.Command("deploy")
func (c *Core) Command(path string, command ...Command) Result {
if len(command) == 0 {
return c.commands.Get(path)
c.commands.mu.RLock()
cmd, ok := c.commands.commands[path]
c.commands.mu.RUnlock()
return Result{cmd, ok}
}
if path == "" || HasPrefix(path, "/") || HasSuffix(path, "/") || Contains(path, "//") {
return Result{E("core.Command", Concat("invalid command path: \"", path, "\""), nil), false}
}
// Check for duplicate executable command
if r := c.commands.Get(path); r.OK {
existing := r.Value.(*Command)
if existing.Action != nil || existing.IsManaged() {
return Result{E("core.Command", Concat("command \"", path, "\" already registered"), nil), false}
}
c.commands.mu.Lock()
defer c.commands.mu.Unlock()
if existing, exists := c.commands.commands[path]; exists && (existing.Action != nil || existing.Lifecycle != nil) {
return Result{E("core.Command", Concat("command \"", path, "\" already registered"), nil), false}
}
cmd := &command[0]
@ -115,8 +156,7 @@ func (c *Core) Command(path string, command ...Command) Result {
}
// Preserve existing subtree when overwriting a placeholder parent
if r := c.commands.Get(path); r.OK {
existing := r.Value.(*Command)
if existing, exists := c.commands.commands[path]; exists {
for k, v := range existing.commands {
if _, has := cmd.commands[k]; !has {
cmd.commands[k] = v
@ -124,35 +164,40 @@ func (c *Core) Command(path string, command ...Command) Result {
}
}
c.commands.Set(path, cmd)
c.commands.commands[path] = cmd
// Build parent chain — "deploy/to/homelab" creates "deploy" and "deploy/to" if missing
parts := Split(path, "/")
for i := len(parts) - 1; i > 0; i-- {
parentPath := JoinPath(parts[:i]...)
if !c.commands.Has(parentPath) {
c.commands.Set(parentPath, &Command{
if _, exists := c.commands.commands[parentPath]; !exists {
c.commands.commands[parentPath] = &Command{
Name: parts[i-1],
Path: parentPath,
commands: make(map[string]*Command),
})
}
}
parent := c.commands.Get(parentPath).Value.(*Command)
parent.commands[parts[i]] = cmd
cmd = parent
c.commands.commands[parentPath].commands[parts[i]] = cmd
cmd = c.commands.commands[parentPath]
}
return Result{OK: true}
}
// Commands returns all registered command paths in registration order.
// Commands returns all registered command paths.
//
// paths := c.Commands()
func (c *Core) Commands() []string {
if c.commands == nil {
return nil
}
return c.commands.Names()
c.commands.mu.RLock()
defer c.commands.mu.RUnlock()
var paths []string
for k := range c.commands.commands {
paths = append(paths, k)
}
return paths
}
// pathName extracts the last segment of a path.

View file

@ -1,40 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleCore_Command_register() {
c := New()
c.Command("deploy/to/homelab", Command{
Description: "Deploy to homelab",
Action: func(opts Options) Result {
return Result{Value: "deployed", OK: true}
},
})
Println(c.Command("deploy/to/homelab").OK)
// Output: true
}
func ExampleCore_Command_managed() {
c := New()
c.Command("serve", Command{
Action: func(_ Options) Result { return Result{OK: true} },
Managed: "process.daemon",
})
cmd := c.Command("serve").Value.(*Command)
Println(cmd.IsManaged())
// Output: true
}
func ExampleCore_Commands() {
c := New()
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("test", Command{Action: func(_ Options) Result { return Result{OK: true} }})
Println(c.Commands())
// Output: [deploy test]
}

View file

@ -10,7 +10,7 @@ import (
// --- Command DTO ---
func TestCommand_Register_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Command("deploy", Command{Action: func(_ Options) Result {
return Result{Value: "deployed", OK: true}
}})
@ -18,7 +18,7 @@ func TestCommand_Register_Good(t *testing.T) {
}
func TestCommand_Get_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
r := c.Command("deploy")
assert.True(t, r.OK)
@ -26,13 +26,13 @@ func TestCommand_Get_Good(t *testing.T) {
}
func TestCommand_Get_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Command("nonexistent")
assert.False(t, r.OK)
}
func TestCommand_Run_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Command("greet", Command{Action: func(opts Options) Result {
return Result{Value: Concat("hello ", opts.String("name")), OK: true}
}})
@ -43,7 +43,7 @@ func TestCommand_Run_Good(t *testing.T) {
}
func TestCommand_Run_NoAction_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Command("empty", Command{Description: "no action"})
cmd := c.Command("empty").Value.(*Command)
r := cmd.Run(NewOptions())
@ -53,7 +53,7 @@ func TestCommand_Run_NoAction_Good(t *testing.T) {
// --- Nested Commands ---
func TestCommand_Nested_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result {
return Result{Value: "deployed to homelab", OK: true}
}})
@ -67,7 +67,7 @@ func TestCommand_Nested_Good(t *testing.T) {
}
func TestCommand_Paths_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("serve", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("deploy/to/homelab", Command{Action: func(_ Options) Result { return Result{OK: true} }})
@ -82,77 +82,127 @@ func TestCommand_Paths_Good(t *testing.T) {
// --- I18n Key Derivation ---
func TestCommand_I18nKey_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
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 := New().Value.(*Core)
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 := New().Value.(*Core)
c.Command("serve", Command{})
cmd := c.Command("serve").Value.(*Command)
assert.Equal(t, "cmd.serve.description", cmd.I18nKey())
}
// --- Managed ---
// --- Lifecycle ---
func TestCommand_IsManaged_Good(t *testing.T) {
c := New()
c.Command("serve", Command{
Action: func(_ Options) Result { return Result{Value: "running", OK: true} },
Managed: "process.daemon",
})
func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) {
c := New().Value.(*Core)
c.Command("serve", Command{Action: func(_ Options) Result {
return Result{Value: "running", OK: true}
}})
cmd := c.Command("serve").Value.(*Command)
assert.True(t, cmd.IsManaged())
r := cmd.Start(NewOptions())
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)
}
func TestCommand_IsManaged_Bad_NotManaged(t *testing.T) {
c := New()
c.Command("deploy", Command{
Action: func(_ Options) Result { return Result{OK: true} },
})
cmd := c.Command("deploy").Value.(*Command)
assert.False(t, cmd.IsManaged())
// --- Lifecycle with Implementation ---
type testLifecycle struct {
started bool
stopped bool
restarted bool
reloaded bool
signalled string
}
func (l *testLifecycle) Start(opts Options) Result {
l.started = true
return Result{Value: "started", OK: true}
}
func (l *testLifecycle) Stop() Result {
l.stopped = true
return Result{OK: true}
}
func (l *testLifecycle) Restart() Result {
l.restarted = true
return Result{OK: true}
}
func (l *testLifecycle) Reload() Result {
l.reloaded = true
return Result{OK: true}
}
func (l *testLifecycle) Signal(sig string) Result {
l.signalled = sig
return Result{Value: sig, OK: true}
}
func TestCommand_Lifecycle_WithImpl_Good(t *testing.T) {
c := New().Value.(*Core)
lc := &testLifecycle{}
c.Command("daemon", Command{Lifecycle: lc})
cmd := c.Command("daemon").Value.(*Command)
r := cmd.Start(NewOptions())
assert.True(t, r.OK)
assert.True(t, lc.started)
assert.True(t, cmd.Stop().OK)
assert.True(t, lc.stopped)
assert.True(t, cmd.Restart().OK)
assert.True(t, lc.restarted)
assert.True(t, cmd.Reload().OK)
assert.True(t, lc.reloaded)
r = cmd.Signal("HUP")
assert.True(t, r.OK)
assert.Equal(t, "HUP", lc.signalled)
}
func TestCommand_Duplicate_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
r := c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
assert.False(t, r.OK)
}
func TestCommand_InvalidPath_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.False(t, c.Command("/leading", Command{}).OK)
assert.False(t, c.Command("trailing/", Command{}).OK)
assert.False(t, c.Command("double//slash", Command{}).OK)
}
// --- Cli Run with Managed ---
// --- Cli Run with Lifecycle ---
func TestCli_Run_Managed_Good(t *testing.T) {
c := New()
ran := false
c.Command("serve", Command{
Action: func(_ Options) Result { ran = true; return Result{OK: true} },
Managed: "process.daemon",
})
func TestCli_Run_Lifecycle_Good(t *testing.T) {
c := New().Value.(*Core)
lc := &testLifecycle{}
c.Command("serve", Command{Lifecycle: lc})
r := c.Cli().Run("serve")
assert.True(t, r.OK)
assert.True(t, ran)
assert.True(t, lc.started)
}
func TestCli_Run_NoAction_Bad(t *testing.T) {
c := New()
func TestCli_Run_NoActionNoLifecycle_Bad(t *testing.T) {
c := New().Value.(*Core)
c.Command("empty", Command{})
r := c.Cli().Run("empty")
assert.False(t, r.OK)
@ -161,7 +211,7 @@ func TestCli_Run_NoAction_Bad(t *testing.T) {
// --- Empty path ---
func TestCommand_EmptyPath_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Command("", Command{})
assert.False(t, r.OK)
}

View file

@ -14,34 +14,15 @@ type ConfigVar[T any] struct {
set bool
}
// Get returns the current value.
//
// val := v.Get()
func (v *ConfigVar[T]) Get() T { return v.val }
// Set sets the value and marks it as explicitly set.
//
// v.Set(true)
func (v *ConfigVar[T]) Set(val T) { v.val = val; v.set = true }
// IsSet returns true if the value was explicitly set (distinguishes "set to false" from "never set").
//
// if v.IsSet() { /* explicitly configured */ }
func (v *ConfigVar[T]) Get() T { return v.val }
func (v *ConfigVar[T]) Set(val T) { v.val = val; v.set = true }
func (v *ConfigVar[T]) IsSet() bool { return v.set }
// Unset resets to zero value and marks as not set.
//
// v.Unset()
// v.IsSet() // false
func (v *ConfigVar[T]) Unset() {
v.set = false
var zero T
v.val = zero
}
// NewConfigVar creates a ConfigVar with an initial value marked as set.
//
// debug := core.NewConfigVar(true)
func NewConfigVar[T any](val T) ConfigVar[T] {
return ConfigVar[T]{val: val, set: true}
}
@ -67,15 +48,6 @@ type Config struct {
mu sync.RWMutex
}
// New initialises a Config with empty settings and features.
//
// cfg := (&core.Config{}).New()
func (e *Config) New() *Config {
e.ConfigOptions = &ConfigOptions{}
e.ConfigOptions.init()
return e
}
// Set stores a configuration value by key.
func (e *Config) Set(key string, val any) {
e.mu.Lock()
@ -101,20 +73,9 @@ func (e *Config) Get(key string) Result {
return Result{val, true}
}
// String retrieves a string config value (empty string if missing).
//
// host := c.Config().String("database.host")
func (e *Config) String(key string) string { return ConfigGet[string](e, key) }
// Int retrieves an int config value (0 if missing).
//
// port := c.Config().Int("database.port")
func (e *Config) Int(key string) int { return ConfigGet[int](e, key) }
// Bool retrieves a bool config value (false if missing).
//
// debug := c.Config().Bool("debug")
func (e *Config) Bool(key string) bool { return ConfigGet[bool](e, key) }
func (e *Config) Int(key string) int { return ConfigGet[int](e, key) }
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 {
@ -129,9 +90,6 @@ func ConfigGet[T any](e *Config, key string) T {
// --- Feature Flags ---
// Enable activates a feature flag.
//
// c.Config().Enable("dark-mode")
func (e *Config) Enable(feature string) {
e.mu.Lock()
if e.ConfigOptions == nil {
@ -142,9 +100,6 @@ func (e *Config) Enable(feature string) {
e.mu.Unlock()
}
// Disable deactivates a feature flag.
//
// c.Config().Disable("dark-mode")
func (e *Config) Disable(feature string) {
e.mu.Lock()
if e.ConfigOptions == nil {
@ -155,9 +110,6 @@ func (e *Config) Disable(feature string) {
e.mu.Unlock()
}
// Enabled returns true if a feature flag is active.
//
// if c.Config().Enabled("dark-mode") { ... }
func (e *Config) Enabled(feature string) bool {
e.mu.RLock()
defer e.mu.RUnlock()
@ -167,9 +119,6 @@ func (e *Config) Enabled(feature string) bool {
return e.Features[feature]
}
// EnabledFeatures returns all active feature flag names.
//
// features := c.Config().EnabledFeatures()
func (e *Config) EnabledFeatures() []string {
e.mu.RLock()
defer e.mu.RUnlock()

View file

@ -1,41 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleConfig_Set() {
c := New()
c.Config().Set("database.host", "localhost")
c.Config().Set("database.port", 5432)
Println(c.Config().String("database.host"))
Println(c.Config().Int("database.port"))
// Output:
// localhost
// 5432
}
func ExampleConfig_Enable() {
c := New()
c.Config().Enable("dark-mode")
c.Config().Enable("beta-features")
Println(c.Config().Enabled("dark-mode"))
Println(c.Config().EnabledFeatures())
// Output:
// true
// [dark-mode beta-features]
}
func ExampleConfigVar() {
v := NewConfigVar(42)
Println(v.Get(), v.IsSet())
v.Unset()
Println(v.Get(), v.IsSet())
// Output:
// 42 true
// 0 false
}

View file

@ -10,7 +10,7 @@ import (
// --- Config ---
func TestConfig_SetGet_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Config().Set("api_url", "https://api.lthn.ai")
c.Config().Set("max_agents", 5)
@ -20,14 +20,14 @@ func TestConfig_SetGet_Good(t *testing.T) {
}
func TestConfig_Get_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
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 := New().Value.(*Core)
c.Config().Set("url", "https://lthn.ai")
c.Config().Set("port", 8080)
c.Config().Set("debug", true)
@ -38,7 +38,7 @@ func TestConfig_TypedAccessors_Good(t *testing.T) {
}
func TestConfig_TypedAccessors_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
// Missing keys return zero values
assert.Equal(t, "", c.Config().String("missing"))
assert.Equal(t, 0, c.Config().Int("missing"))
@ -48,7 +48,7 @@ func TestConfig_TypedAccessors_Bad(t *testing.T) {
// --- Feature Flags ---
func TestConfig_Features_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Config().Enable("dark-mode")
c.Config().Enable("beta")
@ -58,7 +58,7 @@ func TestConfig_Features_Good(t *testing.T) {
}
func TestConfig_Features_Disable_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Config().Enable("feature")
assert.True(t, c.Config().Enabled("feature"))
@ -67,14 +67,14 @@ func TestConfig_Features_Disable_Good(t *testing.T) {
}
func TestConfig_Features_CaseSensitive(t *testing.T) {
c := New()
c := New().Value.(*Core)
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 := New().Value.(*Core)
c.Config().Enable("a")
c.Config().Enable("b")
c.Config().Enable("c")
@ -88,7 +88,7 @@ func TestConfig_EnabledFeatures_Good(t *testing.T) {
// --- ConfigVar ---
func TestConfig_ConfigVar_Good(t *testing.T) {
func TestConfigVar_Good(t *testing.T) {
v := NewConfigVar("hello")
assert.True(t, v.IsSet())
assert.Equal(t, "hello", v.Get())

View file

@ -7,7 +7,6 @@ package core
import (
"context"
"reflect"
"sync"
)
// Message is the type for IPC broadcasts (fire-and-forget).
@ -16,25 +15,30 @@ type Message any
// Query is the type for read-only IPC requests.
type Query any
// Task is the type for IPC requests that perform side effects.
type Task any
// TaskWithIdentifier is an optional interface for tasks that need to know their assigned identifier.
type TaskWithIdentifier interface {
Task
SetTaskIdentifier(id string)
GetTaskIdentifier() string
}
// QueryHandler handles Query requests. Returns Result{Value, OK}.
type QueryHandler func(*Core, Query) Result
// TaskHandler handles Task requests. Returns Result{Value, OK}.
type TaskHandler func(*Core, Task) Result
// Startable is implemented by services that need startup initialisation.
//
// func (s *MyService) OnStartup(ctx context.Context) core.Result {
// return core.Result{OK: true}
// }
type Startable interface {
OnStartup(ctx context.Context) Result
OnStartup(ctx context.Context) error
}
// Stoppable is implemented by services that need shutdown cleanup.
//
// func (s *MyService) OnShutdown(ctx context.Context) core.Result {
// return core.Result{OK: true}
// }
type Stoppable interface {
OnShutdown(ctx context.Context) Result
OnShutdown(ctx context.Context) error
}
// --- Action Messages ---
@ -44,21 +48,21 @@ type ActionServiceShutdown struct{}
type ActionTaskStarted struct {
TaskIdentifier string
Action string
Options Options
Task Task
}
type ActionTaskProgress struct {
TaskIdentifier string
Action string
Task Task
Progress float64
Message string
}
type ActionTaskCompleted struct {
TaskIdentifier string
Action string
Result Result
Task Task
Result any
Error error
}
// --- Constructor ---
@ -77,47 +81,44 @@ type CoreOption func(*Core) Result
// Services registered here form the application conclave — they share
// IPC access and participate in the lifecycle (ServiceStartup/ServiceShutdown).
//
// c := core.New(
// core.WithOption("name", "myapp"),
// r := core.New(
// core.WithOptions(core.NewOptions(core.Option{Key: "name", Value: "myapp"})),
// core.WithService(auth.Register),
// core.WithServiceLock(),
// )
// c.Run()
func New(opts ...CoreOption) *Core {
// if !r.OK { log.Fatal(r.Value) }
// c := r.Value.(*Core)
func New(opts ...CoreOption) Result {
c := &Core{
app: &App{},
data: &Data{Registry: NewRegistry[*Embed]()},
drive: &Drive{Registry: NewRegistry[*DriveHandle]()},
fs: (&Fs{}).New("/"),
config: (&Config{}).New(),
data: &Data{},
drive: &Drive{},
fs: &Fs{root: "/"},
config: &Config{ConfigOptions: &ConfigOptions{}},
error: &ErrorPanic{},
log: &ErrorLog{},
lock: &Lock{locks: NewRegistry[*sync.RWMutex]()},
ipc: &Ipc{actions: NewRegistry[*Action](), tasks: NewRegistry[*Task]()},
log: &ErrorLog{log: Default()},
lock: &Lock{},
ipc: &Ipc{},
info: systemInfo,
i18n: &I18n{},
api: &API{protocols: NewRegistry[StreamFactory]()},
services: &ServiceRegistry{Registry: NewRegistry[*Service]()},
commands: &CommandRegistry{Registry: NewRegistry[*Command]()},
entitlementChecker: defaultChecker,
services: &serviceRegistry{services: make(map[string]*Service)},
commands: &commandRegistry{commands: make(map[string]*Command)},
}
c.context, c.cancel = context.WithCancel(context.Background())
c.api.core = c
// Core services
CliRegister(c)
for _, opt := range opts {
if r := opt(c); !r.OK {
Error("core.New failed", "err", r.Value)
break
return r
}
}
// Apply service lock after all opts — v0.3.3 parity
c.LockApply()
return c
return Result{c, true}
}
// WithOptions applies key-value configuration to Core.
@ -167,7 +168,15 @@ func WithService(factory func(*Core) Result) CoreOption {
return Result{E("core.WithService", Sprintf("service name could not be discovered for type %T", instance), nil), false}
}
// RegisterService handles Startable/Stoppable/HandleIPCEvents discovery
// IPC handler discovery
instanceValue := reflect.ValueOf(instance)
handlerMethod := instanceValue.MethodByName("HandleIPCEvents")
if handlerMethod.IsValid() {
if handler, ok := handlerMethod.Interface().(func(*Core, Message) Result); ok {
c.RegisterAction(handler)
}
}
return c.RegisterService(name, instance)
}
}

View file

@ -31,8 +31,10 @@ func stubFactory(c *Core) Result {
// stubFactory lives in package "dappco.re/go/core_test", so the last path
// segment is "core_test" — WithService strips the "_test" suffix and registers
// the service under the name "core".
func TestContract_WithService_NameDiscovery_Good(t *testing.T) {
c := New(WithService(stubFactory))
func TestWithService_NameDiscovery_Good(t *testing.T) {
r := New(WithService(stubFactory))
assert.True(t, r.OK)
c := r.Value.(*Core)
names := c.Services()
// Service should be auto-registered under a discovered name (not just "cli" which is built-in)
@ -42,14 +44,16 @@ func TestContract_WithService_NameDiscovery_Good(t *testing.T) {
// TestWithService_FactorySelfRegisters_Good verifies that when a factory
// returns Result{OK:true} with no Value (it registered itself), WithService
// does not attempt a second registration and returns success.
func TestContract_WithService_FactorySelfRegisters_Good(t *testing.T) {
func TestWithService_FactorySelfRegisters_Good(t *testing.T) {
selfReg := func(c *Core) Result {
// Factory registers directly, returns no instance.
c.Service("self", Service{})
return Result{OK: true}
}
c := New(WithService(selfReg))
r := New(WithService(selfReg))
assert.True(t, r.OK)
c := r.Value.(*Core)
// "self" must be present and registered exactly once.
svc := c.Service("self")
@ -58,12 +62,14 @@ func TestContract_WithService_FactorySelfRegisters_Good(t *testing.T) {
// --- WithName ---
func TestContract_WithName_Good(t *testing.T) {
c := New(
func TestWithName_Good(t *testing.T) {
r := New(
WithName("custom", func(c *Core) Result {
return Result{Value: &stubNamedService{}, OK: true}
}),
)
assert.True(t, r.OK)
c := r.Value.(*Core)
assert.Contains(t, c.Services(), "custom")
}
@ -73,18 +79,20 @@ type lifecycleService struct {
started bool
}
func (s *lifecycleService) OnStartup(_ context.Context) Result {
func (s *lifecycleService) OnStartup(_ context.Context) error {
s.started = true
return Result{OK: true}
return nil
}
func TestContract_WithService_Lifecycle_Good(t *testing.T) {
func TestWithService_Lifecycle_Good(t *testing.T) {
svc := &lifecycleService{}
c := New(
r := New(
WithService(func(c *Core) Result {
return Result{Value: svc, OK: true}
}),
)
assert.True(t, r.OK)
c := r.Value.(*Core)
c.ServiceStartup(context.Background(), nil)
assert.True(t, svc.started)
@ -101,13 +109,15 @@ func (s *ipcService) HandleIPCEvents(c *Core, msg Message) Result {
return Result{OK: true}
}
func TestContract_WithService_IPCHandler_Good(t *testing.T) {
func TestWithService_IPCHandler_Good(t *testing.T) {
svc := &ipcService{}
c := New(
r := New(
WithService(func(c *Core) Result {
return Result{Value: svc, OK: true}
}),
)
assert.True(t, r.OK)
c := r.Value.(*Core)
c.ACTION("ping")
assert.Equal(t, "ping", svc.received)
@ -115,19 +125,11 @@ func TestContract_WithService_IPCHandler_Good(t *testing.T) {
// --- Error ---
// TestWithService_FactoryError_Bad verifies that a failing factory
// stops further option processing (second service not registered).
func TestContract_WithService_FactoryError_Bad(t *testing.T) {
secondCalled := false
c := New(
WithService(func(c *Core) Result {
return Result{Value: E("test", "factory failed", nil), OK: false}
}),
WithService(func(c *Core) Result {
secondCalled = true
return Result{OK: true}
}),
)
assert.NotNil(t, c)
assert.False(t, secondCalled, "second option should not run after first fails")
// TestWithService_FactoryError_Bad verifies that a factory returning an error
// causes New() to stop and propagate the failure.
func TestWithService_FactoryError_Bad(t *testing.T) {
r := New(WithService(func(c *Core) Result {
return Result{Value: E("test", "factory failed", nil), OK: false}
}))
assert.False(t, r.OK, "expected New() to fail when factory returns error")
}

191
core.go
View file

@ -7,7 +7,6 @@ package core
import (
"context"
"os"
"sync"
"sync/atomic"
)
@ -25,17 +24,13 @@ type Core struct {
error *ErrorPanic // c.Error() — Panic recovery and crash reporting
log *ErrorLog // c.Log() — Structured logging + error wrapping
// cli accessed via ServiceFor[*Cli](c, "cli")
commands *CommandRegistry // c.Command("path") — Command tree
services *ServiceRegistry // c.Service("name") — Service registry
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
api *API // c.API() — Remote streams
info *SysInfo // c.Env("key") — Read-only system/environment information
i18n *I18n // c.I18n() — Internationalisation and locale collection
entitlementChecker EntitlementChecker // default: everything permitted
usageRecorder UsageRecorder // default: nil (no-op)
context context.Context
cancel context.CancelFunc
taskIDCounter atomic.Uint64
@ -45,146 +40,31 @@ type Core struct {
// --- Accessors ---
// Options returns the input configuration passed to core.New().
//
// opts := c.Options()
// name := opts.String("name")
func (c *Core) Options() *Options { return c.options }
// App returns application identity metadata.
//
// c.App().Name // "my-app"
// c.App().Version // "1.0.0"
func (c *Core) App() *App { return c.app }
// Data returns the embedded asset registry (Registry[*Embed]).
//
// r := c.Data().ReadString("prompts/coding.md")
func (c *Core) Data() *Data { return c.data }
// Drive returns the transport handle registry (Registry[*DriveHandle]).
//
// r := c.Drive().Get("forge")
func (c *Core) Drive() *Drive { return c.drive }
// Fs returns the sandboxed filesystem.
//
// r := c.Fs().Read("/path/to/file")
// c.Fs().WriteAtomic("/status.json", data)
func (c *Core) Fs() *Fs { return c.fs }
// Config returns runtime settings and feature flags.
//
// host := c.Config().String("database.host")
// c.Config().Enable("dark-mode")
func (c *Core) Config() *Config { return c.config }
// Error returns the panic recovery subsystem.
//
// c.Error().Recover()
func (c *Core) Error() *ErrorPanic { return c.error }
// Log returns the structured logging subsystem.
//
// c.Log().Info("started", "port", 8080)
func (c *Core) Log() *ErrorLog { return c.log }
// Cli returns the CLI command framework (registered as service "cli").
//
// c.Cli().Run("deploy", "to", "homelab")
func (c *Core) Options() *Options { return c.options }
func (c *Core) App() *App { return c.app }
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.config }
func (c *Core) Error() *ErrorPanic { return c.error }
func (c *Core) Log() *ErrorLog { return c.log }
func (c *Core) Cli() *Cli {
cl, _ := ServiceFor[*Cli](c, "cli")
return cl
}
// IPC returns the message bus internals.
//
// c.IPC()
func (c *Core) IPC() *Ipc { return c.ipc }
// I18n returns the internationalisation subsystem.
//
// tr := c.I18n().Translate("cmd.deploy.description")
func (c *Core) I18n() *I18n { return c.i18n }
// Env returns an environment variable by key (cached at init, falls back to os.Getenv).
//
// home := c.Env("DIR_HOME")
// token := c.Env("FORGE_TOKEN")
func (c *Core) Env(key string) string { return Env(key) }
// Context returns Core's lifecycle context (cancelled on shutdown).
//
// ctx := c.Context()
func (c *Core) IPC() *Ipc { return c.ipc }
func (c *Core) I18n() *I18n { return c.i18n }
func (c *Core) Env(key string) string { return Env(key) }
func (c *Core) Context() context.Context { return c.context }
// Core returns self — satisfies the ServiceRuntime interface.
//
// c := s.Core()
func (c *Core) Core() *Core { return c }
// --- Lifecycle ---
// RunE starts all services, runs the CLI, then shuts down.
// Returns an error instead of calling os.Exit — let main() handle the exit.
// ServiceShutdown is always called via defer, even on startup failure or panic.
//
// if err := c.RunE(); err != nil {
// os.Exit(1)
// }
func (c *Core) RunE() error {
defer c.ServiceShutdown(context.Background())
r := c.ServiceStartup(c.context, nil)
if !r.OK {
if err, ok := r.Value.(error); ok {
return err
}
return E("core.Run", "startup failed", nil)
}
if cli := c.Cli(); cli != nil {
r = cli.Run()
}
if !r.OK {
if err, ok := r.Value.(error); ok {
return err
}
}
return nil
}
// Run starts all services, runs the CLI, then shuts down.
// Calls os.Exit(1) on failure. For error handling use RunE().
//
// c := core.New(core.WithService(myService.Register))
// c.Run()
func (c *Core) Run() {
if err := c.RunE(); err != nil {
Error(err.Error())
os.Exit(1)
}
}
func (c *Core) Core() *Core { return c }
// --- IPC (uppercase aliases) ---
// ACTION broadcasts a message to all registered handlers (fire-and-forget).
// Each handler is wrapped in panic recovery. All handlers fire regardless.
//
// c.ACTION(messages.AgentCompleted{Agent: "codex", Status: "completed"})
func (c *Core) ACTION(msg Message) Result { return c.broadcast(msg) }
// QUERY sends a request — first handler to return OK wins.
//
// r := c.QUERY(MyQuery{Name: "brain"})
func (c *Core) QUERY(q Query) Result { return c.Query(q) }
// QUERYALL sends a request — collects all OK responses.
//
// r := c.QUERYALL(countQuery{})
// results := r.Value.([]any)
func (c *Core) QUERYALL(q Query) Result { return c.QueryAll(q) }
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 ---
@ -203,37 +83,4 @@ func (c *Core) Must(err error, op, msg string) {
c.log.Must(err, op, msg)
}
// --- Registry Accessor ---
// RegistryOf returns a named registry for cross-cutting queries.
// Known registries: "services", "commands", "actions".
//
// c.RegistryOf("services").Names() // all service names
// c.RegistryOf("actions").List("process.*") // process capabilities
// c.RegistryOf("commands").Len() // command count
func (c *Core) RegistryOf(name string) *Registry[any] {
// Bridge typed registries to untyped access for cross-cutting queries.
// Each registry is wrapped in a read-only proxy.
switch name {
case "services":
return registryProxy(c.services.Registry)
case "commands":
return registryProxy(c.commands.Registry)
case "actions":
return registryProxy(c.ipc.actions)
default:
return NewRegistry[any]() // empty registry for unknown names
}
}
// registryProxy creates a read-only any-typed view of a typed registry.
// Copies current state — not a live view (avoids type parameter leaking).
func registryProxy[T any](src *Registry[T]) *Registry[any] {
proxy := NewRegistry[any]()
src.Each(func(name string, item T) {
proxy.Set(name, item)
})
return proxy
}
// --- Global Instance ---

View file

@ -10,26 +10,26 @@ import (
// --- New ---
func TestCore_New_Good(t *testing.T) {
c := New()
func TestNew_Good(t *testing.T) {
c := New().Value.(*Core)
assert.NotNil(t, c)
}
func TestCore_New_WithOptions_Good(t *testing.T) {
c := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"})))
func TestNew_WithOptions_Good(t *testing.T) {
c := New(WithOptions(NewOptions(Option{Key: "name", Value: "myapp"}))).Value.(*Core)
assert.NotNil(t, c)
assert.Equal(t, "myapp", c.App().Name)
}
func TestCore_New_WithOptions_Bad(t *testing.T) {
func TestNew_WithOptions_Bad(t *testing.T) {
// Empty options — should still create a valid Core
c := New(WithOptions(NewOptions()))
c := New(WithOptions(NewOptions())).Value.(*Core)
assert.NotNil(t, c)
}
func TestCore_New_WithService_Good(t *testing.T) {
func TestNew_WithService_Good(t *testing.T) {
started := false
c := New(
r := New(
WithOptions(NewOptions(Option{Key: "name", Value: "myapp"})),
WithService(func(c *Core) Result {
c.Service("test", Service{
@ -38,6 +38,8 @@ func TestCore_New_WithService_Good(t *testing.T) {
return Result{OK: true}
}),
)
assert.True(t, r.OK)
c := r.Value.(*Core)
svc := c.Service("test")
assert.True(t, svc.OK)
@ -46,23 +48,25 @@ func TestCore_New_WithService_Good(t *testing.T) {
assert.True(t, started)
}
func TestCore_New_WithServiceLock_Good(t *testing.T) {
c := New(
func TestNew_WithServiceLock_Good(t *testing.T) {
r := New(
WithService(func(c *Core) Result {
c.Service("allowed", Service{})
return Result{OK: true}
}),
WithServiceLock(),
)
assert.True(t, r.OK)
c := r.Value.(*Core)
// Registration after lock should fail
reg := c.Service("blocked", Service{})
assert.False(t, reg.OK)
}
func TestCore_New_WithService_Bad_FailingOption(t *testing.T) {
func TestNew_WithService_Bad_FailingOption(t *testing.T) {
secondCalled := false
_ = New(
r := New(
WithService(func(c *Core) Result {
return Result{Value: E("test", "intentional failure", nil), OK: false}
}),
@ -71,13 +75,14 @@ func TestCore_New_WithService_Bad_FailingOption(t *testing.T) {
return Result{OK: true}
}),
)
assert.False(t, r.OK)
assert.False(t, secondCalled, "second option should not run after first fails")
}
// --- Accessors ---
func TestCore_Accessors_Good(t *testing.T) {
c := New()
func TestAccessors_Good(t *testing.T) {
c := New().Value.(*Core)
assert.NotNil(t, c.App())
assert.NotNil(t, c.Data())
assert.NotNil(t, c.Drive())
@ -96,7 +101,7 @@ func TestOptions_Accessor_Good(t *testing.T) {
Option{Key: "name", Value: "testapp"},
Option{Key: "port", Value: 8080},
Option{Key: "debug", Value: true},
)))
))).Value.(*Core)
opts := c.Options()
assert.NotNil(t, opts)
assert.Equal(t, "testapp", opts.String("name"))
@ -105,7 +110,7 @@ func TestOptions_Accessor_Good(t *testing.T) {
}
func TestOptions_Accessor_Nil(t *testing.T) {
c := New()
c := New().Value.(*Core)
// No options passed — Options() returns nil
assert.Nil(t, c.Options())
}
@ -113,133 +118,33 @@ func TestOptions_Accessor_Nil(t *testing.T) {
// --- Core Error/Log Helpers ---
func TestCore_LogError_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
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()
c := New().Value.(*Core)
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()
c := New().Value.(*Core)
assert.Panics(t, func() {
c.Must(assert.AnError, "test.Operation", "fatal")
})
}
func TestCore_Must_Nil_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.NotPanics(t, func() {
c.Must(nil, "test.Operation", "no error")
})
}
// --- RegistryOf ---
func TestCore_RegistryOf_Good_Services(t *testing.T) {
c := New(
WithService(func(c *Core) Result {
return c.Service("alpha", Service{})
}),
WithService(func(c *Core) Result {
return c.Service("bravo", Service{})
}),
)
reg := c.RegistryOf("services")
// cli is auto-registered + our 2
assert.True(t, reg.Has("alpha"))
assert.True(t, reg.Has("bravo"))
assert.True(t, reg.Has("cli"))
}
func TestCore_RegistryOf_Good_Commands(t *testing.T) {
c := New()
c.Command("deploy", Command{Action: func(_ Options) Result { return Result{OK: true} }})
c.Command("test", Command{Action: func(_ Options) Result { return Result{OK: true} }})
reg := c.RegistryOf("commands")
assert.True(t, reg.Has("deploy"))
assert.True(t, reg.Has("test"))
}
func TestCore_RegistryOf_Good_Actions(t *testing.T) {
c := New()
c.Action("process.run", func(_ context.Context, _ Options) Result { return Result{OK: true} })
c.Action("brain.recall", func(_ context.Context, _ Options) Result { return Result{OK: true} })
reg := c.RegistryOf("actions")
assert.True(t, reg.Has("process.run"))
assert.True(t, reg.Has("brain.recall"))
assert.Equal(t, 2, reg.Len())
}
func TestCore_RegistryOf_Bad_Unknown(t *testing.T) {
c := New()
reg := c.RegistryOf("nonexistent")
assert.Equal(t, 0, reg.Len(), "unknown registry returns empty")
}
// --- RunE ---
func TestCore_RunE_Good(t *testing.T) {
c := New(
WithService(func(c *Core) Result {
return c.Service("healthy", Service{
OnStart: func() Result { return Result{OK: true} },
OnStop: func() Result { return Result{OK: true} },
})
}),
)
err := c.RunE()
assert.NoError(t, err)
}
func TestCore_RunE_Bad_StartupFailure(t *testing.T) {
c := New(
WithService(func(c *Core) Result {
return c.Service("broken", Service{
OnStart: func() Result {
return Result{Value: NewError("startup failed"), OK: false}
},
})
}),
)
err := c.RunE()
assert.Error(t, err)
assert.Contains(t, err.Error(), "startup failed")
}
func TestCore_RunE_Ugly_StartupFailureCallsShutdown(t *testing.T) {
shutdownCalled := false
c := New(
WithService(func(c *Core) Result {
return c.Service("cleanup", Service{
OnStart: func() Result { return Result{OK: true} },
OnStop: func() Result { shutdownCalled = true; return Result{OK: true} },
})
}),
WithService(func(c *Core) Result {
return c.Service("broken", Service{
OnStart: func() Result {
return Result{Value: NewError("boom"), OK: false}
},
})
}),
)
err := c.RunE()
assert.Error(t, err)
assert.True(t, shutdownCalled, "ServiceShutdown must be called even when startup fails — cleanup service must get OnStop")
}
// Run() delegates to RunE() — tested via RunE tests above.
// os.Exit behaviour is verified by RunE returning error correctly.

50
data.go
View file

@ -25,12 +25,13 @@ package core
import (
"io/fs"
"path/filepath"
"sync"
)
// Data manages mounted embedded filesystems from core packages.
// Embeds Registry[*Embed] for thread-safe named storage.
type Data struct {
*Registry[*Embed]
mounts map[string]*Embed
mu sync.RWMutex
}
// New registers an embedded filesystem under a named prefix.
@ -61,27 +62,54 @@ func (d *Data) New(opts Options) Result {
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.Set(name, emb)
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, ""
}
r := d.Get(parts[0])
if !r.OK {
if d.mounts == nil {
return nil, ""
}
return r.Value.(*Embed), parts[1]
emb := d.mounts[parts[0]]
return emb, parts[1]
}
// ReadFile reads a file by full path.
@ -160,9 +188,15 @@ func (d *Data) Extract(path, targetDir string, templateData any) Result {
return Extract(r.Value.(*Embed).FS(), targetDir, templateData)
}
// Mounts returns the names of all mounted content in registration order.
// Mounts returns the names of all mounted content.
//
// names := c.Data().Mounts()
func (d *Data) Mounts() []string {
return d.Names()
d.mu.RLock()
defer d.mu.RUnlock()
var names []string
for k := range d.mounts {
names = append(names, k)
}
return names
}

View file

@ -2,6 +2,7 @@ package core_test
import (
"embed"
"io"
"testing"
. "dappco.re/go/core"
@ -25,7 +26,7 @@ func mountTestData(t *testing.T, c *Core, name string) {
}
func TestData_New_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Data().New(NewOptions(
Option{Key: "name", Value: "test"},
Option{Key: "source", Value: testFS},
@ -36,7 +37,7 @@ func TestData_New_Good(t *testing.T) {
}
func TestData_New_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Data().New(NewOptions(Option{Key: "source", Value: testFS}))
assert.False(t, r.OK)
@ -49,7 +50,7 @@ func TestData_New_Bad(t *testing.T) {
}
func TestData_ReadString_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
mountTestData(t, c, "app")
r := c.Data().ReadString("app/test.txt")
assert.True(t, r.OK)
@ -57,13 +58,13 @@ func TestData_ReadString_Good(t *testing.T) {
}
func TestData_ReadString_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Data().ReadString("nonexistent/file.txt")
assert.False(t, r.OK)
}
func TestData_ReadFile_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
mountTestData(t, c, "app")
r := c.Data().ReadFile("app/test.txt")
assert.True(t, r.OK)
@ -71,7 +72,7 @@ func TestData_ReadFile_Good(t *testing.T) {
}
func TestData_Get_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
mountTestData(t, c, "brain")
gr := c.Data().Get("brain")
assert.True(t, gr.OK)
@ -79,40 +80,47 @@ func TestData_Get_Good(t *testing.T) {
r := emb.Open("test.txt")
assert.True(t, r.OK)
cr := ReadAll(r.Value)
assert.True(t, cr.OK)
assert.Equal(t, "hello from testdata\n", cr.Value)
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()
c := New().Value.(*Core)
r := c.Data().Get("nonexistent")
assert.False(t, r.OK)
}
func TestData_Mounts_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
mountTestData(t, c, "a")
mountTestData(t, c, "b")
mounts := c.Data().Mounts()
assert.Len(t, mounts, 2)
}
func TestEmbed_Legacy_Good(t *testing.T) {
c := New().Value.(*Core)
mountTestData(t, c, "app")
assert.NotNil(t, c.Embed())
}
func TestData_List_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
mountTestData(t, c, "app")
r := c.Data().List("app/.")
assert.True(t, r.OK)
}
func TestData_List_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Data().List("nonexistent/path")
assert.False(t, r.OK)
}
func TestData_ListNames_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
mountTestData(t, c, "app")
r := c.Data().ListNames("app/.")
assert.True(t, r.OK)
@ -120,14 +128,14 @@ func TestData_ListNames_Good(t *testing.T) {
}
func TestData_Extract_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
mountTestData(t, c, "app")
r := c.Data().Extract("app/.", t.TempDir(), nil)
assert.True(t, r.OK)
}
func TestData_Extract_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Data().Extract("nonexistent/path", t.TempDir(), nil)
assert.False(t, r.OK)
}

File diff suppressed because it is too large Load diff

View file

@ -69,15 +69,20 @@ c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
return core.Result{}
})
c.Action("workspace.create", func(_ context.Context, opts core.Options) core.Result {
name := opts.String("name")
path := "/tmp/agent-workbench/" + name
return core.Result{Value: path, OK: true}
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case createWorkspaceTask:
path := "/tmp/agent-workbench/" + task.Name
return core.Result{Value: path, OK: true}
}
return core.Result{}
})
c.Command("workspace/create", core.Command{
Action: func(opts core.Options) core.Result {
return c.Action("workspace.create").Run(context.Background(), opts)
return c.PERFORM(createWorkspaceTask{
Name: opts.String("name"),
})
},
})
```
@ -165,15 +170,20 @@ func main() {
return core.Result{}
})
c.Action("workspace.create", func(_ context.Context, opts core.Options) core.Result {
name := opts.String("name")
path := c.Config().String("workspace.root") + "/" + name
return core.Result{Value: path, OK: true}
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case createWorkspaceTask:
path := c.Config().String("workspace.root") + "/" + task.Name
return core.Result{Value: path, OK: true}
}
return core.Result{}
})
c.Command("workspace/create", core.Command{
Action: func(opts core.Options) core.Result {
return c.Action("workspace.create").Run(context.Background(), opts)
return c.PERFORM(createWorkspaceTask{
Name: opts.String("name"),
})
},
})

View file

@ -5,56 +5,108 @@ description: AX-first documentation for the CoreGO framework.
# CoreGO
CoreGO is the foundation layer for the Core ecosystem. Module: `dappco.re/go/core`.
CoreGO is the foundation layer for the Core ecosystem. It gives you one container, one command tree, one message bus, and a small set of shared primitives that repeat across the whole framework.
## What CoreGO Provides
The current module path is `dappco.re/go/core`.
| Primitive | Purpose |
|-----------|---------|
| `Core` | Central container — everything registers here |
| `Service` | Lifecycle-managed component (Startable/Stoppable return Result) |
| `Action` | Named callable with panic recovery + entitlement |
| `Task` | Composed sequence of Actions |
| `Registry[T]` | Thread-safe named collection (universal brick) |
| `Command` | Path-based CLI command tree |
| `Process` | Managed execution (Action sugar over go-process) |
| `API` | Remote streams (protocol handlers + Drive) |
| `Entitlement` | Permission gate (default permissive, consumer replaces) |
| `ACTION`, `QUERY` | Anonymous broadcast + request/response |
| `Data`, `Drive`, `Fs`, `Config`, `I18n` | Built-in subsystems |
## AX View
CoreGO already follows the main AX ideas from RFC-025:
- predictable names such as `Core`, `Service`, `Command`, `Options`, `Result`, `Message`
- path-shaped command registration such as `deploy/to/homelab`
- one repeated input shape (`Options`) and one repeated return shape (`Result`)
- comments and examples that show real usage instead of restating the type signature
## What CoreGO Owns
| Surface | Purpose |
|---------|---------|
| `Core` | Central container and access point |
| `Service` | Managed lifecycle component |
| `Command` | Path-based command tree node |
| `ACTION`, `QUERY`, `PERFORM` | Decoupled communication between components |
| `Data`, `Drive`, `Fs`, `Config`, `I18n`, `Cli` | Built-in subsystems for common runtime work |
| `E`, `Wrap`, `ErrorLog`, `ErrorPanic` | Structured failures and panic recovery |
## Quick Example
```go
package main
import "dappco.re/go/core"
import (
"context"
"fmt"
"dappco.re/go/core"
)
type flushCacheTask struct {
Name string
}
func main() {
c := core.New(
core.WithOption("name", "agent-workbench"),
core.WithService(cache.Register),
core.WithServiceLock(),
)
c.Run()
c := core.New(core.Options{
{Key: "name", Value: "agent-workbench"},
})
c.Service("cache", core.Service{
OnStart: func() core.Result {
core.Info("cache ready", "app", c.App().Name)
return core.Result{OK: true}
},
OnStop: func() core.Result {
core.Info("cache stopped", "app", c.App().Name)
return core.Result{OK: true}
},
})
c.RegisterTask(func(_ *core.Core, task core.Task) core.Result {
switch task.(type) {
case flushCacheTask:
return core.Result{Value: "cache flushed", OK: true}
}
return core.Result{}
})
c.Command("cache/flush", core.Command{
Action: func(opts core.Options) core.Result {
return c.PERFORM(flushCacheTask{Name: opts.String("name")})
},
})
if !c.ServiceStartup(context.Background(), nil).OK {
panic("startup failed")
}
r := c.Cli().Run("cache", "flush", "--name=session-store")
fmt.Println(r.Value)
_ = c.ServiceShutdown(context.Background())
}
```
## API Specification
The full contract is `docs/RFC.md` (21 sections, 1476 lines). An agent should be able to write a service from RFC.md alone.
## Documentation
## Documentation Paths
| Path | Covers |
|------|--------|
| [RFC.md](RFC.md) | Authoritative API contract (21 sections) |
| [primitives.md](primitives.md) | Option, Result, Action, Task, Registry, Entitlement |
| [services.md](services.md) | Service registry, ServiceRuntime, service locks |
| [commands.md](commands.md) | Path-based commands, Managed field |
| [messaging.md](messaging.md) | ACTION, QUERY, named Actions, PerformAsync |
| [lifecycle.md](lifecycle.md) | RunE, ServiceStartup, ServiceShutdown |
| [subsystems.md](subsystems.md) | App, Data, Drive, Fs, Config, I18n |
| [errors.md](errors.md) | core.E(), structured errors, panic recovery |
| [testing.md](testing.md) | AX-7 TestFile_Function_{Good,Bad,Ugly} |
| [configuration.md](configuration.md) | WithOption, WithService, WithServiceLock |
| [getting-started.md](getting-started.md) | First runnable CoreGO app |
| [primitives.md](primitives.md) | `Options`, `Result`, `Service`, `Message`, `Query`, `Task` |
| [services.md](services.md) | Service registry, service locks, runtime helpers |
| [commands.md](commands.md) | Path-based commands and CLI execution |
| [messaging.md](messaging.md) | `ACTION`, `QUERY`, `QUERYALL`, `PERFORM`, `PerformAsync` |
| [lifecycle.md](lifecycle.md) | Startup, shutdown, context, background task draining |
| [configuration.md](configuration.md) | Constructor options, config state, feature flags |
| [subsystems.md](subsystems.md) | `App`, `Data`, `Drive`, `Fs`, `I18n`, `Cli` |
| [errors.md](errors.md) | Structured errors, logging helpers, panic recovery |
| [testing.md](testing.md) | Test naming and framework-level testing patterns |
| [pkg/core.md](pkg/core.md) | Package-level reference summary |
| [pkg/log.md](pkg/log.md) | Logging reference for the root package |
| [pkg/PACKAGE_STANDARDS.md](pkg/PACKAGE_STANDARDS.md) | AX package-authoring guidance |
## Good Reading Order
1. Start with [getting-started.md](getting-started.md).
2. Learn the repeated shapes in [primitives.md](primitives.md).
3. Pick the integration path you need next: [services.md](services.md), [commands.md](commands.md), or [messaging.md](messaging.md).
4. Use [subsystems.md](subsystems.md), [errors.md](errors.md), and [testing.md](testing.md) as reference pages while building.

View file

@ -1,127 +1,171 @@
---
title: Messaging
description: ACTION, QUERY, QUERYALL, named Actions, and async dispatch.
description: ACTION, QUERY, QUERYALL, PERFORM, and async task flow.
---
# Messaging
CoreGO has two messaging layers: anonymous broadcast (ACTION/QUERY) and named Actions.
CoreGO uses one message bus for broadcasts, lookups, and work dispatch.
## Anonymous Broadcast
## Message Types
### `ACTION`
```go
type Message any
type Query any
type Task any
```
Fire-and-forget broadcast to all registered handlers. Each handler is wrapped in panic recovery. Handler return values are ignored — all handlers fire regardless.
Your own structs define the protocol.
```go
type repositoryIndexed struct {
Name string
}
type repositoryCountQuery struct{}
type syncRepositoryTask struct {
Name string
}
```
## `ACTION`
`ACTION` is a broadcast.
```go
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if ev, ok := msg.(repositoryIndexed); ok {
core.Info("indexed", "name", ev.Name)
}
return core.Result{OK: true}
switch m := msg.(type) {
case repositoryIndexed:
core.Info("repository indexed", "name", m.Name)
return core.Result{OK: true}
}
return core.Result{OK: true}
})
c.ACTION(repositoryIndexed{Name: "core-go"})
r := c.ACTION(repositoryIndexed{Name: "core-go"})
```
### `QUERY`
### Behavior
First handler to return `OK:true` wins.
- all registered action handlers are called in their current registration order
- if a handler returns `OK:false`, dispatch stops and that `Result` is returned
- if no handler fails, `ACTION` returns `Result{OK:true}`
## `QUERY`
`QUERY` is first-match request-response.
```go
c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result {
if _, ok := q.(repositoryCountQuery); ok {
return core.Result{Value: 42, OK: true}
}
return core.Result{}
switch q.(type) {
case repositoryCountQuery:
return core.Result{Value: 42, OK: true}
}
return core.Result{}
})
r := c.QUERY(repositoryCountQuery{})
```
### `QUERYALL`
### Behavior
Collects every successful non-nil response.
- handlers run until one returns `OK:true`
- the first successful result wins
- if nothing handles the query, CoreGO returns an empty `Result`
## `QUERYALL`
`QUERYALL` collects every successful non-nil response.
```go
r := c.QUERYALL(repositoryCountQuery{})
results := r.Value.([]any)
```
## Named Actions
### Behavior
Named Actions are the typed, inspectable replacement for anonymous dispatch. See Section 18 of `RFC.md`.
- every query handler is called
- only `OK:true` results with non-nil `Value` are collected
- the call itself returns `OK:true` even when the result list is empty
### Register and Invoke
## `PERFORM`
`PERFORM` dispatches a task to the first handler that accepts it.
```go
// Register during OnStartup
c.Action("repo.sync", func(ctx context.Context, opts core.Options) core.Result {
name := opts.String("name")
return core.Result{Value: "synced " + name, OK: true}
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
switch task := t.(type) {
case syncRepositoryTask:
return core.Result{Value: "synced " + task.Name, OK: true}
}
return core.Result{}
})
// Invoke by name
r := c.Action("repo.sync").Run(ctx, core.NewOptions(
core.Option{Key: "name", Value: "core-go"},
))
r := c.PERFORM(syncRepositoryTask{Name: "core-go"})
```
### Capability Check
### Behavior
- handlers run until one returns `OK:true`
- the first successful result wins
- if nothing handles the task, CoreGO returns an empty `Result`
## `PerformAsync`
`PerformAsync` runs a task in a background goroutine and returns a generated task identifier.
```go
if c.Action("process.run").Exists() {
// go-process is registered
}
c.Actions() // []string of all registered action names
```
### Permission Gate
Every `Action.Run()` checks `c.Entitled(action.Name)` before executing. See Section 21 of `RFC.md`.
## Task Composition
A Task is a named sequence of Actions:
```go
c.Task("deploy", core.Task{
Steps: []core.Step{
{Action: "go.build"},
{Action: "go.test"},
{Action: "docker.push"},
{Action: "notify.slack", Async: true},
},
})
r := c.Task("deploy").Run(ctx, c, opts)
```
Sequential steps stop on first failure. `Async: true` steps fire without blocking. `Input: "previous"` pipes output.
## Background Execution
```go
r := c.PerformAsync("repo.sync", opts)
r := c.PerformAsync(syncRepositoryTask{Name: "core-go"})
taskID := r.Value.(string)
c.Progress(taskID, 0.5, "indexing commits", "repo.sync")
```
Broadcasts `ActionTaskStarted`, `ActionTaskProgress`, `ActionTaskCompleted` as ACTION messages.
### Generated Events
### Completion Listener
Async execution emits three action messages:
| Message | When |
|---------|------|
| `ActionTaskStarted` | just before background execution begins |
| `ActionTaskProgress` | whenever `Progress` is called |
| `ActionTaskCompleted` | after the task finishes or panics |
Example listener:
```go
c.RegisterAction(func(_ *core.Core, msg core.Message) core.Result {
if ev, ok := msg.(core.ActionTaskCompleted); ok {
core.Info("done", "task", ev.TaskIdentifier, "ok", ev.Result.OK)
}
return core.Result{OK: true}
switch m := msg.(type) {
case core.ActionTaskCompleted:
core.Info("task completed", "task", m.TaskIdentifier, "err", m.Error)
}
return core.Result{OK: true}
})
```
## Shutdown
## Progress Updates
When shutdown has started, `PerformAsync` returns an empty `Result`. `ServiceShutdown` drains outstanding background work before stopping services.
```go
c.Progress(taskID, 0.5, "indexing commits", syncRepositoryTask{Name: "core-go"})
```
That broadcasts `ActionTaskProgress`.
## `TaskWithIdentifier`
Tasks that implement `TaskWithIdentifier` receive the generated ID before dispatch.
```go
type trackedTask struct {
ID string
Name string
}
func (t *trackedTask) SetTaskIdentifier(id string) { t.ID = id }
func (t *trackedTask) GetTaskIdentifier() string { return t.ID }
```
## Shutdown Interaction
When shutdown has started, `PerformAsync` returns an empty `Result` instead of scheduling more work.
This is why `ServiceShutdown` can safely drain the outstanding background tasks before stopping services.

View file

@ -5,172 +5,165 @@ description: The repeated shapes that make CoreGO easy to navigate.
# Core Primitives
CoreGO is built from a small vocabulary repeated everywhere.
CoreGO is easiest to use when you read it as a small vocabulary repeated everywhere. Most of the framework is built from the same handful of types.
## Primitive Map
| Type | Used For |
|------|----------|
| `Option` / `Options` | Input values and metadata |
| `Options` | Input values and lightweight metadata |
| `Result` | Output values and success state |
| `Service` | Lifecycle-managed components |
| `Action` | Named callable with panic recovery + entitlement |
| `Task` | Composed sequence of Actions |
| `Registry[T]` | Thread-safe named collection |
| `Entitlement` | Permission check result |
| `Message` | Broadcast events (ACTION) |
| `Query` | Request-response lookups (QUERY) |
| `Message` | Broadcast events |
| `Query` | Request-response lookups |
| `Task` | Side-effecting work items |
## `Option` and `Options`
`Option` is one key-value pair. `Options` is an ordered slice of them.
```go
opts := core.NewOptions(
core.Option{Key: "name", Value: "brain"},
core.Option{Key: "path", Value: "prompts"},
core.Option{Key: "debug", Value: true},
)
name := opts.String("name")
debug := opts.Bool("debug")
raw := opts.Get("name") // Result{Value, OK}
opts.Has("path") // true
opts.Len() // 3
opts := core.Options{
{Key: "name", Value: "brain"},
{Key: "path", Value: "prompts"},
{Key: "debug", Value: true},
}
```
Use the helpers to read values:
```go
name := opts.String("name")
path := opts.String("path")
debug := opts.Bool("debug")
hasPath := opts.Has("path")
raw := opts.Get("name")
```
### Important Details
- `Get` returns the first matching key.
- `String`, `Int`, and `Bool` do not convert between types.
- Missing keys return zero values.
- CLI flags with values are stored as strings, so `--port=8080` should be read with `opts.String("port")`, not `opts.Int("port")`.
## `Result`
Universal return shape. Every Core operation returns Result.
`Result` is the universal return shape.
```go
type Result struct {
Value any
OK bool
}
r := core.Result{Value: "ready", OK: true}
r := c.Config().Get("host")
if r.OK {
host := r.Value.(string)
fmt.Println(r.Value)
}
```
The `Result()` method adapts Go `(value, error)` pairs:
It has two jobs:
- carry a value when work succeeds
- carry either an error or an empty state when work does not succeed
### `Result.Result(...)`
The `Result()` method adapts plain Go values and `(value, error)` pairs into a `core.Result`.
```go
r := core.Result{}.Result(file, err)
r1 := core.Result{}.Result("hello")
r2 := core.Result{}.Result(file, err)
```
This is how several built-in helpers bridge standard-library calls.
## `Service`
Managed lifecycle component stored in the `ServiceRegistry`.
`Service` is the managed lifecycle DTO stored in the registry.
```go
core.Service{
OnStart: func() core.Result { return core.Result{OK: true} },
OnStop: func() core.Result { return core.Result{OK: true} },
svc := core.Service{
Name: "cache",
Options: core.Options{
{Key: "backend", Value: "memory"},
},
OnStart: func() core.Result {
return core.Result{OK: true}
},
OnStop: func() core.Result {
return core.Result{OK: true}
},
OnReload: func() core.Result {
return core.Result{OK: true}
},
}
```
Or via `Startable`/`Stoppable` interfaces (preferred for named services):
### Important Details
- `OnStart` and `OnStop` are used by the framework lifecycle.
- `OnReload` is stored on the service DTO, but CoreGO does not currently call it automatically.
- The registry stores `*core.Service`, not arbitrary typed service instances.
## `Message`, `Query`, and `Task`
These are simple aliases to `any`.
```go
type Startable interface { OnStartup(ctx context.Context) Result }
type Stoppable interface { OnShutdown(ctx context.Context) Result }
type Message any
type Query any
type Task any
```
## `Action`
Named callable — the atomic unit of work. Registered by name, invoked by name.
That means your own structs become the protocol:
```go
type ActionHandler func(context.Context, Options) Result
type deployStarted struct {
Environment string
}
type Action struct {
Name string
Handler ActionHandler
Description string
Schema Options
type workspaceCountQuery struct{}
type syncRepositoryTask struct {
Name string
}
```
`Action.Run()` includes panic recovery and entitlement checking.
## `TaskWithIdentifier`
## `Task`
Composed sequence of Actions:
Long-running tasks can opt into task identifiers.
```go
type Task struct {
Name string
Description string
Steps []Step
type indexedTask struct {
ID string
}
type Step struct {
Action string
With Options
Async bool
Input string // "previous" = output of last step
}
func (t *indexedTask) SetTaskIdentifier(id string) { t.ID = id }
func (t *indexedTask) GetTaskIdentifier() string { return t.ID }
```
## `Registry[T]`
Thread-safe named collection with insertion order and 3 lock modes:
```go
r := core.NewRegistry[*MyService]()
r.Set("brain", svc)
r.Get("brain") // Result
r.Has("brain") // bool
r.Names() // []string (insertion order)
r.Each(func(name string, svc *MyService) { ... })
r.Lock() // fully frozen
r.Seal() // no new keys, updates OK
```
## `Entitlement`
Permission check result:
```go
type Entitlement struct {
Allowed bool
Unlimited bool
Limit int
Used int
Remaining int
Reason string
}
e := c.Entitled("social.accounts", 3)
e.NearLimit(0.8) // true if > 80% used
e.UsagePercent() // 75.0
```
## `Message` and `Query`
IPC type aliases for the anonymous broadcast system:
```go
type Message any // broadcast via ACTION
type Query any // request/response via QUERY
```
For typed, named dispatch use `c.Action("name").Run(ctx, opts)`.
If a task implements `TaskWithIdentifier`, `PerformAsync` injects the generated `task-N` identifier before dispatch.
## `ServiceRuntime[T]`
Composition helper for services that need Core access and typed options:
`ServiceRuntime[T]` is the small helper for packages that want to keep a Core reference and a typed options struct together.
```go
type MyService struct {
*core.ServiceRuntime[MyOptions]
type agentServiceOptions struct {
WorkspacePath string
}
runtime := core.NewServiceRuntime(c, MyOptions{BufferSize: 1024})
runtime.Core() // *Core
runtime.Options() // MyOptions
runtime.Config() // shortcut to Core().Config()
type agentService struct {
*core.ServiceRuntime[agentServiceOptions]
}
runtime := core.NewServiceRuntime(c, agentServiceOptions{
WorkspacePath: "/srv/agent-workspaces",
})
```
It exposes:
- `Core()`
- `Options()`
- `Config()`
This helper does not register anything by itself. It is a composition aid for package authors.

View file

@ -78,12 +78,14 @@ assert.Equal(t, "pong", c.QUERY("ping").Value)
```
```go
c.Action("compute", func(_ context.Context, _ core.Options) core.Result {
return core.Result{Value: 42, OK: true}
c.RegisterTask(func(_ *core.Core, t core.Task) core.Result {
if t == "compute" {
return core.Result{Value: 42, OK: true}
}
return core.Result{}
})
r := c.Action("compute").Run(context.Background(), core.NewOptions())
assert.Equal(t, 42, r.Value)
assert.Equal(t, 42, c.PERFORM("compute").Value)
```
## Test Async Work

View file

@ -24,6 +24,10 @@
// api := c.Drive().Get("api")
package core
import (
"sync"
)
// DriveHandle holds a named transport resource.
type DriveHandle struct {
Name string
@ -31,9 +35,10 @@ type DriveHandle struct {
Options Options
}
// Drive manages named transport handles. Embeds Registry[*DriveHandle].
// Drive manages named transport handles.
type Drive struct {
*Registry[*DriveHandle]
handles map[string]*DriveHandle
mu sync.RWMutex
}
// New registers a transport handle.
@ -48,12 +53,58 @@ func (d *Drive) New(opts Options) Result {
return Result{}
}
transport := opts.String("transport")
d.mu.Lock()
defer d.mu.Unlock()
if d.handles == nil {
d.handles = make(map[string]*DriveHandle)
}
handle := &DriveHandle{
Name: name,
Transport: opts.String("transport"),
Transport: transport,
Options: opts,
}
d.Set(name, handle)
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

@ -1,35 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleDrive_New() {
c := New()
c.Drive().New(NewOptions(
Option{Key: "name", Value: "forge"},
Option{Key: "transport", Value: "https://forge.lthn.ai"},
))
Println(c.Drive().Has("forge"))
Println(c.Drive().Names())
// Output:
// true
// [forge]
}
func ExampleDrive_Get() {
c := New()
c.Drive().New(NewOptions(
Option{Key: "name", Value: "charon"},
Option{Key: "transport", Value: "http://10.69.69.165:9101"},
))
r := c.Drive().Get("charon")
if r.OK {
h := r.Value.(*DriveHandle)
Println(h.Transport)
}
// Output: http://10.69.69.165:9101
}

View file

@ -10,7 +10,7 @@ import (
// --- Drive (Transport Handles) ---
func TestDrive_New_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Drive().New(NewOptions(
Option{Key: "name", Value: "api"},
Option{Key: "transport", Value: "https://api.lthn.ai"},
@ -21,7 +21,7 @@ func TestDrive_New_Good(t *testing.T) {
}
func TestDrive_New_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
// Missing name
r := c.Drive().New(NewOptions(
Option{Key: "transport", Value: "https://api.lthn.ai"},
@ -30,7 +30,7 @@ func TestDrive_New_Bad(t *testing.T) {
}
func TestDrive_Get_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Drive().New(NewOptions(
Option{Key: "name", Value: "ssh"},
Option{Key: "transport", Value: "ssh://claude@10.69.69.165"},
@ -42,20 +42,20 @@ func TestDrive_Get_Good(t *testing.T) {
}
func TestDrive_Get_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Drive().Get("nonexistent")
assert.False(t, r.OK)
}
func TestDrive_Has_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Drive().New(NewOptions(Option{Key: "name", Value: "mcp"}, Option{Key: "transport", Value: "mcp://mcp.lthn.sh"}))
assert.True(t, c.Drive().Has("mcp"))
assert.False(t, c.Drive().Has("missing"))
}
func TestDrive_Names_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Drive().New(NewOptions(Option{Key: "name", Value: "api"}, Option{Key: "transport", Value: "https://api.lthn.ai"}))
c.Drive().New(NewOptions(Option{Key: "name", Value: "ssh"}, Option{Key: "transport", Value: "ssh://claude@10.69.69.165"}))
c.Drive().New(NewOptions(Option{Key: "name", Value: "mcp"}, Option{Key: "transport", Value: "mcp://mcp.lthn.sh"}))
@ -67,7 +67,7 @@ func TestDrive_Names_Good(t *testing.T) {
}
func TestDrive_OptionsPreserved_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Drive().New(NewOptions(
Option{Key: "name", Value: "api"},
Option{Key: "transport", Value: "https://api.lthn.ai"},

View file

@ -4,6 +4,7 @@ import (
"bytes"
"compress/gzip"
"encoding/base64"
"os"
"testing"
. "dappco.re/go/core"
@ -20,12 +21,12 @@ func mustMountTestFS(t *testing.T, basedir string) *Embed {
return r.Value.(*Embed)
}
func TestEmbed_Mount_Good(t *testing.T) {
func TestMount_Good(t *testing.T) {
r := Mount(testFS, "testdata")
assert.True(t, r.OK)
}
func TestEmbed_Mount_Bad(t *testing.T) {
func TestMount_Bad(t *testing.T) {
r := Mount(testFS, "nonexistent")
assert.False(t, r.OK)
}
@ -87,45 +88,45 @@ func TestEmbed_EmbedFS_Good(t *testing.T) {
// --- Extract ---
func TestEmbed_Extract_Good(t *testing.T) {
func TestExtract_Good(t *testing.T) {
dir := t.TempDir()
r := Extract(testFS, dir, nil)
assert.True(t, r.OK)
cr := (&Fs{}).New("/").Read(Path(dir, "testdata/test.txt"))
assert.True(t, cr.OK)
assert.Equal(t, "hello from testdata\n", cr.Value)
content, err := os.ReadFile(dir + "/testdata/test.txt")
assert.NoError(t, err)
assert.Equal(t, "hello from testdata\n", string(content))
}
// --- Asset Pack ---
func TestEmbed_AddGetAsset_Good(t *testing.T) {
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 TestEmbed_GetAsset_Bad(t *testing.T) {
func TestGetAsset_Bad(t *testing.T) {
r := GetAsset("missing-group", "missing")
assert.False(t, r.OK)
}
func TestEmbed_GetAssetBytes_Good(t *testing.T) {
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 TestEmbed_MountEmbed_Good(t *testing.T) {
func TestMountEmbed_Good(t *testing.T) {
r := MountEmbed(testFS, "testdata")
assert.True(t, r.OK)
}
// --- ScanAssets ---
func TestEmbed_ScanAssets_Good(t *testing.T) {
func TestScanAssets_Good(t *testing.T) {
r := ScanAssets([]string{"testdata/scantest/sample.go"})
assert.True(t, r.OK)
pkgs := r.Value.([]ScannedPackage)
@ -133,27 +134,27 @@ func TestEmbed_ScanAssets_Good(t *testing.T) {
assert.Equal(t, "scantest", pkgs[0].PackageName)
}
func TestEmbed_ScanAssets_Bad(t *testing.T) {
func TestScanAssets_Bad(t *testing.T) {
r := ScanAssets([]string{"nonexistent.go"})
assert.False(t, r.OK)
}
func TestEmbed_GeneratePack_Empty_Good(t *testing.T) {
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 TestEmbed_GeneratePack_WithFiles_Good(t *testing.T) {
func TestGeneratePack_WithFiles_Good(t *testing.T) {
dir := t.TempDir()
assetDir := Path(dir, "mygroup")
(&Fs{}).New("/").EnsureDir(assetDir)
(&Fs{}).New("/").Write(Path(assetDir, "hello.txt"), "hello world")
assetDir := dir + "/mygroup"
os.MkdirAll(assetDir, 0755)
os.WriteFile(assetDir+"/hello.txt", []byte("hello world"), 0644)
source := "package test\nimport \"dappco.re/go/core\"\nfunc example() {\n\t_, _ = core.GetAsset(\"mygroup\", \"hello.txt\")\n}\n"
goFile := Path(dir, "test.go")
(&Fs{}).New("/").Write(goFile, source)
goFile := dir + "/test.go"
os.WriteFile(goFile, []byte(source), 0644)
sr := ScanAssets([]string{goFile})
assert.True(t, sr.OK)
@ -166,48 +167,46 @@ func TestEmbed_GeneratePack_WithFiles_Good(t *testing.T) {
// --- Extract (template + nested) ---
func TestEmbed_Extract_WithTemplate_Good(t *testing.T) {
func TestExtract_WithTemplate_Good(t *testing.T) {
dir := t.TempDir()
// Create an in-memory FS with a template file and a plain file
tmplDir := DirFS(t.TempDir())
tmplDir := os.DirFS(t.TempDir())
// Use a real temp dir with files
srcDir := t.TempDir()
(&Fs{}).New("/").Write(Path(srcDir, "plain.txt"), "static content")
(&Fs{}).New("/").Write(Path(srcDir, "greeting.tmpl"), "Hello {{.Name}}!")
(&Fs{}).New("/").EnsureDir(Path(srcDir, "sub"))
(&Fs{}).New("/").Write(Path(srcDir, "sub/nested.txt"), "nested")
os.WriteFile(srcDir+"/plain.txt", []byte("static content"), 0644)
os.WriteFile(srcDir+"/greeting.tmpl", []byte("Hello {{.Name}}!"), 0644)
os.MkdirAll(srcDir+"/sub", 0755)
os.WriteFile(srcDir+"/sub/nested.txt", []byte("nested"), 0644)
_ = tmplDir
fsys := DirFS(srcDir)
fsys := os.DirFS(srcDir)
data := map[string]string{"Name": "World"}
r := Extract(fsys, dir, data)
assert.True(t, r.OK)
f := (&Fs{}).New("/")
// Plain file copied
cr := f.Read(Path(dir, "plain.txt"))
assert.True(t, cr.OK)
assert.Equal(t, "static content", cr.Value)
content, err := os.ReadFile(dir + "/plain.txt")
assert.NoError(t, err)
assert.Equal(t, "static content", string(content))
// Template processed and .tmpl stripped
gr := f.Read(Path(dir, "greeting"))
assert.True(t, gr.OK)
assert.Equal(t, "Hello World!", gr.Value)
greeting, err := os.ReadFile(dir + "/greeting")
assert.NoError(t, err)
assert.Equal(t, "Hello World!", string(greeting))
// Nested directory preserved
nr := f.Read(Path(dir, "sub/nested.txt"))
assert.True(t, nr.OK)
assert.Equal(t, "nested", nr.Value)
nested, err := os.ReadFile(dir + "/sub/nested.txt")
assert.NoError(t, err)
assert.Equal(t, "nested", string(nested))
}
func TestEmbed_Extract_BadTargetDir_Ugly(t *testing.T) {
func TestExtract_BadTargetDir_Ugly(t *testing.T) {
srcDir := t.TempDir()
(&Fs{}).New("/").Write(Path(srcDir, "f.txt"), "x")
r := Extract(DirFS(srcDir), "/nonexistent/deeply/nested/impossible", nil)
os.WriteFile(srcDir+"/f.txt", []byte("x"), 0644)
r := Extract(os.DirFS(srcDir), "/nonexistent/deeply/nested/impossible", nil)
// Should fail gracefully, not panic
_ = r
}
@ -245,12 +244,12 @@ func TestEmbed_EmbedFS_Original_Good(t *testing.T) {
assert.NoError(t, err)
}
func TestEmbed_Extract_NilData_Good(t *testing.T) {
func TestExtract_NilData_Good(t *testing.T) {
dir := t.TempDir()
srcDir := t.TempDir()
(&Fs{}).New("/").Write(Path(srcDir, "file.txt"), "no template")
os.WriteFile(srcDir+"/file.txt", []byte("no template"), 0644)
r := Extract(DirFS(srcDir), dir, nil)
r := Extract(os.DirFS(srcDir), dir, nil)
assert.True(t, r.OK)
}

View file

@ -1,130 +0,0 @@
// SPDX-License-Identifier: EUPL-1.2
// Permission primitive for the Core framework.
// Entitlement answers "can [subject] do [action] with [quantity]?"
// Default: everything permitted (trusted conclave).
// With go-entitlements: checks workspace packages, features, usage, boosts.
// With commerce-matrix: checks entity hierarchy, lock cascade.
//
// Usage:
//
// e := c.Entitled("process.run") // boolean gate
// e := c.Entitled("social.accounts", 3) // quantity check
// if e.Allowed { proceed() }
// if e.NearLimit(0.8) { showUpgradePrompt() }
//
// Registration:
//
// c.SetEntitlementChecker(myChecker)
// c.SetUsageRecorder(myRecorder)
package core
import "context"
// Entitlement is the result of a permission check.
// Carries context for both boolean gates (Allowed) and usage limits (Limit/Used/Remaining).
//
// e := c.Entitled("social.accounts", 3)
// e.Allowed // true
// e.Limit // 5
// e.Used // 2
// e.Remaining // 3
// e.NearLimit(0.8) // false
type Entitlement struct {
Allowed bool // permission granted
Unlimited bool // no cap (agency tier, admin, trusted conclave)
Limit int // total allowed (0 = boolean gate)
Used int // current consumption
Remaining int // Limit - Used
Reason string // denial reason — for UI and audit logging
}
// NearLimit returns true if usage exceeds the threshold percentage.
//
// if e.NearLimit(0.8) { showUpgradePrompt() }
func (e Entitlement) NearLimit(threshold float64) bool {
if e.Unlimited || e.Limit == 0 {
return false
}
return float64(e.Used)/float64(e.Limit) >= threshold
}
// UsagePercent returns current usage as a percentage of the limit.
//
// pct := e.UsagePercent() // 75.0
func (e Entitlement) UsagePercent() float64 {
if e.Limit == 0 {
return 0
}
return float64(e.Used) / float64(e.Limit) * 100
}
// EntitlementChecker answers "can [subject] do [action] with [quantity]?"
// Subject comes from context (workspace, entity, user — consumer's concern).
type EntitlementChecker func(action string, quantity int, ctx context.Context) Entitlement
// UsageRecorder records consumption after a gated action succeeds.
// Consumer packages provide the implementation (database, cache, etc).
type UsageRecorder func(action string, quantity int, ctx context.Context)
// defaultChecker — trusted conclave, everything permitted.
func defaultChecker(_ string, _ int, _ context.Context) Entitlement {
return Entitlement{Allowed: true, Unlimited: true}
}
// Entitled checks if an action is permitted in the current context.
// Default: always returns Allowed=true, Unlimited=true.
// Denials are logged via core.Security().
//
// e := c.Entitled("process.run")
// e := c.Entitled("social.accounts", 3)
func (c *Core) Entitled(action string, quantity ...int) Entitlement {
qty := 1
if len(quantity) > 0 {
qty = quantity[0]
}
e := c.entitlementChecker(action, qty, c.Context())
if !e.Allowed {
Security("entitlement.denied", "action", action, "quantity", qty, "reason", e.Reason)
}
return e
}
// SetEntitlementChecker replaces the default (permissive) checker.
// Called by go-entitlements or commerce-matrix during OnStartup.
//
// func (s *EntitlementService) OnStartup(ctx context.Context) core.Result {
// s.Core().SetEntitlementChecker(s.check)
// return core.Result{OK: true}
// }
func (c *Core) SetEntitlementChecker(checker EntitlementChecker) {
c.entitlementChecker = checker
}
// RecordUsage records consumption after a gated action succeeds.
// Delegates to the registered UsageRecorder. No-op if none registered.
//
// e := c.Entitled("ai.credits", 10)
// if e.Allowed {
// doWork()
// c.RecordUsage("ai.credits", 10)
// }
func (c *Core) RecordUsage(action string, quantity ...int) {
if c.usageRecorder == nil {
return
}
qty := 1
if len(quantity) > 0 {
qty = quantity[0]
}
c.usageRecorder(action, qty, c.Context())
}
// SetUsageRecorder registers a usage tracking function.
// Called by go-entitlements during OnStartup.
func (c *Core) SetUsageRecorder(recorder UsageRecorder) {
c.usageRecorder = recorder
}

View file

@ -1,52 +0,0 @@
package core_test
import (
"context"
. "dappco.re/go/core"
)
func ExampleEntitlement_UsagePercent() {
e := Entitlement{Limit: 100, Used: 75}
Println(e.UsagePercent())
// Output: 75
}
func ExampleCore_SetEntitlementChecker() {
c := New()
c.SetEntitlementChecker(func(action string, qty int, _ context.Context) Entitlement {
limits := map[string]int{"social.accounts": 5, "ai.credits": 100}
usage := map[string]int{"social.accounts": 3, "ai.credits": 95}
limit, ok := limits[action]
if !ok {
return Entitlement{Allowed: false, Reason: "not in package"}
}
used := usage[action]
remaining := limit - used
if qty > remaining {
return Entitlement{Allowed: false, Limit: limit, Used: used, Remaining: remaining, Reason: "limit exceeded"}
}
return Entitlement{Allowed: true, Limit: limit, Used: used, Remaining: remaining}
})
Println(c.Entitled("social.accounts", 2).Allowed)
Println(c.Entitled("social.accounts", 5).Allowed)
Println(c.Entitled("ai.credits").NearLimit(0.9))
// Output:
// true
// false
// true
}
func ExampleCore_RecordUsage() {
c := New()
var recorded string
c.SetUsageRecorder(func(action string, qty int, _ context.Context) {
recorded = Concat(action, ":", Sprint(qty))
})
c.RecordUsage("ai.credits", 10)
Println(recorded)
// Output: ai.credits:10
}

View file

@ -1,235 +0,0 @@
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Entitled ---
func TestEntitlement_Entitled_Good_DefaultPermissive(t *testing.T) {
c := New()
e := c.Entitled("anything")
assert.True(t, e.Allowed, "default checker permits everything")
assert.True(t, e.Unlimited)
}
func TestEntitlement_Entitled_Good_BooleanGate(t *testing.T) {
c := New()
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
if action == "premium.feature" {
return Entitlement{Allowed: true}
}
return Entitlement{Allowed: false, Reason: "not in package"}
})
assert.True(t, c.Entitled("premium.feature").Allowed)
assert.False(t, c.Entitled("other.feature").Allowed)
assert.Equal(t, "not in package", c.Entitled("other.feature").Reason)
}
func TestEntitlement_Entitled_Good_QuantityCheck(t *testing.T) {
c := New()
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
if action == "social.accounts" {
limit := 5
used := 3
remaining := limit - used
if qty > remaining {
return Entitlement{Allowed: false, Limit: limit, Used: used, Remaining: remaining, Reason: "limit exceeded"}
}
return Entitlement{Allowed: true, Limit: limit, Used: used, Remaining: remaining}
}
return Entitlement{Allowed: true, Unlimited: true}
})
// Can create 2 more (3 used of 5)
e := c.Entitled("social.accounts", 2)
assert.True(t, e.Allowed)
assert.Equal(t, 5, e.Limit)
assert.Equal(t, 3, e.Used)
assert.Equal(t, 2, e.Remaining)
// Can't create 3 more
e = c.Entitled("social.accounts", 3)
assert.False(t, e.Allowed)
assert.Equal(t, "limit exceeded", e.Reason)
}
func TestEntitlement_Entitled_Bad_Denied(t *testing.T) {
c := New()
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
return Entitlement{Allowed: false, Reason: "locked by M1"}
})
e := c.Entitled("product.create")
assert.False(t, e.Allowed)
assert.Equal(t, "locked by M1", e.Reason)
}
func TestEntitlement_Entitled_Ugly_DefaultQuantityIsOne(t *testing.T) {
c := New()
var receivedQty int
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
receivedQty = qty
return Entitlement{Allowed: true}
})
c.Entitled("test")
assert.Equal(t, 1, receivedQty, "default quantity should be 1")
}
// --- Action.Run Entitlement Enforcement ---
func TestEntitlement_ActionRun_Good_Permitted(t *testing.T) {
c := New()
c.Action("work", func(_ context.Context, _ Options) Result {
return Result{Value: "done", OK: true}
})
r := c.Action("work").Run(context.Background(), NewOptions())
assert.True(t, r.OK)
assert.Equal(t, "done", r.Value)
}
func TestEntitlement_ActionRun_Bad_Denied(t *testing.T) {
c := New()
c.Action("restricted", func(_ context.Context, _ Options) Result {
return Result{Value: "should not reach", OK: true}
})
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
if action == "restricted" {
return Entitlement{Allowed: false, Reason: "tier too low"}
}
return Entitlement{Allowed: true, Unlimited: true}
})
r := c.Action("restricted").Run(context.Background(), NewOptions())
assert.False(t, r.OK, "denied action must not execute")
err, ok := r.Value.(error)
assert.True(t, ok)
assert.Contains(t, err.Error(), "not entitled")
assert.Contains(t, err.Error(), "tier too low")
}
func TestEntitlement_ActionRun_Good_OtherActionsStillWork(t *testing.T) {
c := New()
c.Action("allowed", func(_ context.Context, _ Options) Result {
return Result{Value: "ok", OK: true}
})
c.Action("blocked", func(_ context.Context, _ Options) Result {
return Result{Value: "nope", OK: true}
})
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
if action == "blocked" {
return Entitlement{Allowed: false, Reason: "nope"}
}
return Entitlement{Allowed: true, Unlimited: true}
})
assert.True(t, c.Action("allowed").Run(context.Background(), NewOptions()).OK)
assert.False(t, c.Action("blocked").Run(context.Background(), NewOptions()).OK)
}
// --- NearLimit ---
func TestEntitlement_NearLimit_Good(t *testing.T) {
e := Entitlement{Allowed: true, Limit: 100, Used: 85, Remaining: 15}
assert.True(t, e.NearLimit(0.8))
assert.False(t, e.NearLimit(0.9))
}
func TestEntitlement_NearLimit_Bad_Unlimited(t *testing.T) {
e := Entitlement{Allowed: true, Unlimited: true}
assert.False(t, e.NearLimit(0.8), "unlimited should never be near limit")
}
func TestEntitlement_NearLimit_Ugly_ZeroLimit(t *testing.T) {
e := Entitlement{Allowed: true, Limit: 0}
assert.False(t, e.NearLimit(0.8), "boolean gate (limit=0) should not report near limit")
}
// --- UsagePercent ---
func TestEntitlement_UsagePercent_Good(t *testing.T) {
e := Entitlement{Limit: 100, Used: 75}
assert.Equal(t, 75.0, e.UsagePercent())
}
func TestEntitlement_UsagePercent_Ugly_ZeroLimit(t *testing.T) {
e := Entitlement{Limit: 0, Used: 5}
assert.Equal(t, 0.0, e.UsagePercent(), "zero limit = boolean gate, no percentage")
}
// --- RecordUsage ---
func TestEntitlement_RecordUsage_Good(t *testing.T) {
c := New()
var recorded string
var recordedQty int
c.SetUsageRecorder(func(action string, qty int, ctx context.Context) {
recorded = action
recordedQty = qty
})
c.RecordUsage("ai.credits", 10)
assert.Equal(t, "ai.credits", recorded)
assert.Equal(t, 10, recordedQty)
}
func TestEntitlement_RecordUsage_Good_NoRecorder(t *testing.T) {
c := New()
// No recorder set — should not panic
assert.NotPanics(t, func() {
c.RecordUsage("anything", 5)
})
}
// --- Permission Model Integration ---
func TestEntitlement_Ugly_SaaSGatingPattern(t *testing.T) {
c := New()
// Simulate RFC-004 entitlement service
packages := map[string]int{
"social.accounts": 5,
"social.posts.scheduled": 100,
"ai.credits": 50,
}
usage := map[string]int{
"social.accounts": 3,
"social.posts.scheduled": 45,
"ai.credits": 48,
}
c.SetEntitlementChecker(func(action string, qty int, ctx context.Context) Entitlement {
limit, hasFeature := packages[action]
if !hasFeature {
return Entitlement{Allowed: false, Reason: "feature not in package"}
}
used := usage[action]
remaining := limit - used
if qty > remaining {
return Entitlement{Allowed: false, Limit: limit, Used: used, Remaining: remaining, Reason: "limit exceeded"}
}
return Entitlement{Allowed: true, Limit: limit, Used: used, Remaining: remaining}
})
// Can create 2 social accounts
e := c.Entitled("social.accounts", 2)
assert.True(t, e.Allowed)
// AI credits near limit
e = c.Entitled("ai.credits", 1)
assert.True(t, e.Allowed)
assert.True(t, e.NearLimit(0.8))
assert.Equal(t, 96.0, e.UsagePercent())
// Feature not in package
e = c.Entitled("premium.feature")
assert.False(t, e.Allowed)
}

View file

@ -1,33 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleE() {
err := E("cache.Get", "key not found", nil)
Println(Operation(err))
Println(ErrorMessage(err))
// Output:
// cache.Get
// key not found
}
func ExampleWrap() {
cause := NewError("connection refused")
err := Wrap(cause, "database.Connect", "failed to reach host")
Println(Operation(err))
Println(Is(err, cause))
// Output:
// database.Connect
// true
}
func ExampleRoot() {
cause := NewError("original")
wrapped := Wrap(cause, "op1", "first wrap")
double := Wrap(wrapped, "op2", "second wrap")
Println(Root(double))
// Output: original
}

View file

@ -1,6 +1,7 @@
package core_test
import (
"errors"
"testing"
. "dappco.re/go/core"
@ -9,39 +10,39 @@ import (
// --- Error Creation ---
func TestError_E_Good(t *testing.T) {
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 TestError_E_WithCause_Good(t *testing.T) {
cause := NewError("connection refused")
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 TestError_Wrap_Good(t *testing.T) {
cause := NewError("timeout")
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 TestError_Wrap_Nil_Good(t *testing.T) {
func TestWrap_Nil_Good(t *testing.T) {
err := Wrap(nil, "api.Call", "request failed")
assert.Nil(t, err)
}
func TestError_WrapCode_Good(t *testing.T) {
cause := NewError("invalid email")
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 TestError_NewCode_Good(t *testing.T) {
func TestNewCode_Good(t *testing.T) {
err := NewCode("NOT_FOUND", "resource not found")
assert.Error(t, err)
assert.Equal(t, "NOT_FOUND", ErrorCode(err))
@ -49,42 +50,42 @@ func TestError_NewCode_Good(t *testing.T) {
// --- Error Introspection ---
func TestError_Operation_Good(t *testing.T) {
func TestOperation_Good(t *testing.T) {
err := E("brain.Recall", "search failed", nil)
assert.Equal(t, "brain.Recall", Operation(err))
}
func TestError_Operation_Bad(t *testing.T) {
err := NewError("plain error")
func TestOperation_Bad(t *testing.T) {
err := errors.New("plain error")
assert.Equal(t, "", Operation(err))
}
func TestError_ErrorMessage_Good(t *testing.T) {
func TestErrorMessage_Good(t *testing.T) {
err := E("op", "the message", nil)
assert.Equal(t, "the message", ErrorMessage(err))
}
func TestError_ErrorMessage_Plain(t *testing.T) {
err := NewError("plain")
func TestErrorMessage_Plain(t *testing.T) {
err := errors.New("plain")
assert.Equal(t, "plain", ErrorMessage(err))
}
func TestError_ErrorMessage_Nil(t *testing.T) {
func TestErrorMessage_Nil(t *testing.T) {
assert.Equal(t, "", ErrorMessage(nil))
}
func TestError_Root_Good(t *testing.T) {
root := NewError("root cause")
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 TestError_Root_Nil(t *testing.T) {
func TestRoot_Nil(t *testing.T) {
assert.Nil(t, Root(nil))
}
func TestError_StackTrace_Good(t *testing.T) {
func TestStackTrace_Good(t *testing.T) {
err := Wrap(E("inner", "cause", nil), "outer", "wrapper")
stack := StackTrace(err)
assert.Len(t, stack, 2)
@ -92,7 +93,7 @@ func TestError_StackTrace_Good(t *testing.T) {
assert.Equal(t, "inner", stack[1])
}
func TestError_FormatStackTrace_Good(t *testing.T) {
func TestFormatStackTrace_Good(t *testing.T) {
err := Wrap(E("a", "x", nil), "b", "y")
formatted := FormatStackTrace(err)
assert.Equal(t, "b -> a", formatted)
@ -100,36 +101,36 @@ func TestError_FormatStackTrace_Good(t *testing.T) {
// --- ErrorLog ---
func TestError_ErrorLog_Good(t *testing.T) {
c := New()
cause := NewError("boom")
func TestErrorLog_Good(t *testing.T) {
c := New().Value.(*Core)
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 TestError_ErrorLog_Nil_Good(t *testing.T) {
c := New()
func TestErrorLog_Nil_Good(t *testing.T) {
c := New().Value.(*Core)
r := c.Log().Error(nil, "test.Operation", "no error")
assert.True(t, r.OK)
}
func TestError_ErrorLog_Warn_Good(t *testing.T) {
c := New()
cause := NewError("warning")
func TestErrorLog_Warn_Good(t *testing.T) {
c := New().Value.(*Core)
cause := errors.New("warning")
r := c.Log().Warn(cause, "test.Operation", "heads up")
assert.False(t, r.OK)
}
func TestError_ErrorLog_Must_Ugly(t *testing.T) {
c := New()
func TestErrorLog_Must_Ugly(t *testing.T) {
c := New().Value.(*Core)
assert.Panics(t, func() {
c.Log().Must(NewError("fatal"), "test.Operation", "must fail")
c.Log().Must(errors.New("fatal"), "test.Operation", "must fail")
})
}
func TestError_ErrorLog_Must_Nil_Good(t *testing.T) {
c := New()
func TestErrorLog_Must_Nil_Good(t *testing.T) {
c := New().Value.(*Core)
assert.NotPanics(t, func() {
c.Log().Must(nil, "test.Operation", "no error")
})
@ -137,8 +138,8 @@ func TestError_ErrorLog_Must_Nil_Good(t *testing.T) {
// --- ErrorPanic ---
func TestError_ErrorPanic_Recover_Good(t *testing.T) {
c := New()
func TestErrorPanic_Recover_Good(t *testing.T) {
c := New().Value.(*Core)
// Should not panic — Recover catches it
assert.NotPanics(t, func() {
defer c.Error().Recover()
@ -146,8 +147,8 @@ func TestError_ErrorPanic_Recover_Good(t *testing.T) {
})
}
func TestError_ErrorPanic_SafeGo_Good(t *testing.T) {
c := New()
func TestErrorPanic_SafeGo_Good(t *testing.T) {
c := New().Value.(*Core)
done := make(chan bool, 1)
c.Error().SafeGo(func() {
done <- true
@ -155,8 +156,8 @@ func TestError_ErrorPanic_SafeGo_Good(t *testing.T) {
assert.True(t, <-done)
}
func TestError_ErrorPanic_SafeGo_Panic_Good(t *testing.T) {
c := New()
func TestErrorPanic_SafeGo_Panic_Good(t *testing.T) {
c := New().Value.(*Core)
done := make(chan bool, 1)
c.Error().SafeGo(func() {
defer func() { done <- true }()
@ -168,27 +169,27 @@ func TestError_ErrorPanic_SafeGo_Panic_Good(t *testing.T) {
// --- Standard Library Wrappers ---
func TestError_Is_Good(t *testing.T) {
target := NewError("target")
func TestIs_Good(t *testing.T) {
target := errors.New("target")
wrapped := Wrap(target, "op", "msg")
assert.True(t, Is(wrapped, target))
}
func TestError_As_Good(t *testing.T) {
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 TestError_NewError_Good(t *testing.T) {
func TestNewError_Good(t *testing.T) {
err := NewError("simple error")
assert.Equal(t, "simple error", err.Error())
}
func TestError_ErrorJoin_Good(t *testing.T) {
e1 := NewError("first")
e2 := NewError("second")
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)
@ -196,12 +197,12 @@ func TestError_ErrorJoin_Good(t *testing.T) {
// --- ErrorPanic Crash Reports ---
func TestError_ErrorPanic_Reports_Good(t *testing.T) {
func TestErrorPanic_Reports_Good(t *testing.T) {
dir := t.TempDir()
path := Path(dir, "crashes.json")
path := dir + "/crashes.json"
// Create ErrorPanic with file output
c := New()
c := New().Value.(*Core)
// Access internals via a crash that writes to file
// Since ErrorPanic fields are unexported, we test via Recover
_ = c
@ -211,16 +212,16 @@ func TestError_ErrorPanic_Reports_Good(t *testing.T) {
// --- ErrorPanic Crash File ---
func TestError_ErrorPanic_CrashFile_Good(t *testing.T) {
func TestErrorPanic_CrashFile_Good(t *testing.T) {
dir := t.TempDir()
path := Path(dir, "crashes.json")
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()
c := New().Value.(*Core)
r := c.Error().Reports(5)
assert.False(t, r.OK)
assert.Nil(t, r.Value)
@ -229,43 +230,43 @@ func TestError_ErrorPanic_CrashFile_Good(t *testing.T) {
// --- Error formatting branches ---
func TestError_Err_Error_WithCode_Good(t *testing.T) {
err := WrapCode(NewError("bad"), "INVALID", "validate", "input failed")
func TestErr_Error_WithCode_Good(t *testing.T) {
err := WrapCode(errors.New("bad"), "INVALID", "validate", "input failed")
assert.Contains(t, err.Error(), "[INVALID]")
assert.Contains(t, err.Error(), "validate")
assert.Contains(t, err.Error(), "bad")
}
func TestError_Err_Error_CodeNoCause_Good(t *testing.T) {
func TestErr_Error_CodeNoCause_Good(t *testing.T) {
err := NewCode("NOT_FOUND", "resource missing")
assert.Contains(t, err.Error(), "[NOT_FOUND]")
assert.Contains(t, err.Error(), "resource missing")
}
func TestError_Err_Error_NoOp_Good(t *testing.T) {
func TestErr_Error_NoOp_Good(t *testing.T) {
err := &Err{Message: "bare error"}
assert.Equal(t, "bare error", err.Error())
}
func TestError_WrapCode_NilErr_EmptyCode_Good(t *testing.T) {
func TestWrapCode_NilErr_EmptyCode_Good(t *testing.T) {
err := WrapCode(nil, "", "op", "msg")
assert.Nil(t, err)
}
func TestError_Wrap_PreservesCode_Good(t *testing.T) {
inner := WrapCode(NewError("root"), "AUTH_FAIL", "auth", "denied")
func TestWrap_PreservesCode_Good(t *testing.T) {
inner := WrapCode(errors.New("root"), "AUTH_FAIL", "auth", "denied")
outer := Wrap(inner, "handler", "request failed")
assert.Equal(t, "AUTH_FAIL", ErrorCode(outer))
}
func TestError_ErrorLog_Warn_Nil_Good(t *testing.T) {
c := New()
func TestErrorLog_Warn_Nil_Good(t *testing.T) {
c := New().Value.(*Core)
r := c.LogWarn(nil, "op", "msg")
assert.True(t, r.OK)
}
func TestError_ErrorLog_Error_Nil_Good(t *testing.T) {
c := New()
func TestErrorLog_Error_Nil_Good(t *testing.T) {
c := New().Value.(*Core)
r := c.LogError(nil, "op", "msg")
assert.True(t, r.OK)
}

View file

@ -1,314 +0,0 @@
package core_test
import (
"context"
. "dappco.re/go/core"
)
// --- Core Creation ---
func ExampleNew() {
c := New(
WithOption("name", "my-app"),
WithServiceLock(),
)
Println(c.App().Name)
// Output: my-app
}
func ExampleNew_withService() {
c := New(
WithOption("name", "example"),
WithService(func(c *Core) Result {
return c.Service("greeter", Service{
OnStart: func() Result {
Info("greeter started", "app", c.App().Name)
return Result{OK: true}
},
})
}),
)
c.ServiceStartup(context.Background(), nil)
Println(c.Services())
c.ServiceShutdown(context.Background())
// Output is non-deterministic (map order), so no Output comment
}
// --- Options ---
func ExampleNewOptions() {
opts := NewOptions(
Option{Key: "name", Value: "brain"},
Option{Key: "port", Value: 8080},
Option{Key: "debug", Value: true},
)
Println(opts.String("name"))
Println(opts.Int("port"))
Println(opts.Bool("debug"))
// Output:
// brain
// 8080
// true
}
// --- Result ---
func ExampleResult() {
r := Result{Value: "hello", OK: true}
if r.OK {
Println(r.Value)
}
// Output: hello
}
// --- Action ---
func ExampleCore_Action_register() {
c := New()
c.Action("greet", func(_ context.Context, opts Options) Result {
name := opts.String("name")
return Result{Value: Concat("hello ", name), OK: true}
})
Println(c.Action("greet").Exists())
// Output: true
}
func ExampleCore_Action_invoke() {
c := New()
c.Action("add", func(_ context.Context, opts Options) Result {
a := opts.Int("a")
b := opts.Int("b")
return Result{Value: a + b, OK: true}
})
r := c.Action("add").Run(context.Background(), NewOptions(
Option{Key: "a", Value: 3},
Option{Key: "b", Value: 4},
))
Println(r.Value)
// Output: 7
}
func ExampleCore_Actions() {
c := New()
c.Action("process.run", func(_ context.Context, _ Options) Result { return Result{OK: true} })
c.Action("brain.recall", func(_ context.Context, _ Options) Result { return Result{OK: true} })
Println(c.Actions())
// Output: [process.run brain.recall]
}
// --- Task ---
func ExampleCore_Task() {
c := New()
order := ""
c.Action("step.a", func(_ context.Context, _ Options) Result {
order += "a"
return Result{Value: "from-a", OK: true}
})
c.Action("step.b", func(_ context.Context, opts Options) Result {
order += "b"
return Result{OK: true}
})
c.Task("pipeline", Task{
Steps: []Step{
{Action: "step.a"},
{Action: "step.b", Input: "previous"},
},
})
c.Task("pipeline").Run(context.Background(), c, NewOptions())
Println(order)
// Output: ab
}
// --- Registry ---
func ExampleNewRegistry() {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Set("bravo", "second")
Println(r.Has("alpha"))
Println(r.Names())
Println(r.Len())
// Output:
// true
// [alpha bravo]
// 2
}
func ExampleRegistry_Lock() {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Lock()
result := r.Set("beta", "second")
Println(result.OK)
// Output: false
}
func ExampleRegistry_Seal() {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Seal()
// Can update existing
Println(r.Set("alpha", "updated").OK)
// Can't add new
Println(r.Set("beta", "new").OK)
// Output:
// true
// false
}
// --- Entitlement ---
func ExampleCore_Entitled_default() {
c := New()
e := c.Entitled("anything")
Println(e.Allowed)
Println(e.Unlimited)
// Output:
// true
// true
}
func ExampleCore_Entitled_custom() {
c := New()
c.SetEntitlementChecker(func(action string, qty int, _ context.Context) Entitlement {
if action == "premium" {
return Entitlement{Allowed: false, Reason: "upgrade required"}
}
return Entitlement{Allowed: true, Unlimited: true}
})
Println(c.Entitled("basic").Allowed)
Println(c.Entitled("premium").Allowed)
Println(c.Entitled("premium").Reason)
// Output:
// true
// false
// upgrade required
}
func ExampleEntitlement_NearLimit() {
e := Entitlement{Allowed: true, Limit: 100, Used: 85, Remaining: 15}
Println(e.NearLimit(0.8))
Println(e.UsagePercent())
// Output:
// true
// 85
}
// --- Process ---
func ExampleCore_Process() {
c := New()
// No go-process registered — permission by registration
Println(c.Process().Exists())
// Register a mock process handler
c.Action("process.run", func(_ context.Context, opts Options) Result {
return Result{Value: Concat("output of ", opts.String("command")), OK: true}
})
Println(c.Process().Exists())
r := c.Process().Run(context.Background(), "echo", "hello")
Println(r.Value)
// Output:
// false
// true
// output of echo
}
// --- JSON ---
func ExampleJSONMarshal() {
type config struct {
Host string `json:"host"`
Port int `json:"port"`
}
r := JSONMarshal(config{Host: "localhost", Port: 8080})
Println(string(r.Value.([]byte)))
// Output: {"host":"localhost","port":8080}
}
func ExampleJSONUnmarshalString() {
type config struct {
Host string `json:"host"`
Port int `json:"port"`
}
var cfg config
JSONUnmarshalString(`{"host":"localhost","port":8080}`, &cfg)
Println(cfg.Host, cfg.Port)
// Output: localhost 8080
}
// --- Utilities ---
func ExampleID() {
id := ID()
Println(HasPrefix(id, "id-"))
// Output: true
}
func ExampleValidateName() {
Println(ValidateName("brain").OK)
Println(ValidateName("").OK)
Println(ValidateName("..").OK)
Println(ValidateName("path/traversal").OK)
// Output:
// true
// false
// false
// false
}
func ExampleSanitisePath() {
Println(SanitisePath("../../etc/passwd"))
Println(SanitisePath(""))
Println(SanitisePath("/some/path/file.txt"))
// Output:
// passwd
// invalid
// file.txt
}
// --- Command ---
func ExampleCore_Command() {
c := New()
c.Command("deploy/to/homelab", Command{
Action: func(opts Options) Result {
return Result{Value: Concat("deployed to ", opts.String("_arg")), OK: true}
},
})
r := c.Cli().Run("deploy", "to", "homelab")
Println(r.OK)
// Output: true
}
// --- Config ---
func ExampleConfig() {
c := New()
c.Config().Set("database.host", "localhost")
c.Config().Set("database.port", 5432)
c.Config().Enable("dark-mode")
Println(c.Config().String("database.host"))
Println(c.Config().Int("database.port"))
Println(c.Config().Enabled("dark-mode"))
// Output:
// localhost
// 5432
// true
}
// Error examples in error_example_test.go

127
fs.go
View file

@ -2,8 +2,6 @@
package core
import (
"io"
"io/fs"
"os"
"os/user"
"path/filepath"
@ -15,37 +13,6 @@ type Fs struct {
root string
}
// New initialises an Fs with the given root directory.
// Root "/" means unrestricted access. Empty root defaults to "/".
//
// fs := (&core.Fs{}).New("/")
func (m *Fs) New(root string) *Fs {
if root == "" {
root = "/"
}
m.root = root
return m
}
// NewUnrestricted returns a new Fs with root "/", granting full filesystem access.
// Use this instead of unsafe.Pointer to bypass the sandbox.
//
// fs := c.Fs().NewUnrestricted()
// fs.Read("/etc/hostname") // works — no sandbox
func (m *Fs) NewUnrestricted() *Fs {
return (&Fs{}).New("/")
}
// Root returns the sandbox root path.
//
// root := c.Fs().Root() // e.g. "/home/agent/.core"
func (m *Fs) Root() string {
if m.root == "" {
return "/"
}
return m.root
}
// path sanitises and returns the full path.
// Absolute paths are sandboxed under root (unless root is "/").
// Empty root defaults to "/" — the zero value of Fs is usable.
@ -169,52 +136,6 @@ func (m *Fs) WriteMode(p, content string, mode os.FileMode) Result {
return Result{OK: true}
}
// TempDir creates a temporary directory and returns its path.
// The caller is responsible for cleanup via fs.DeleteAll().
//
// dir := fs.TempDir("agent-workspace")
// defer fs.DeleteAll(dir)
func (m *Fs) TempDir(prefix string) string {
dir, err := os.MkdirTemp("", prefix)
if err != nil {
return ""
}
return dir
}
// DirFS returns an fs.FS rooted at the given directory path.
//
// fsys := core.DirFS("/path/to/templates")
func DirFS(dir string) fs.FS {
return os.DirFS(dir)
}
// WriteAtomic writes content by writing to a temp file then renaming.
// Rename is atomic on POSIX — concurrent readers never see a partial file.
// Use this for status files, config, or any file read from multiple goroutines.
//
// r := fs.WriteAtomic("/status.json", jsonData)
func (m *Fs) WriteAtomic(p, content 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 Result{err, false}
}
tmp := full + ".tmp." + shortRand()
if err := os.WriteFile(tmp, []byte(content), 0644); err != nil {
return Result{err, false}
}
if err := os.Rename(tmp, full); err != nil {
os.Remove(tmp)
return Result{err, false}
}
return Result{OK: true}
}
// EnsureDir creates directory if it doesn't exist.
func (m *Fs) EnsureDir(p string) Result {
vp := m.validatePath(p)
@ -326,54 +247,6 @@ func (m *Fs) WriteStream(path string) Result {
return m.Create(path)
}
// ReadAll reads all bytes from a ReadCloser and closes it.
// Wraps io.ReadAll so consumers don't import "io".
//
// r := fs.ReadStream(path)
// data := core.ReadAll(r.Value)
func ReadAll(reader any) Result {
rc, ok := reader.(io.Reader)
if !ok {
return Result{E("core.ReadAll", "not a reader", nil), false}
}
data, err := io.ReadAll(rc)
if closer, ok := reader.(io.Closer); ok {
closer.Close()
}
if err != nil {
return Result{err, false}
}
return Result{string(data), true}
}
// WriteAll writes content to a writer and closes it if it implements Closer.
//
// r := fs.WriteStream(path)
// core.WriteAll(r.Value, "content")
func WriteAll(writer any, content string) Result {
wc, ok := writer.(io.Writer)
if !ok {
return Result{E("core.WriteAll", "not a writer", nil), false}
}
_, err := wc.Write([]byte(content))
if closer, ok := writer.(io.Closer); ok {
closer.Close()
}
if err != nil {
return Result{err, false}
}
return Result{OK: true}
}
// CloseStream closes any value that implements io.Closer.
//
// core.CloseStream(r.Value)
func CloseStream(v any) {
if closer, ok := v.(io.Closer); ok {
closer.Close()
}
}
// Delete removes a file or empty directory.
func (m *Fs) Delete(p string) Result {
vp := m.validatePath(p)

View file

@ -1,42 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleFs_WriteAtomic() {
f := (&Fs{}).New("/")
dir := f.TempDir("example")
defer f.DeleteAll(dir)
path := Path(dir, "status.json")
f.WriteAtomic(path, `{"status":"completed"}`)
r := f.Read(path)
Println(r.Value)
// Output: {"status":"completed"}
}
func ExampleFs_NewUnrestricted() {
f := (&Fs{}).New("/")
dir := f.TempDir("example")
defer f.DeleteAll(dir)
// Write outside sandbox using Core's Fs
outside := Path(dir, "outside.txt")
f.Write(outside, "hello")
sandbox := (&Fs{}).New(Path(dir, "sandbox"))
unrestricted := sandbox.NewUnrestricted()
r := unrestricted.Read(outside)
Println(r.Value)
// Output: hello
}
func ExampleFs_Root() {
f := (&Fs{}).New("/srv/workspaces")
Println(f.Root())
// Output: /srv/workspaces
}

View file

@ -1,7 +1,10 @@
package core_test
import (
"io"
"io/fs"
"os"
"path/filepath"
"testing"
. "dappco.re/go/core"
@ -12,9 +15,9 @@ import (
func TestFs_WriteRead_Good(t *testing.T) {
dir := t.TempDir()
c := New()
c := New().Value.(*Core)
path := Path(dir, "test.txt")
path := filepath.Join(dir, "test.txt")
assert.True(t, c.Fs().Write(path, "hello core").OK)
r := c.Fs().Read(path)
@ -23,31 +26,31 @@ func TestFs_WriteRead_Good(t *testing.T) {
}
func TestFs_Read_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
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 := Path(dir, "sub", "dir")
c := New().Value.(*Core)
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()
c := New().Value.(*Core)
dir := t.TempDir()
assert.True(t, c.Fs().IsDir(dir))
assert.False(t, c.Fs().IsDir(Path(dir, "nonexistent")))
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 := Path(dir, "test.txt")
c := New().Value.(*Core)
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))
@ -56,19 +59,19 @@ func TestFs_IsFile_Good(t *testing.T) {
func TestFs_Exists_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "exists.txt")
c := New().Value.(*Core)
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(Path(dir, "nope")))
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(Path(dir, "a.txt"), "a")
c.Fs().Write(Path(dir, "b.txt"), "b")
c := New().Value.(*Core)
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)
@ -76,70 +79,76 @@ func TestFs_List_Good(t *testing.T) {
func TestFs_Stat_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "stat.txt")
c := New().Value.(*Core)
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.(fs.FileInfo).Name())
assert.Equal(t, "stat.txt", r.Value.(os.FileInfo).Name())
}
func TestFs_Open_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "open.txt")
c := New().Value.(*Core)
path := filepath.Join(dir, "open.txt")
c.Fs().Write(path, "content")
r := c.Fs().Open(path)
assert.True(t, r.OK)
CloseStream(r.Value)
r.Value.(io.Closer).Close()
}
func TestFs_Create_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "sub", "created.txt")
c := New().Value.(*Core)
path := filepath.Join(dir, "sub", "created.txt")
r := c.Fs().Create(path)
assert.True(t, r.OK)
WriteAll(r.Value, "hello")
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 := Path(dir, "append.txt")
c := New().Value.(*Core)
path := filepath.Join(dir, "append.txt")
c.Fs().Write(path, "first")
r := c.Fs().Append(path)
assert.True(t, r.OK)
WriteAll(r.Value, " second")
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 := Path(dir, "stream.txt")
c := New().Value.(*Core)
path := filepath.Join(dir, "stream.txt")
c.Fs().Write(path, "streamed")
r := c.Fs().ReadStream(path)
assert.True(t, r.OK)
CloseStream(r.Value)
r.Value.(io.Closer).Close()
}
func TestFs_WriteStream_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "sub", "ws.txt")
c := New().Value.(*Core)
path := filepath.Join(dir, "sub", "ws.txt")
r := c.Fs().WriteStream(path)
assert.True(t, r.OK)
WriteAll(r.Value, "stream")
w := r.Value.(io.WriteCloser)
w.Write([]byte("stream"))
w.Close()
}
func TestFs_Delete_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "delete.txt")
c := New().Value.(*Core)
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))
@ -147,19 +156,19 @@ func TestFs_Delete_Good(t *testing.T) {
func TestFs_DeleteAll_Good(t *testing.T) {
dir := t.TempDir()
c := New()
sub := Path(dir, "deep", "nested")
c := New().Value.(*Core)
sub := filepath.Join(dir, "deep", "nested")
c.Fs().EnsureDir(sub)
c.Fs().Write(Path(sub, "file.txt"), "data")
assert.True(t, c.Fs().DeleteAll(Path(dir, "deep")).OK)
assert.False(t, c.Fs().Exists(Path(dir, "deep")))
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 := Path(dir, "old.txt")
nw := Path(dir, "new.txt")
c := New().Value.(*Core)
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))
@ -168,12 +177,12 @@ func TestFs_Rename_Good(t *testing.T) {
func TestFs_WriteMode_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "secret.txt")
c := New().Value.(*Core)
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.(fs.FileInfo).Name())
assert.Equal(t, "secret.txt", r.Value.(os.FileInfo).Name())
}
// --- Zero Value ---
@ -182,7 +191,7 @@ func TestFs_ZeroValue_Good(t *testing.T) {
dir := t.TempDir()
zeroFs := &Fs{}
path := Path(dir, "zero.txt")
path := filepath.Join(dir, "zero.txt")
assert.True(t, zeroFs.Write(path, "zero value works").OK)
r := zeroFs.Read(path)
assert.True(t, r.OK)
@ -196,7 +205,7 @@ func TestFs_ZeroValue_List_Good(t *testing.T) {
dir := t.TempDir()
zeroFs := &Fs{}
(&Fs{}).New("/").Write(Path(dir, "a.txt"), "a")
os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644)
r := zeroFs.List(dir)
assert.True(t, r.OK)
entries := r.Value.([]fs.DirEntry)
@ -204,40 +213,40 @@ func TestFs_ZeroValue_List_Good(t *testing.T) {
}
func TestFs_Exists_NotFound_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.False(t, c.Fs().Exists("/nonexistent/path/xyz"))
}
// --- Fs path/validatePath edge cases ---
func TestFs_Read_EmptyPath_Ugly(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Fs().Read("")
assert.False(t, r.OK)
}
func TestFs_Write_EmptyPath_Ugly(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Fs().Write("", "data")
assert.False(t, r.OK)
}
func TestFs_Delete_Protected_Ugly(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Fs().Delete("/")
assert.False(t, r.OK)
}
func TestFs_DeleteAll_Protected_Ugly(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Fs().DeleteAll("/")
assert.False(t, r.OK)
}
func TestFs_ReadStream_WriteStream_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "stream.txt")
c := New().Value.(*Core)
path := filepath.Join(dir, "stream.txt")
c.Fs().Write(path, "streamed")
r := c.Fs().ReadStream(path)
@ -246,104 +255,3 @@ func TestFs_ReadStream_WriteStream_Good(t *testing.T) {
w := c.Fs().WriteStream(path)
assert.True(t, w.OK)
}
// --- WriteAtomic ---
func TestFs_WriteAtomic_Good(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "status.json")
r := c.Fs().WriteAtomic(path, `{"status":"completed"}`)
assert.True(t, r.OK)
read := c.Fs().Read(path)
assert.True(t, read.OK)
assert.Equal(t, `{"status":"completed"}`, read.Value)
}
func TestFs_WriteAtomic_Good_Overwrite(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "data.txt")
c.Fs().WriteAtomic(path, "first")
c.Fs().WriteAtomic(path, "second")
read := c.Fs().Read(path)
assert.Equal(t, "second", read.Value)
}
func TestFs_WriteAtomic_Bad_ReadOnlyDir(t *testing.T) {
// Write to a non-existent root that can't be created
m := (&Fs{}).New("/proc/nonexistent")
r := m.WriteAtomic("file.txt", "data")
assert.False(t, r.OK, "WriteAtomic must fail when parent dir cannot be created")
}
func TestFs_WriteAtomic_Ugly_NoTempFileLeftOver(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "clean.txt")
c.Fs().WriteAtomic(path, "content")
// Check no .tmp files remain
lr := c.Fs().List(dir)
entries, _ := lr.Value.([]fs.DirEntry)
for _, e := range entries {
assert.False(t, Contains(e.Name(), ".tmp."), "temp file should not remain after successful atomic write")
}
}
func TestFs_WriteAtomic_Good_CreatesParentDir(t *testing.T) {
dir := t.TempDir()
c := New()
path := Path(dir, "sub", "dir", "file.txt")
r := c.Fs().WriteAtomic(path, "nested")
assert.True(t, r.OK)
read := c.Fs().Read(path)
assert.Equal(t, "nested", read.Value)
}
// --- NewUnrestricted ---
func TestFs_NewUnrestricted_Good(t *testing.T) {
sandboxed := (&Fs{}).New(t.TempDir())
unrestricted := sandboxed.NewUnrestricted()
assert.Equal(t, "/", unrestricted.Root())
}
func TestFs_NewUnrestricted_Good_CanReadOutsideSandbox(t *testing.T) {
dir := t.TempDir()
outside := Path(dir, "outside.txt")
(&Fs{}).New("/").Write(outside, "hello")
sandboxed := (&Fs{}).New(Path(dir, "sandbox"))
unrestricted := sandboxed.NewUnrestricted()
r := unrestricted.Read(outside)
assert.True(t, r.OK, "unrestricted Fs must read paths outside the original sandbox")
assert.Equal(t, "hello", r.Value)
}
func TestFs_NewUnrestricted_Ugly_OriginalStaysSandboxed(t *testing.T) {
dir := t.TempDir()
sandbox := Path(dir, "sandbox")
(&Fs{}).New("/").EnsureDir(sandbox)
sandboxed := (&Fs{}).New(sandbox)
_ = sandboxed.NewUnrestricted() // getting unrestricted doesn't affect original
assert.Equal(t, sandbox, sandboxed.Root(), "original Fs must remain sandboxed")
}
// --- Root ---
func TestFs_Root_Good(t *testing.T) {
m := (&Fs{}).New("/home/agent")
assert.Equal(t, "/home/agent", m.Root())
}
func TestFs_Root_Good_Default(t *testing.T) {
m := (&Fs{}).New("")
assert.Equal(t, "/", m.Root())
}

View file

@ -10,12 +10,12 @@ import (
// --- I18n ---
func TestI18n_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.NotNil(t, c.I18n())
}
func TestI18n_AddLocales_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Data().New(NewOptions(
Option{Key: "name", Value: "lang"},
Option{Key: "source", Value: testFS},
@ -30,7 +30,7 @@ func TestI18n_AddLocales_Good(t *testing.T) {
}
func TestI18n_Locales_Empty_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.I18n().Locales()
assert.True(t, r.OK)
assert.Empty(t, r.Value.([]*Embed))
@ -39,7 +39,7 @@ func TestI18n_Locales_Empty_Good(t *testing.T) {
// --- Translator (no translator registered) ---
func TestI18n_Translate_NoTranslator_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
// Without a translator, Translate returns the key as-is
r := c.I18n().Translate("greeting.hello")
assert.True(t, r.OK)
@ -47,24 +47,24 @@ func TestI18n_Translate_NoTranslator_Good(t *testing.T) {
}
func TestI18n_SetLanguage_NoTranslator_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.I18n().SetLanguage("de")
assert.True(t, r.OK) // no-op without translator
}
func TestI18n_Language_NoTranslator_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.Equal(t, "en", c.I18n().Language())
}
func TestI18n_AvailableLanguages_NoTranslator_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
langs := c.I18n().AvailableLanguages()
assert.Equal(t, []string{"en"}, langs)
}
func TestI18n_Translator_Nil_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.False(t, c.I18n().Translator().OK)
}
@ -75,14 +75,14 @@ type mockTranslator struct {
}
func (m *mockTranslator) Translate(id string, args ...any) Result {
return Result{Concat("translated:", id), true}
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()
c := New().Value.(*Core)
tr := &mockTranslator{lang: "en"}
c.I18n().SetTranslator(tr)

View file

@ -1,17 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleEnv() {
Println(Env("OS")) // e.g. "darwin"
Println(Env("ARCH")) // e.g. "arm64"
}
func ExampleEnvKeys() {
keys := EnvKeys()
Println(len(keys) > 0)
// Output: true
}

View file

@ -3,6 +3,8 @@
package core_test
import (
"os"
"runtime"
"testing"
"time"
@ -11,84 +13,88 @@ import (
"github.com/stretchr/testify/require"
)
func TestInfo_Env_OS_Good(t *testing.T) {
v := core.Env("OS")
assert.NotEmpty(t, v)
assert.Contains(t, []string{"darwin", "linux", "windows"}, v)
func TestEnv_OS(t *testing.T) {
assert.Equal(t, runtime.GOOS, core.Env("OS"))
}
func TestInfo_Env_ARCH_Good(t *testing.T) {
v := core.Env("ARCH")
assert.NotEmpty(t, v)
assert.Contains(t, []string{"amd64", "arm64", "386"}, v)
func TestEnv_ARCH(t *testing.T) {
assert.Equal(t, runtime.GOARCH, core.Env("ARCH"))
}
func TestInfo_Env_GO_Good(t *testing.T) {
assert.True(t, core.HasPrefix(core.Env("GO"), "go"))
func TestEnv_GO(t *testing.T) {
assert.Equal(t, runtime.Version(), core.Env("GO"))
}
func TestInfo_Env_DS_Good(t *testing.T) {
ds := core.Env("DS")
assert.Contains(t, []string{"/", "\\"}, ds)
func TestEnv_DS(t *testing.T) {
assert.Equal(t, string(os.PathSeparator), core.Env("DS"))
}
func TestInfo_Env_PS_Good(t *testing.T) {
ps := core.Env("PS")
assert.Contains(t, []string{":", ";"}, ps)
func TestEnv_PS(t *testing.T) {
assert.Equal(t, string(os.PathListSeparator), core.Env("PS"))
}
func TestInfo_Env_DIR_HOME_Good(t *testing.T) {
home := core.Env("DIR_HOME")
assert.NotEmpty(t, home)
assert.True(t, core.PathIsAbs(home), "DIR_HOME should be absolute")
func TestEnv_DIR_HOME(t *testing.T) {
if ch := os.Getenv("CORE_HOME"); ch != "" {
assert.Equal(t, ch, core.Env("DIR_HOME"))
return
}
home, err := os.UserHomeDir()
require.NoError(t, err)
assert.Equal(t, home, core.Env("DIR_HOME"))
}
func TestInfo_Env_DIR_TMP_Good(t *testing.T) {
assert.NotEmpty(t, core.Env("DIR_TMP"))
func TestEnv_DIR_TMP(t *testing.T) {
assert.Equal(t, os.TempDir(), core.Env("DIR_TMP"))
}
func TestInfo_Env_DIR_CONFIG_Good(t *testing.T) {
assert.NotEmpty(t, core.Env("DIR_CONFIG"))
func TestEnv_DIR_CONFIG(t *testing.T) {
cfg, err := os.UserConfigDir()
require.NoError(t, err)
assert.Equal(t, cfg, core.Env("DIR_CONFIG"))
}
func TestInfo_Env_DIR_CACHE_Good(t *testing.T) {
assert.NotEmpty(t, core.Env("DIR_CACHE"))
func TestEnv_DIR_CACHE(t *testing.T) {
cache, err := os.UserCacheDir()
require.NoError(t, err)
assert.Equal(t, cache, core.Env("DIR_CACHE"))
}
func TestInfo_Env_HOSTNAME_Good(t *testing.T) {
assert.NotEmpty(t, core.Env("HOSTNAME"))
func TestEnv_HOSTNAME(t *testing.T) {
hostname, err := os.Hostname()
require.NoError(t, err)
assert.Equal(t, hostname, core.Env("HOSTNAME"))
}
func TestInfo_Env_USER_Good(t *testing.T) {
func TestEnv_USER(t *testing.T) {
assert.NotEmpty(t, core.Env("USER"))
}
func TestInfo_Env_PID_Good(t *testing.T) {
func TestEnv_PID(t *testing.T) {
assert.NotEmpty(t, core.Env("PID"))
}
func TestInfo_Env_NUM_CPU_Good(t *testing.T) {
func TestEnv_NUM_CPU(t *testing.T) {
assert.NotEmpty(t, core.Env("NUM_CPU"))
}
func TestInfo_Env_CORE_START_Good(t *testing.T) {
func TestEnv_CORE_START(t *testing.T) {
ts := core.Env("CORE_START")
require.NotEmpty(t, ts)
_, err := time.Parse(time.RFC3339, ts)
assert.NoError(t, err, "CORE_START should be valid RFC3339")
}
func TestInfo_Env_Bad_Unknown(t *testing.T) {
func TestEnv_Unknown(t *testing.T) {
assert.Equal(t, "", core.Env("NOPE"))
}
func TestInfo_Env_Good_CoreInstance(t *testing.T) {
c := core.New()
func TestEnv_CoreInstance(t *testing.T) {
c := core.New().Value.(*core.Core)
assert.Equal(t, core.Env("OS"), c.Env("OS"))
assert.Equal(t, core.Env("DIR_HOME"), c.Env("DIR_HOME"))
}
func TestInfo_EnvKeys_Good(t *testing.T) {
func TestEnvKeys(t *testing.T) {
keys := core.EnvKeys()
assert.NotEmpty(t, keys)
assert.Contains(t, keys, "OS")

55
ipc.go
View file

@ -11,9 +11,7 @@ import (
"sync"
)
// Ipc holds IPC dispatch data and the named action registry.
//
// ipc := (&core.Ipc{}).New()
// Ipc holds IPC dispatch data.
type Ipc struct {
ipcMu sync.RWMutex
ipcHandlers []func(*Core, Message) Result
@ -21,33 +19,23 @@ type Ipc struct {
queryMu sync.RWMutex
queryHandlers []QueryHandler
actions *Registry[*Action] // named action registry
tasks *Registry[*Task] // named task registry
taskMu sync.RWMutex
taskHandlers []TaskHandler
}
// broadcast dispatches a message to all registered IPC handlers.
// Each handler is wrapped in panic recovery. All handlers fire regardless of individual results.
func (c *Core) broadcast(msg Message) Result {
func (c *Core) Action(msg Message) Result {
c.ipc.ipcMu.RLock()
handlers := slices.Clone(c.ipc.ipcHandlers)
c.ipc.ipcMu.RUnlock()
for _, h := range handlers {
func() {
defer func() {
if r := recover(); r != nil {
Error("ACTION handler panicked", "panic", r)
}
}()
h(c, msg)
}()
if r := h(c, msg); !r.OK {
return r
}
}
return Result{OK: true}
}
// Query dispatches a request — first handler to return OK wins.
//
// r := c.Query(MyQuery{})
func (c *Core) Query(q Query) Result {
c.ipc.queryMu.RLock()
handlers := slices.Clone(c.ipc.queryHandlers)
@ -62,10 +50,6 @@ func (c *Core) Query(q Query) Result {
return Result{}
}
// QueryAll dispatches a request — collects all OK responses.
//
// r := c.QueryAll(countQuery{})
// results := r.Value.([]any)
func (c *Core) QueryAll(q Query) Result {
c.ipc.queryMu.RLock()
handlers := slices.Clone(c.ipc.queryHandlers)
@ -81,33 +65,8 @@ func (c *Core) QueryAll(q Query) Result {
return Result{results, true}
}
// RegisterQuery registers a handler for QUERY dispatch.
//
// c.RegisterQuery(func(_ *core.Core, q core.Query) core.Result { ... })
func (c *Core) RegisterQuery(handler QueryHandler) {
c.ipc.queryMu.Lock()
c.ipc.queryHandlers = append(c.ipc.queryHandlers, handler)
c.ipc.queryMu.Unlock()
}
// --- IPC Registration (handlers) ---
// RegisterAction registers a broadcast handler for ACTION messages.
//
// c.RegisterAction(func(c *core.Core, msg core.Message) core.Result {
// if ev, ok := msg.(AgentCompleted); ok { ... }
// return core.Result{OK: true}
// })
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()
}
// RegisterActions registers multiple broadcast handlers.
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()
}

View file

@ -1,7 +1,6 @@
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
@ -13,7 +12,7 @@ import (
type testMessage struct{ payload string }
func TestAction_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
var received Message
c.RegisterAction(func(_ *Core, msg Message) Result {
received = msg
@ -25,7 +24,7 @@ func TestAction_Good(t *testing.T) {
}
func TestAction_Multiple_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
count := 0
handler := func(_ *Core, _ Message) Result { count++; return Result{OK: true} }
c.RegisterActions(handler, handler, handler)
@ -34,65 +33,16 @@ func TestAction_Multiple_Good(t *testing.T) {
}
func TestAction_None_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
// No handlers registered — should succeed
r := c.ACTION(nil)
assert.True(t, r.OK)
}
func TestAction_Bad_HandlerFails(t *testing.T) {
c := New()
c.RegisterAction(func(_ *Core, _ Message) Result {
return Result{Value: NewError("intentional"), OK: false}
})
// ACTION is broadcast — even with a failing handler, dispatch succeeds
r := c.ACTION(testMessage{payload: "test"})
assert.True(t, r.OK)
}
func TestAction_Ugly_HandlerFailsChainContinues(t *testing.T) {
c := New()
var order []int
c.RegisterAction(func(_ *Core, _ Message) Result {
order = append(order, 1)
return Result{OK: true}
})
c.RegisterAction(func(_ *Core, _ Message) Result {
order = append(order, 2)
return Result{Value: NewError("handler 2 fails"), OK: false}
})
c.RegisterAction(func(_ *Core, _ Message) Result {
order = append(order, 3)
return Result{OK: true}
})
r := c.ACTION(testMessage{payload: "test"})
assert.True(t, r.OK)
assert.Equal(t, []int{1, 2, 3}, order, "all 3 handlers must fire even when handler 2 returns !OK")
}
func TestAction_Ugly_HandlerPanicsChainContinues(t *testing.T) {
c := New()
var order []int
c.RegisterAction(func(_ *Core, _ Message) Result {
order = append(order, 1)
return Result{OK: true}
})
c.RegisterAction(func(_ *Core, _ Message) Result {
panic("handler 2 explodes")
})
c.RegisterAction(func(_ *Core, _ Message) Result {
order = append(order, 3)
return Result{OK: true}
})
r := c.ACTION(testMessage{payload: "test"})
assert.True(t, r.OK)
assert.Equal(t, []int{1, 3}, order, "handlers 1 and 3 must fire even when handler 2 panics")
}
// --- IPC: Queries ---
func TestIpc_Query_Good(t *testing.T) {
c := New()
func TestQuery_Good(t *testing.T) {
c := New().Value.(*Core)
c.RegisterQuery(func(_ *Core, q Query) Result {
if q == "ping" {
return Result{Value: "pong", OK: true}
@ -104,8 +54,8 @@ func TestIpc_Query_Good(t *testing.T) {
assert.Equal(t, "pong", r.Value)
}
func TestIpc_Query_Unhandled_Good(t *testing.T) {
c := New()
func TestQuery_Unhandled_Good(t *testing.T) {
c := New().Value.(*Core)
c.RegisterQuery(func(_ *Core, q Query) Result {
return Result{}
})
@ -113,8 +63,8 @@ func TestIpc_Query_Unhandled_Good(t *testing.T) {
assert.False(t, r.OK)
}
func TestIpc_QueryAll_Good(t *testing.T) {
c := New()
func TestQueryAll_Good(t *testing.T) {
c := New().Value.(*Core)
c.RegisterQuery(func(_ *Core, _ Query) Result {
return Result{Value: "a", OK: true}
})
@ -129,14 +79,17 @@ func TestIpc_QueryAll_Good(t *testing.T) {
assert.Contains(t, results, "b")
}
// --- IPC: Named Action Invocation ---
// --- IPC: Tasks ---
func TestIpc_ActionInvoke_Good(t *testing.T) {
c := New()
c.Action("compute", func(_ context.Context, opts Options) Result {
return Result{Value: 42, OK: true}
func TestPerform_Good(t *testing.T) {
c := New().Value.(*Core)
c.RegisterTask(func(_ *Core, t Task) Result {
if t == "compute" {
return Result{Value: 42, OK: true}
}
return Result{}
})
r := c.Action("compute").Run(context.Background(), NewOptions())
r := c.PERFORM("compute")
assert.True(t, r.OK)
assert.Equal(t, 42, r.Value)
}

58
json.go
View file

@ -1,58 +0,0 @@
// SPDX-License-Identifier: EUPL-1.2
// JSON helpers for the Core framework.
// Wraps encoding/json so consumers don't import it directly.
// Same guardrail pattern as string.go wraps strings.
//
// Usage:
//
// data := core.JSONMarshal(myStruct)
// if data.OK { json := data.Value.([]byte) }
//
// r := core.JSONUnmarshal(jsonBytes, &target)
// if !r.OK { /* handle error */ }
package core
import "encoding/json"
// JSONMarshal serialises a value to JSON bytes.
//
// r := core.JSONMarshal(myStruct)
// if r.OK { data := r.Value.([]byte) }
func JSONMarshal(v any) Result {
data, err := json.Marshal(v)
if err != nil {
return Result{err, false}
}
return Result{data, true}
}
// JSONMarshalString serialises a value to a JSON string.
//
// s := core.JSONMarshalString(myStruct)
func JSONMarshalString(v any) string {
data, err := json.Marshal(v)
if err != nil {
return "{}"
}
return string(data)
}
// JSONUnmarshal deserialises JSON bytes into a target.
//
// var cfg Config
// r := core.JSONUnmarshal(data, &cfg)
func JSONUnmarshal(data []byte, target any) Result {
if err := json.Unmarshal(data, target); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
// JSONUnmarshalString deserialises a JSON string into a target.
//
// var cfg Config
// r := core.JSONUnmarshalString(`{"port":8080}`, &cfg)
func JSONUnmarshalString(s string, target any) Result {
return JSONUnmarshal([]byte(s), target)
}

View file

@ -1,63 +0,0 @@
package core_test
import (
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
type testJSON struct {
Name string `json:"name"`
Port int `json:"port"`
}
// --- JSONMarshal ---
func TestJson_JSONMarshal_Good(t *testing.T) {
r := JSONMarshal(testJSON{Name: "brain", Port: 8080})
assert.True(t, r.OK)
assert.Contains(t, string(r.Value.([]byte)), `"name":"brain"`)
}
func TestJson_JSONMarshal_Bad_Unmarshalable(t *testing.T) {
r := JSONMarshal(make(chan int))
assert.False(t, r.OK)
}
// --- JSONMarshalString ---
func TestJson_JSONMarshalString_Good(t *testing.T) {
s := JSONMarshalString(testJSON{Name: "x", Port: 1})
assert.Contains(t, s, `"name":"x"`)
}
func TestJson_JSONMarshalString_Ugly_Fallback(t *testing.T) {
s := JSONMarshalString(make(chan int))
assert.Equal(t, "{}", s)
}
// --- JSONUnmarshal ---
func TestJson_JSONUnmarshal_Good(t *testing.T) {
var target testJSON
r := JSONUnmarshal([]byte(`{"name":"brain","port":8080}`), &target)
assert.True(t, r.OK)
assert.Equal(t, "brain", target.Name)
assert.Equal(t, 8080, target.Port)
}
func TestJson_JSONUnmarshal_Bad_Invalid(t *testing.T) {
var target testJSON
r := JSONUnmarshal([]byte(`not json`), &target)
assert.False(t, r.OK)
}
// --- JSONUnmarshalString ---
func TestJson_JSONUnmarshalString_Good(t *testing.T) {
var target testJSON
r := JSONUnmarshalString(`{"name":"x","port":1}`, &target)
assert.True(t, r.OK)
assert.Equal(t, "x", target.Name)
}

46
llm.txt
View file

@ -1,46 +0,0 @@
# core/go — CoreGO Framework
> dappco.re/go/core — Dependency injection, service lifecycle, permission,
> and message-passing framework for Go. Foundation layer for the Lethean ecosystem.
## Entry Points
- CLAUDE.md — Agent instructions, build commands, subsystem table
- docs/RFC.md — API contract specification (21 sections, the authoritative spec)
## Package Layout
All source files at module root. No pkg/ nesting. Tests are *_test.go alongside source.
## Key Types
- Core — Central application container (core.New() returns *Core)
- Option — Single key-value pair {Key: string, Value: any}
- Options — Collection of Option with typed accessors
- Result — Universal return type {Value: any, OK: bool}
- Service — Managed component with lifecycle (Startable/Stoppable return Result)
- Action — Named callable with panic recovery and entitlement enforcement
- Task — Composed sequence of Actions (Steps, Async, Input piping)
- Registry[T] — Thread-safe named collection (universal brick)
- Process — Managed execution (sugar over Actions)
- API — Remote streams (protocol handlers, Drive integration)
- Entitlement — Permission check result (Allowed, Limit, Used, Remaining)
- Message — IPC broadcast type for ACTION
- Query — IPC request/response type for QUERY
## Service Pattern
core.New(
core.WithService(mypackage.Register),
)
func Register(c *core.Core) core.Result {
svc := &MyService{ServiceRuntime: core.NewServiceRuntime(c, opts)}
return core.Result{Value: svc, OK: true}
}
## Conventions
Follows RFC-025 Agent Experience (AX) principles.
Tests: TestFile_Function_{Good,Bad,Ugly} — 100% AX-7 naming.
See: https://core.help/specs/RFC-025-AGENT-EXPERIENCE/

49
lock.go
View file

@ -8,61 +8,82 @@ import (
"sync"
)
// package-level mutex infrastructure
var (
lockMu sync.Mutex
lockMap = make(map[string]*sync.RWMutex)
)
// Lock is the DTO for a named mutex.
type Lock struct {
Name string
Mutex *sync.RWMutex
locks *Registry[*sync.RWMutex] // per-Core named mutexes
}
// Lock returns a named Lock, creating the mutex if needed.
// Locks are per-Core — separate Core instances do not share mutexes.
func (c *Core) Lock(name string) *Lock {
r := c.lock.locks.Get(name)
if r.OK {
return &Lock{Name: name, Mutex: r.Value.(*sync.RWMutex)}
lockMu.Lock()
m, ok := lockMap[name]
if !ok {
m = &sync.RWMutex{}
lockMap[name] = m
}
m := &sync.RWMutex{}
c.lock.locks.Set(name, m)
lockMu.Unlock()
return &Lock{Name: name, Mutex: m}
}
// LockEnable marks that the service lock should be applied after initialisation.
func (c *Core) LockEnable(name ...string) {
n := "srv"
if len(name) > 0 {
n = name[0]
}
c.Lock(n).Mutex.Lock()
defer c.Lock(n).Mutex.Unlock()
c.services.lockEnabled = true
}
// LockApply activates the service lock if it was enabled.
func (c *Core) LockApply(name ...string) {
n := "srv"
if len(name) > 0 {
n = name[0]
}
c.Lock(n).Mutex.Lock()
defer c.Lock(n).Mutex.Unlock()
if c.services.lockEnabled {
c.services.Lock()
c.services.locked = true
}
}
// Startables returns services that have an OnStart function, in registration order.
// 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
c.services.Each(func(_ string, svc *Service) {
for _, svc := range c.services.services {
if svc.OnStart != nil {
out = append(out, svc)
}
})
}
return Result{out, true}
}
// Stoppables returns services that have an OnStop function, in registration order.
// 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
c.services.Each(func(_ string, svc *Service) {
for _, svc := range c.services.services {
if svc.OnStop != nil {
out = append(out, svc)
}
})
}
return Result{out, true}
}

View file

@ -1,18 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleCore_Lock() {
c := New()
lock := c.Lock("drain")
lock.Mutex.Lock()
Println("locked")
lock.Mutex.Unlock()
Println("unlocked")
// Output:
// locked
// unlocked
}

View file

@ -8,28 +8,28 @@ import (
)
func TestLock_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
lock := c.Lock("test")
assert.NotNil(t, lock)
assert.NotNil(t, lock.Mutex)
}
func TestLock_SameName_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
l1 := c.Lock("shared")
l2 := c.Lock("shared")
assert.Equal(t, l1, l2)
}
func TestLock_DifferentName_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
l1 := c.Lock("a")
l2 := c.Lock("b")
assert.NotEqual(t, l1, l2)
}
func TestLock_LockEnable_Good(t *testing.T) {
c := New()
func TestLockEnable_Good(t *testing.T) {
c := New().Value.(*Core)
c.Service("early", Service{})
c.LockEnable()
c.LockApply()
@ -38,16 +38,16 @@ func TestLock_LockEnable_Good(t *testing.T) {
assert.False(t, r.OK)
}
func TestLock_Startables_Good(t *testing.T) {
c := New()
func TestStartables_Good(t *testing.T) {
c := New().Value.(*Core)
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 TestLock_Stoppables_Good(t *testing.T) {
c := New()
func TestStoppables_Good(t *testing.T) {
c := New().Value.(*Core)
c.Service("s", Service{OnStop: func() Result { return Result{OK: true} }})
r := c.Stoppables()
assert.True(t, r.OK)

View file

@ -1,15 +0,0 @@
package core_test
import . "dappco.re/go/core"
func ExampleInfo() {
Info("server started", "port", 8080)
}
func ExampleWarn() {
Warn("deprecated", "feature", "old-api")
}
func ExampleSecurity() {
Security("access denied", "user", "unknown", "action", "admin.nuke")
}

View file

@ -1,6 +1,7 @@
package core_test
import (
"os"
"testing"
. "dappco.re/go/core"
@ -53,7 +54,7 @@ func TestLog_LevelString_Good(t *testing.T) {
}
func TestLog_CoreLog_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
assert.NotNil(t, c.Log())
}
@ -104,7 +105,7 @@ func TestLog_Username_Good(t *testing.T) {
// --- LogErr ---
func TestLog_LogErr_Good(t *testing.T) {
func TestLogErr_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
le := NewLogErr(l)
assert.NotNil(t, le)
@ -113,7 +114,7 @@ func TestLog_LogErr_Good(t *testing.T) {
le.Log(err)
}
func TestLog_LogErr_Nil_Good(t *testing.T) {
func TestLogErr_Nil_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
le := NewLogErr(l)
le.Log(nil) // should not panic
@ -121,13 +122,13 @@ func TestLog_LogErr_Nil_Good(t *testing.T) {
// --- LogPanic ---
func TestLog_LogPanic_Good(t *testing.T) {
func TestLogPanic_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
lp := NewLogPanic(l)
assert.NotNil(t, lp)
}
func TestLog_LogPanic_Recover_Good(t *testing.T) {
func TestLogPanic_Recover_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
lp := NewLogPanic(l)
assert.NotPanics(t, func() {
@ -140,7 +141,7 @@ func TestLog_LogPanic_Recover_Good(t *testing.T) {
func TestLog_SetOutput_Good(t *testing.T) {
l := NewLog(LogOptions{Level: LevelInfo})
l.SetOutput(NewBuilder())
l.SetOutput(os.Stderr)
l.Info("redirected")
}

View file

@ -38,9 +38,6 @@ func (r Result) Result(args ...any) Result {
return r.New(args...)
}
// New adapts Go (value, error) pairs into a Result.
//
// r := core.Result{}.New(file, err)
func (r Result) New(args ...any) Result {
if len(args) == 0 {
return r
@ -70,9 +67,6 @@ func (r Result) New(args ...any) Result {
return r
}
// Get returns the Result if OK, empty Result otherwise.
//
// r := core.Result{Value: "hello", OK: true}.Get()
func (r Result) Get() Result {
if r.OK {
return r

View file

@ -9,7 +9,7 @@ import (
// --- NewOptions ---
func TestOptions_NewOptions_Good(t *testing.T) {
func TestNewOptions_Good(t *testing.T) {
opts := NewOptions(
Option{Key: "name", Value: "brain"},
Option{Key: "port", Value: 8080},
@ -17,7 +17,7 @@ func TestOptions_NewOptions_Good(t *testing.T) {
assert.Equal(t, 2, opts.Len())
}
func TestOptions_NewOptions_Empty_Good(t *testing.T) {
func TestNewOptions_Empty_Good(t *testing.T) {
opts := NewOptions()
assert.Equal(t, 0, opts.Len())
assert.False(t, opts.Has("anything"))
@ -133,45 +133,45 @@ func TestOptions_TypedStruct_Good(t *testing.T) {
// --- Result ---
func TestOptions_Result_New_Good(t *testing.T) {
func TestResult_New_Good(t *testing.T) {
r := Result{}.New("value")
assert.Equal(t, "value", r.Value)
}
func TestOptions_Result_New_Error_Bad(t *testing.T) {
func TestResult_New_Error_Bad(t *testing.T) {
err := E("test", "failed", nil)
r := Result{}.New(err)
assert.False(t, r.OK)
assert.Equal(t, err, r.Value)
}
func TestOptions_Result_Result_Good(t *testing.T) {
func TestResult_Result_Good(t *testing.T) {
r := Result{Value: "hello", OK: true}
assert.Equal(t, r, r.Result())
}
func TestOptions_Result_Result_WithArgs_Good(t *testing.T) {
func TestResult_Result_WithArgs_Good(t *testing.T) {
r := Result{}.Result("value")
assert.Equal(t, "value", r.Value)
}
func TestOptions_Result_Get_Good(t *testing.T) {
func TestResult_Get_Good(t *testing.T) {
r := Result{Value: "hello", OK: true}
assert.True(t, r.Get().OK)
}
func TestOptions_Result_Get_Bad(t *testing.T) {
func TestResult_Get_Bad(t *testing.T) {
r := Result{Value: "err", OK: false}
assert.False(t, r.Get().OK)
}
// --- WithOption ---
func TestOptions_WithOption_Good(t *testing.T) {
func TestWithOption_Good(t *testing.T) {
c := New(
WithOption("name", "myapp"),
WithOption("port", 8080),
)
).Value.(*Core)
assert.Equal(t, "myapp", c.App().Name)
assert.Equal(t, 8080, c.Options().Int("port"))
}

View file

@ -1,37 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleJoinPath() {
Println(JoinPath("deploy", "to", "homelab"))
// Output: deploy/to/homelab
}
func ExamplePathBase() {
Println(PathBase("/srv/workspaces/alpha"))
// Output: alpha
}
func ExamplePathDir() {
Println(PathDir("/srv/workspaces/alpha"))
// Output: /srv/workspaces
}
func ExamplePathExt() {
Println(PathExt("report.pdf"))
// Output: .pdf
}
func ExampleCleanPath() {
Println(CleanPath("/tmp//file", "/"))
Println(CleanPath("a/b/../c", "/"))
Println(CleanPath("deploy/to/homelab", "/"))
// Output:
// /tmp/file
// a/c
// deploy/to/homelab
}

View file

@ -3,15 +3,18 @@
package core_test
import (
"os"
"path/filepath"
"testing"
core "dappco.re/go/core"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestPath_Relative(t *testing.T) {
home := core.Env("DIR_HOME")
home, err := os.UserHomeDir()
require.NoError(t, err)
ds := core.Env("DS")
assert.Equal(t, home+ds+"Code"+ds+".core", core.Path("Code", ".core"))
}
@ -22,14 +25,14 @@ func TestPath_Absolute(t *testing.T) {
}
func TestPath_Empty(t *testing.T) {
home := core.Env("DIR_HOME")
home, err := os.UserHomeDir()
require.NoError(t, err)
assert.Equal(t, home, core.Path())
}
func TestPath_Cleans(t *testing.T) {
home := core.Env("DIR_HOME")
home, err := os.UserHomeDir()
require.NoError(t, err)
assert.Equal(t, home+core.Env("DS")+"Code", core.Path("Code", "sub", ".."))
}
@ -38,32 +41,32 @@ func TestPath_CleanDoubleSlash(t *testing.T) {
assert.Equal(t, ds+"tmp"+ds+"file", core.Path("/tmp//file"))
}
func TestPath_PathBase(t *testing.T) {
func TestPathBase(t *testing.T) {
assert.Equal(t, "core", core.PathBase("/Users/snider/Code/core"))
assert.Equal(t, "homelab", core.PathBase("deploy/to/homelab"))
}
func TestPath_PathBase_Root(t *testing.T) {
func TestPathBase_Root(t *testing.T) {
assert.Equal(t, "/", core.PathBase("/"))
}
func TestPath_PathBase_Empty(t *testing.T) {
func TestPathBase_Empty(t *testing.T) {
assert.Equal(t, ".", core.PathBase(""))
}
func TestPath_PathDir(t *testing.T) {
func TestPathDir(t *testing.T) {
assert.Equal(t, "/Users/snider/Code", core.PathDir("/Users/snider/Code/core"))
}
func TestPath_PathDir_Root(t *testing.T) {
func TestPathDir_Root(t *testing.T) {
assert.Equal(t, "/", core.PathDir("/file"))
}
func TestPath_PathDir_NoDir(t *testing.T) {
func TestPathDir_NoDir(t *testing.T) {
assert.Equal(t, ".", core.PathDir("file.go"))
}
func TestPath_PathExt(t *testing.T) {
func TestPathExt(t *testing.T) {
assert.Equal(t, ".go", core.PathExt("main.go"))
assert.Equal(t, "", core.PathExt("Makefile"))
assert.Equal(t, ".gz", core.PathExt("archive.tar.gz"))
@ -73,38 +76,36 @@ func TestPath_EnvConsistency(t *testing.T) {
assert.Equal(t, core.Env("DIR_HOME"), core.Path())
}
func TestPath_PathGlob_Good(t *testing.T) {
func TestPathGlob_Good(t *testing.T) {
dir := t.TempDir()
f := (&core.Fs{}).New("/")
f.Write(core.Path(dir, "a.txt"), "a")
f.Write(core.Path(dir, "b.txt"), "b")
f.Write(core.Path(dir, "c.log"), "c")
os.WriteFile(filepath.Join(dir, "a.txt"), []byte("a"), 0644)
os.WriteFile(filepath.Join(dir, "b.txt"), []byte("b"), 0644)
os.WriteFile(filepath.Join(dir, "c.log"), []byte("c"), 0644)
matches := core.PathGlob(core.Path(dir, "*.txt"))
matches := core.PathGlob(filepath.Join(dir, "*.txt"))
assert.Len(t, matches, 2)
}
func TestPath_PathGlob_NoMatch(t *testing.T) {
func TestPathGlob_NoMatch(t *testing.T) {
matches := core.PathGlob("/nonexistent/pattern-*.xyz")
assert.Empty(t, matches)
}
func TestPath_PathIsAbs_Good(t *testing.T) {
func TestPathIsAbs_Good(t *testing.T) {
assert.True(t, core.PathIsAbs("/tmp"))
assert.True(t, core.PathIsAbs("/"))
assert.False(t, core.PathIsAbs("relative"))
assert.False(t, core.PathIsAbs(""))
}
func TestPath_CleanPath_Good(t *testing.T) {
func TestCleanPath_Good(t *testing.T) {
assert.Equal(t, "/a/b", core.CleanPath("/a//b", "/"))
assert.Equal(t, "/a/c", core.CleanPath("/a/b/../c", "/"))
assert.Equal(t, "/", core.CleanPath("/", "/"))
assert.Equal(t, ".", core.CleanPath("", "/"))
}
func TestPath_PathDir_TrailingSlash(t *testing.T) {
func TestPathDir_TrailingSlash(t *testing.T) {
result := core.PathDir("/Users/snider/Code/")
assert.Equal(t, "/Users/snider/Code", result)
}

View file

@ -1,96 +0,0 @@
// SPDX-License-Identifier: EUPL-1.2
// Process is the Core primitive for managed execution.
// Methods emit via named Actions — actual execution is handled by
// whichever service registers the "process.*" actions (typically go-process).
//
// If go-process is NOT registered, all methods return Result{OK: false}.
// This is permission-by-registration: no handler = no capability.
//
// Usage:
//
// r := c.Process().Run(ctx, "git", "log", "--oneline")
// if r.OK { output := r.Value.(string) }
//
// r := c.Process().RunIn(ctx, "/path/to/repo", "go", "test", "./...")
//
// Permission model:
//
// // Full Core — process registered:
// c := core.New(core.WithService(process.Register))
// c.Process().Run(ctx, "git", "log") // works
//
// // Sandboxed Core — no process:
// c := core.New()
// c.Process().Run(ctx, "git", "log") // Result{OK: false}
package core
import "context"
// Process is the Core primitive for process management.
// Zero dependencies — delegates to named Actions.
type Process struct {
core *Core
}
// Process returns the process management primitive.
//
// c.Process().Run(ctx, "git", "log")
func (c *Core) Process() *Process {
return &Process{core: c}
}
// Run executes a command synchronously and returns the output.
//
// r := c.Process().Run(ctx, "git", "log", "--oneline")
// if r.OK { output := r.Value.(string) }
func (p *Process) Run(ctx context.Context, command string, args ...string) Result {
return p.core.Action("process.run").Run(ctx, NewOptions(
Option{Key: "command", Value: command},
Option{Key: "args", Value: args},
))
}
// RunIn executes a command in a specific directory.
//
// r := c.Process().RunIn(ctx, "/repo", "go", "test", "./...")
func (p *Process) RunIn(ctx context.Context, dir string, command string, args ...string) Result {
return p.core.Action("process.run").Run(ctx, NewOptions(
Option{Key: "command", Value: command},
Option{Key: "args", Value: args},
Option{Key: "dir", Value: dir},
))
}
// RunWithEnv executes with additional environment variables.
//
// r := c.Process().RunWithEnv(ctx, dir, []string{"GOWORK=off"}, "go", "test")
func (p *Process) RunWithEnv(ctx context.Context, dir string, env []string, command string, args ...string) Result {
return p.core.Action("process.run").Run(ctx, NewOptions(
Option{Key: "command", Value: command},
Option{Key: "args", Value: args},
Option{Key: "dir", Value: dir},
Option{Key: "env", Value: env},
))
}
// Start spawns a detached/background process.
//
// r := c.Process().Start(ctx, ProcessStartOptions{Command: "docker", Args: []string{"run", "..."}})
func (p *Process) Start(ctx context.Context, opts Options) Result {
return p.core.Action("process.start").Run(ctx, opts)
}
// Kill terminates a managed process by ID or PID.
//
// c.Process().Kill(ctx, core.NewOptions(core.Option{Key: "id", Value: processID}))
func (p *Process) Kill(ctx context.Context, opts Options) Result {
return p.core.Action("process.kill").Run(ctx, opts)
}
// Exists returns true if any process execution capability is registered.
//
// if c.Process().Exists() { /* can run commands */ }
func (p *Process) Exists() bool {
return p.core.Action("process.run").Exists()
}

View file

@ -1,144 +0,0 @@
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Process.Run ---
func TestProcess_Run_Good(t *testing.T) {
c := New()
// Register a mock process handler
c.Action("process.run", func(_ context.Context, opts Options) Result {
cmd := opts.String("command")
return Result{Value: Concat("output of ", cmd), OK: true}
})
r := c.Process().Run(context.Background(), "git", "log")
assert.True(t, r.OK)
assert.Equal(t, "output of git", r.Value)
}
func TestProcess_Run_Bad_NotRegistered(t *testing.T) {
c := New()
// No process service registered — sandboxed Core
r := c.Process().Run(context.Background(), "git", "log")
assert.False(t, r.OK, "sandboxed Core must not execute commands")
}
func TestProcess_Run_Ugly_HandlerPanics(t *testing.T) {
c := New()
c.Action("process.run", func(_ context.Context, _ Options) Result {
panic("segfault")
})
r := c.Process().Run(context.Background(), "test")
assert.False(t, r.OK, "panicking handler must not crash")
}
// --- Process.RunIn ---
func TestProcess_RunIn_Good(t *testing.T) {
c := New()
c.Action("process.run", func(_ context.Context, opts Options) Result {
dir := opts.String("dir")
cmd := opts.String("command")
return Result{Value: Concat(cmd, " in ", dir), OK: true}
})
r := c.Process().RunIn(context.Background(), "/repo", "go", "test")
assert.True(t, r.OK)
assert.Equal(t, "go in /repo", r.Value)
}
// --- Process.RunWithEnv ---
func TestProcess_RunWithEnv_Good(t *testing.T) {
c := New()
c.Action("process.run", func(_ context.Context, opts Options) Result {
r := opts.Get("env")
if !r.OK {
return Result{Value: "no env", OK: true}
}
env := r.Value.([]string)
return Result{Value: env[0], OK: true}
})
r := c.Process().RunWithEnv(context.Background(), "/repo", []string{"GOWORK=off"}, "go", "test")
assert.True(t, r.OK)
assert.Equal(t, "GOWORK=off", r.Value)
}
// --- Process.Start ---
func TestProcess_Start_Good(t *testing.T) {
c := New()
c.Action("process.start", func(_ context.Context, opts Options) Result {
return Result{Value: "proc-1", OK: true}
})
r := c.Process().Start(context.Background(), NewOptions(
Option{Key: "command", Value: "docker"},
Option{Key: "args", Value: []string{"run", "nginx"}},
))
assert.True(t, r.OK)
assert.Equal(t, "proc-1", r.Value)
}
func TestProcess_Start_Bad_NotRegistered(t *testing.T) {
c := New()
r := c.Process().Start(context.Background(), NewOptions())
assert.False(t, r.OK)
}
// --- Process.Kill ---
func TestProcess_Kill_Good(t *testing.T) {
c := New()
c.Action("process.kill", func(_ context.Context, opts Options) Result {
return Result{OK: true}
})
r := c.Process().Kill(context.Background(), NewOptions(
Option{Key: "id", Value: "proc-1"},
))
assert.True(t, r.OK)
}
// --- Process.Exists ---
func TestProcess_Exists_Good(t *testing.T) {
c := New()
assert.False(t, c.Process().Exists(), "no process service = no capability")
c.Action("process.run", func(_ context.Context, _ Options) Result {
return Result{OK: true}
})
assert.True(t, c.Process().Exists(), "process.run registered = capability exists")
}
// --- Permission model ---
func TestProcess_Ugly_PermissionByRegistration(t *testing.T) {
// Full Core
full := New()
full.Action("process.run", func(_ context.Context, opts Options) Result {
return Result{Value: Concat("executed ", opts.String("command")), OK: true}
})
// Sandboxed Core
sandboxed := New()
// Full can execute
assert.True(t, full.Process().Exists())
r := full.Process().Run(context.Background(), "whoami")
assert.True(t, r.OK)
// Sandboxed cannot
assert.False(t, sandboxed.Process().Exists())
r = sandboxed.Process().Run(context.Background(), "whoami")
assert.False(t, r.OK)
}

View file

@ -1,271 +0,0 @@
// SPDX-License-Identifier: EUPL-1.2
// Thread-safe named collection primitive for the Core framework.
// Registry[T] is the universal brick — all named registries (services,
// commands, actions, drives, data) embed this type.
//
// Usage:
//
// r := core.NewRegistry[*MyService]()
// r.Set("brain", brainSvc)
// r.Get("brain") // Result{brainSvc, true}
// r.Has("brain") // true
// r.Names() // []string{"brain"} (insertion order)
// r.Each(func(name string, svc *MyService) { ... })
// r.Lock() // fully frozen — no more writes
// r.Seal() // no new keys, updates to existing OK
//
// Three lock modes:
//
// Open (default) — anything goes
// Sealed — no new keys, existing keys CAN be updated
// Locked — fully frozen, no writes at all
package core
import (
"path/filepath"
"sync"
)
// registryMode controls write behaviour.
type registryMode int
const (
registryOpen registryMode = iota // anything goes
registrySealed // update existing, no new keys
registryLocked // fully frozen
)
// Registry is a thread-safe named collection. The universal brick
// for all named registries in Core.
//
// r := core.NewRegistry[*Service]()
// r.Set("brain", svc)
// if r.Has("brain") { ... }
type Registry[T any] struct {
items map[string]T
disabled map[string]bool
order []string // insertion order
mu sync.RWMutex
mode registryMode
}
// NewRegistry creates an empty registry in Open mode.
//
// r := core.NewRegistry[*Service]()
func NewRegistry[T any]() *Registry[T] {
return &Registry[T]{
items: make(map[string]T),
disabled: make(map[string]bool),
}
}
// Set registers an item by name. Returns Result{OK: false} if the
// registry is locked, or if sealed and the key doesn't already exist.
//
// r.Set("brain", brainSvc)
func (r *Registry[T]) Set(name string, item T) Result {
r.mu.Lock()
defer r.mu.Unlock()
switch r.mode {
case registryLocked:
return Result{E("registry.Set", Concat("registry is locked, cannot set: ", name), nil), false}
case registrySealed:
if _, exists := r.items[name]; !exists {
return Result{E("registry.Set", Concat("registry is sealed, cannot add new key: ", name), nil), false}
}
}
if _, exists := r.items[name]; !exists {
r.order = append(r.order, name)
}
r.items[name] = item
return Result{OK: true}
}
// Get retrieves an item by name.
//
// res := r.Get("brain")
// if res.OK { svc := res.Value.(*Service) }
func (r *Registry[T]) Get(name string) Result {
r.mu.RLock()
defer r.mu.RUnlock()
item, ok := r.items[name]
if !ok {
return Result{}
}
return Result{item, true}
}
// Has returns true if the name exists in the registry.
//
// if r.Has("brain") { ... }
func (r *Registry[T]) Has(name string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
_, ok := r.items[name]
return ok
}
// Names returns all registered names in insertion order.
//
// names := r.Names() // ["brain", "monitor", "process"]
func (r *Registry[T]) Names() []string {
r.mu.RLock()
defer r.mu.RUnlock()
out := make([]string, len(r.order))
copy(out, r.order)
return out
}
// List returns items whose names match the glob pattern.
// Uses filepath.Match semantics: "*" matches any sequence, "?" matches one char.
//
// services := r.List("process.*")
func (r *Registry[T]) List(pattern string) []T {
r.mu.RLock()
defer r.mu.RUnlock()
var result []T
for _, name := range r.order {
if matched, _ := filepath.Match(pattern, name); matched {
if !r.disabled[name] {
result = append(result, r.items[name])
}
}
}
return result
}
// Each iterates over all items in insertion order, calling fn for each.
// Disabled items are skipped.
//
// r.Each(func(name string, svc *Service) {
// fmt.Println(name, svc)
// })
func (r *Registry[T]) Each(fn func(string, T)) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, name := range r.order {
if !r.disabled[name] {
fn(name, r.items[name])
}
}
}
// Len returns the number of registered items (including disabled).
//
// count := r.Len()
func (r *Registry[T]) Len() int {
r.mu.RLock()
defer r.mu.RUnlock()
return len(r.items)
}
// Delete removes an item. Returns Result{OK: false} if locked or not found.
//
// r.Delete("old-service")
func (r *Registry[T]) Delete(name string) Result {
r.mu.Lock()
defer r.mu.Unlock()
if r.mode == registryLocked {
return Result{E("registry.Delete", Concat("registry is locked, cannot delete: ", name), nil), false}
}
if _, exists := r.items[name]; !exists {
return Result{E("registry.Delete", Concat("not found: ", name), nil), false}
}
delete(r.items, name)
delete(r.disabled, name)
// Remove from order slice
for i, n := range r.order {
if n == name {
r.order = append(r.order[:i], r.order[i+1:]...)
break
}
}
return Result{OK: true}
}
// Disable soft-disables an item. It still exists but Each/List skip it.
// Returns Result{OK: false} if not found.
//
// r.Disable("broken-handler")
func (r *Registry[T]) Disable(name string) Result {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.items[name]; !exists {
return Result{E("registry.Disable", Concat("not found: ", name), nil), false}
}
r.disabled[name] = true
return Result{OK: true}
}
// Enable re-enables a disabled item.
//
// r.Enable("fixed-handler")
func (r *Registry[T]) Enable(name string) Result {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.items[name]; !exists {
return Result{E("registry.Enable", Concat("not found: ", name), nil), false}
}
delete(r.disabled, name)
return Result{OK: true}
}
// Disabled returns true if the item is soft-disabled.
func (r *Registry[T]) Disabled(name string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.disabled[name]
}
// Lock fully freezes the registry. No Set, no Delete.
//
// r.Lock() // after startup, prevent late registration
func (r *Registry[T]) Lock() {
r.mu.Lock()
defer r.mu.Unlock()
r.mode = registryLocked
}
// Locked returns true if the registry is fully frozen.
func (r *Registry[T]) Locked() bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.mode == registryLocked
}
// Seal prevents new keys but allows updates to existing keys.
// Use for hot-reload: shape is fixed, implementations can change.
//
// r.Seal() // no new capabilities, but handlers can be swapped
func (r *Registry[T]) Seal() {
r.mu.Lock()
defer r.mu.Unlock()
r.mode = registrySealed
}
// Sealed returns true if the registry is sealed (no new keys).
func (r *Registry[T]) Sealed() bool {
r.mu.RLock()
defer r.mu.RUnlock()
return r.mode == registrySealed
}
// Open resets the registry to open mode (default).
//
// r.Open() // re-enable writes for testing
func (r *Registry[T]) Open() {
r.mu.Lock()
defer r.mu.Unlock()
r.mode = registryOpen
}

View file

@ -1,70 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleRegistry_Set() {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Set("bravo", "second")
Println(r.Get("alpha").Value)
// Output: first
}
func ExampleRegistry_Names() {
r := NewRegistry[int]()
r.Set("charlie", 3)
r.Set("alpha", 1)
r.Set("bravo", 2)
Println(r.Names())
// Output: [charlie alpha bravo]
}
func ExampleRegistry_List() {
r := NewRegistry[string]()
r.Set("process.run", "run")
r.Set("process.kill", "kill")
r.Set("brain.recall", "recall")
items := r.List("process.*")
Println(len(items))
// Output: 2
}
func ExampleRegistry_Each() {
r := NewRegistry[int]()
r.Set("a", 1)
r.Set("b", 2)
r.Set("c", 3)
sum := 0
r.Each(func(_ string, v int) { sum += v })
Println(sum)
// Output: 6
}
func ExampleRegistry_Disable() {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Set("bravo", "second")
r.Disable("alpha")
var names []string
r.Each(func(name string, _ string) { names = append(names, name) })
Println(names)
// Output: [bravo]
}
func ExampleRegistry_Delete() {
r := NewRegistry[string]()
r.Set("temp", "value")
Println(r.Has("temp"))
r.Delete("temp")
Println(r.Has("temp"))
// Output:
// true
// false
}

View file

@ -1,387 +0,0 @@
package core_test
import (
"sync"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- Set ---
func TestRegistry_Set_Good(t *testing.T) {
r := NewRegistry[string]()
res := r.Set("alpha", "first")
assert.True(t, res.OK)
assert.True(t, r.Has("alpha"))
}
func TestRegistry_Set_Good_Update(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Set("alpha", "second")
res := r.Get("alpha")
assert.Equal(t, "second", res.Value)
assert.Equal(t, 1, r.Len(), "update should not increase count")
}
func TestRegistry_Set_Bad_Locked(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Lock()
res := r.Set("beta", "second")
assert.False(t, res.OK)
}
func TestRegistry_Set_Bad_SealedNewKey(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Seal()
res := r.Set("beta", "new")
assert.False(t, res.OK, "sealed registry must reject new keys")
}
func TestRegistry_Set_Good_SealedExistingKey(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Seal()
res := r.Set("alpha", "updated")
assert.True(t, res.OK, "sealed registry must allow updates to existing keys")
assert.Equal(t, "updated", r.Get("alpha").Value)
}
func TestRegistry_Set_Ugly_ConcurrentWrites(t *testing.T) {
r := NewRegistry[int]()
var wg sync.WaitGroup
for i := 0; i < 100; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
r.Set(Sprintf("key-%d", n), n)
}(i)
}
wg.Wait()
assert.Equal(t, 100, r.Len())
}
// --- Get ---
func TestRegistry_Get_Good(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
res := r.Get("alpha")
assert.True(t, res.OK)
assert.Equal(t, "value", res.Value)
}
func TestRegistry_Get_Bad_NotFound(t *testing.T) {
r := NewRegistry[string]()
res := r.Get("missing")
assert.False(t, res.OK)
}
func TestRegistry_Get_Ugly_EmptyKey(t *testing.T) {
r := NewRegistry[string]()
r.Set("", "empty-key")
res := r.Get("")
assert.True(t, res.OK, "empty string is a valid key")
assert.Equal(t, "empty-key", res.Value)
}
// --- Has ---
func TestRegistry_Has_Good(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
assert.True(t, r.Has("alpha"))
}
func TestRegistry_Has_Bad_NotFound(t *testing.T) {
r := NewRegistry[string]()
assert.False(t, r.Has("missing"))
}
func TestRegistry_Has_Ugly_AfterDelete(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
r.Delete("alpha")
assert.False(t, r.Has("alpha"))
}
// --- Names ---
func TestRegistry_Names_Good(t *testing.T) {
r := NewRegistry[int]()
r.Set("charlie", 3)
r.Set("alpha", 1)
r.Set("bravo", 2)
assert.Equal(t, []string{"charlie", "alpha", "bravo"}, r.Names(), "must preserve insertion order")
}
func TestRegistry_Names_Bad_Empty(t *testing.T) {
r := NewRegistry[int]()
assert.Empty(t, r.Names())
}
func TestRegistry_Names_Ugly_AfterDeleteAndReinsert(t *testing.T) {
r := NewRegistry[int]()
r.Set("a", 1)
r.Set("b", 2)
r.Set("c", 3)
r.Delete("b")
r.Set("d", 4)
assert.Equal(t, []string{"a", "c", "d"}, r.Names())
}
// --- Each ---
func TestRegistry_Each_Good(t *testing.T) {
r := NewRegistry[int]()
r.Set("a", 1)
r.Set("b", 2)
r.Set("c", 3)
var names []string
var sum int
r.Each(func(name string, val int) {
names = append(names, name)
sum += val
})
assert.Equal(t, []string{"a", "b", "c"}, names)
assert.Equal(t, 6, sum)
}
func TestRegistry_Each_Bad_Empty(t *testing.T) {
r := NewRegistry[int]()
called := false
r.Each(func(_ string, _ int) { called = true })
assert.False(t, called)
}
func TestRegistry_Each_Ugly_SkipsDisabled(t *testing.T) {
r := NewRegistry[int]()
r.Set("a", 1)
r.Set("b", 2)
r.Set("c", 3)
r.Disable("b")
var names []string
r.Each(func(name string, _ int) { names = append(names, name) })
assert.Equal(t, []string{"a", "c"}, names)
}
// --- Len ---
func TestRegistry_Len_Good(t *testing.T) {
r := NewRegistry[string]()
assert.Equal(t, 0, r.Len())
r.Set("a", "1")
assert.Equal(t, 1, r.Len())
r.Set("b", "2")
assert.Equal(t, 2, r.Len())
}
// --- List ---
func TestRegistry_List_Good(t *testing.T) {
r := NewRegistry[string]()
r.Set("process.run", "run")
r.Set("process.start", "start")
r.Set("agentic.dispatch", "dispatch")
items := r.List("process.*")
assert.Len(t, items, 2)
assert.Contains(t, items, "run")
assert.Contains(t, items, "start")
}
func TestRegistry_List_Bad_NoMatch(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "1")
items := r.List("beta.*")
assert.Empty(t, items)
}
func TestRegistry_List_Ugly_SkipsDisabled(t *testing.T) {
r := NewRegistry[string]()
r.Set("process.run", "run")
r.Set("process.kill", "kill")
r.Disable("process.kill")
items := r.List("process.*")
assert.Len(t, items, 1)
assert.Equal(t, "run", items[0])
}
func TestRegistry_List_Good_WildcardAll(t *testing.T) {
r := NewRegistry[string]()
r.Set("a", "1")
r.Set("b", "2")
items := r.List("*")
assert.Len(t, items, 2)
}
// --- Delete ---
func TestRegistry_Delete_Good(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
res := r.Delete("alpha")
assert.True(t, res.OK)
assert.False(t, r.Has("alpha"))
assert.Equal(t, 0, r.Len())
}
func TestRegistry_Delete_Bad_NotFound(t *testing.T) {
r := NewRegistry[string]()
res := r.Delete("missing")
assert.False(t, res.OK)
}
func TestRegistry_Delete_Ugly_Locked(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
r.Lock()
res := r.Delete("alpha")
assert.False(t, res.OK, "locked registry must reject delete")
assert.True(t, r.Has("alpha"), "item must survive failed delete")
}
// --- Disable / Enable ---
func TestRegistry_Disable_Good(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
res := r.Disable("alpha")
assert.True(t, res.OK)
assert.True(t, r.Disabled("alpha"))
// Still exists via Get/Has
assert.True(t, r.Has("alpha"))
assert.True(t, r.Get("alpha").OK)
}
func TestRegistry_Disable_Bad_NotFound(t *testing.T) {
r := NewRegistry[string]()
res := r.Disable("missing")
assert.False(t, res.OK)
}
func TestRegistry_Disable_Ugly_EnableRoundtrip(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
r.Disable("alpha")
assert.True(t, r.Disabled("alpha"))
res := r.Enable("alpha")
assert.True(t, res.OK)
assert.False(t, r.Disabled("alpha"))
// Verify Each sees it again
var seen []string
r.Each(func(name string, _ string) { seen = append(seen, name) })
assert.Equal(t, []string{"alpha"}, seen)
}
func TestRegistry_Enable_Bad_NotFound(t *testing.T) {
r := NewRegistry[string]()
res := r.Enable("missing")
assert.False(t, res.OK)
}
// --- Lock ---
func TestRegistry_Lock_Good(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
r.Lock()
assert.True(t, r.Locked())
// Reads still work
assert.True(t, r.Get("alpha").OK)
assert.True(t, r.Has("alpha"))
}
func TestRegistry_Lock_Bad_SetAfterLock(t *testing.T) {
r := NewRegistry[string]()
r.Lock()
res := r.Set("new", "value")
assert.False(t, res.OK)
}
func TestRegistry_Lock_Ugly_UpdateAfterLock(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Lock()
res := r.Set("alpha", "second")
assert.False(t, res.OK, "locked registry must reject even updates")
assert.Equal(t, "first", r.Get("alpha").Value, "value must not change")
}
// --- Seal ---
func TestRegistry_Seal_Good(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "first")
r.Seal()
assert.True(t, r.Sealed())
// Update existing OK
res := r.Set("alpha", "second")
assert.True(t, res.OK)
assert.Equal(t, "second", r.Get("alpha").Value)
}
func TestRegistry_Seal_Bad_NewKey(t *testing.T) {
r := NewRegistry[string]()
r.Seal()
res := r.Set("new", "value")
assert.False(t, res.OK)
}
func TestRegistry_Seal_Ugly_DeleteWhileSealed(t *testing.T) {
r := NewRegistry[string]()
r.Set("alpha", "value")
r.Seal()
// Delete is NOT locked by seal — only Set for new keys
res := r.Delete("alpha")
assert.True(t, res.OK, "seal blocks new keys, not deletes")
}
// --- Open ---
func TestRegistry_Open_Good(t *testing.T) {
r := NewRegistry[string]()
r.Lock()
assert.True(t, r.Locked())
r.Open()
assert.False(t, r.Locked())
// Can write again
res := r.Set("new", "value")
assert.True(t, res.OK)
}
// --- Concurrency ---
func TestRegistry_Ugly_ConcurrentReadWrite(t *testing.T) {
r := NewRegistry[int]()
var wg sync.WaitGroup
// Concurrent writers
for i := 0; i < 50; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
r.Set(Sprintf("w-%d", n), n)
}(i)
}
// Concurrent readers
for i := 0; i < 50; i++ {
wg.Add(1)
go func(n int) {
defer wg.Done()
r.Has(Sprintf("w-%d", n))
r.Get(Sprintf("w-%d", n))
r.Names()
r.Len()
}(i)
}
wg.Wait()
assert.Equal(t, 50, r.Len())
}

View file

@ -25,19 +25,8 @@ func NewServiceRuntime[T any](c *Core, opts T) *ServiceRuntime[T] {
return &ServiceRuntime[T]{core: c, opts: opts}
}
// Core returns the Core instance this service is registered with.
//
// c := s.Core()
func (r *ServiceRuntime[T]) Core() *Core { return r.core }
// Options returns the typed options this service was created with.
//
// opts := s.Options() // MyOptions{BufferSize: 1024, ...}
func (r *ServiceRuntime[T]) Options() T { return r.opts }
// Config is a shortcut to s.Core().Config().
//
// host := s.Config().String("database.host")
func (r *ServiceRuntime[T]) Core() *Core { return r.core }
func (r *ServiceRuntime[T]) Options() T { return r.opts }
func (r *ServiceRuntime[T]) Config() *Config { return r.core.Config() }
// --- Lifecycle ---
@ -117,7 +106,11 @@ type ServiceFactory func() Result
// NewWithFactories creates a Runtime with the provided service factories.
func NewWithFactories(app any, factories map[string]ServiceFactory) Result {
c := New(WithOptions(NewOptions(Option{Key: "name", Value: "core"})))
r := New(WithOptions(NewOptions(Option{Key: "name", Value: "core"})))
if !r.OK {
return r
}
c := r.Value.(*Core)
c.app.Runtime = app
names := slices.Sorted(maps.Keys(factories))
@ -148,14 +141,10 @@ func NewRuntime(app any) Result {
return NewWithFactories(app, map[string]ServiceFactory{})
}
// ServiceName returns "Core" — the Runtime's service identity.
func (r *Runtime) ServiceName() string { return "Core" }
// ServiceStartup starts all services via the embedded Core.
func (r *Runtime) ServiceStartup(ctx context.Context, options any) Result {
return r.Core.ServiceStartup(ctx, options)
}
// ServiceShutdown stops all services via the embedded Core.
func (r *Runtime) ServiceShutdown(ctx context.Context) Result {
if r.Core != nil {
return r.Core.ServiceShutdown(ctx)

View file

@ -15,8 +15,8 @@ type testOpts struct {
Timeout int
}
func TestRuntime_ServiceRuntime_Good(t *testing.T) {
c := New()
func TestServiceRuntime_Good(t *testing.T) {
c := New().Value.(*Core)
opts := testOpts{URL: "https://api.lthn.ai", Timeout: 30}
rt := NewServiceRuntime(c, opts)
@ -28,7 +28,7 @@ func TestRuntime_ServiceRuntime_Good(t *testing.T) {
// --- NewWithFactories ---
func TestRuntime_NewWithFactories_Good(t *testing.T) {
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} },
@ -38,14 +38,14 @@ func TestRuntime_NewWithFactories_Good(t *testing.T) {
assert.NotNil(t, rt.Core)
}
func TestRuntime_NewWithFactories_NilFactory_Good(t *testing.T) {
func TestNewWithFactories_NilFactory_Good(t *testing.T) {
r := NewWithFactories(nil, map[string]ServiceFactory{
"bad": nil,
})
assert.True(t, r.OK) // nil factories skipped
}
func TestRuntime_NewRuntime_Good(t *testing.T) {
func TestNewRuntime_Good(t *testing.T) {
r := NewRuntime(nil)
assert.True(t, r.OK)
}
@ -102,7 +102,7 @@ func TestRuntime_ServiceShutdown_NilCore_Good(t *testing.T) {
func TestCore_ServiceShutdown_Good(t *testing.T) {
stopped := false
c := New()
c := New().Value.(*Core)
c.Service("test", Service{
OnStart: func() Result { return Result{OK: true} },
OnStop: func() Result { stopped = true; return Result{OK: true} },
@ -114,7 +114,7 @@ func TestCore_ServiceShutdown_Good(t *testing.T) {
}
func TestCore_Context_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.ServiceStartup(context.Background(), nil)
assert.NotNil(t, c.Context())
c.ServiceShutdown(context.Background())

View file

@ -17,8 +17,6 @@
package core
import "context"
// Service is a managed component with optional lifecycle.
type Service struct {
Name string
@ -29,11 +27,11 @@ type Service struct {
OnReload func() Result
}
// ServiceRegistry holds registered services. Embeds Registry[*Service]
// for thread-safe named storage with insertion order.
type ServiceRegistry struct {
*Registry[*Service]
// serviceRegistry holds registered services.
type serviceRegistry struct {
services map[string]*Service
lockEnabled bool
locked bool
}
// --- Core service methods ---
@ -44,32 +42,31 @@ type ServiceRegistry struct {
// r := c.Service("auth")
func (c *Core) Service(name string, service ...Service) Result {
if len(service) == 0 {
r := c.services.Get(name)
if !r.OK {
return Result{}
}
svc := r.Value.(*Service)
// Return the instance if available, otherwise the Service DTO
if svc.Instance != nil {
return Result{svc.Instance, true}
}
return Result{svc, true}
c.Lock("srv").Mutex.RLock()
v, ok := c.services.services[name]
c.Lock("srv").Mutex.RUnlock()
return Result{v, ok}
}
if name == "" {
return Result{E("core.Service", "service name cannot be empty", nil), false}
}
if c.services.Locked() {
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 c.services.Has(name) {
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
return c.services.Set(name, srv)
c.services.services[name] = srv
return Result{OK: true}
}
// RegisterService registers a service instance by name.
@ -82,10 +79,13 @@ func (c *Core) RegisterService(name string, instance any) Result {
return Result{E("core.RegisterService", "service name cannot be empty", nil), false}
}
if c.services.Locked() {
c.Lock("srv").Mutex.Lock()
defer c.Lock("srv").Mutex.Unlock()
if c.services.locked {
return Result{E("core.RegisterService", Concat("service \"", name, "\" not permitted — registry locked"), nil), false}
}
if c.services.Has(name) {
if _, exists := c.services.services[name]; exists {
return Result{E("core.RegisterService", Join(" ", "service", name, "already registered"), nil), false}
}
@ -94,16 +94,22 @@ func (c *Core) RegisterService(name string, instance any) Result {
// Auto-discover lifecycle interfaces
if s, ok := instance.(Startable); ok {
srv.OnStart = func() Result {
return s.OnStartup(c.context)
if err := s.OnStartup(c.context); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
}
if s, ok := instance.(Stoppable); ok {
srv.OnStop = func() Result {
return s.OnShutdown(context.Background())
if err := s.OnShutdown(c.context); err != nil {
return Result{err, false}
}
return Result{OK: true}
}
}
c.services.Set(name, srv)
c.services.services[name] = srv
// Auto-discover IPC handler
if handler, ok := instance.(interface {
@ -126,7 +132,11 @@ func ServiceFor[T any](c *Core, name string) (T, bool) {
if !r.OK {
return zero, false
}
typed, ok := r.Value.(T)
svc := r.Value.(*Service)
if svc.Instance == nil {
return zero, false
}
typed, ok := svc.Instance.(T)
return typed, ok
}
@ -142,12 +152,18 @@ func MustServiceFor[T any](c *Core, name string) T {
return v
}
// Services returns all registered service names in registration order.
// Services returns all registered service names.
//
// names := c.Services()
func (c *Core) Services() []string {
if c.services == nil {
return nil
}
return c.services.Names()
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
}

View file

@ -1,50 +0,0 @@
package core_test
import (
"context"
. "dappco.re/go/core"
)
func ExampleServiceFor() {
c := New(
WithService(func(c *Core) Result {
return c.Service("cache", Service{
OnStart: func() Result { return Result{OK: true} },
})
}),
)
svc := c.Service("cache")
Println(svc.OK)
// Output: true
}
func ExampleWithService() {
started := false
c := New(
WithService(func(c *Core) Result {
return c.Service("worker", Service{
OnStart: func() Result { started = true; return Result{OK: true} },
})
}),
)
c.ServiceStartup(context.Background(), nil)
Println(started)
c.ServiceShutdown(context.Background())
// Output: true
}
func ExampleWithServiceLock() {
c := New(
WithService(func(c *Core) Result {
return c.Service("allowed", Service{})
}),
WithServiceLock(),
)
// Can't register after lock
r := c.Service("blocked", Service{})
Println(r.OK)
// Output: false
}

View file

@ -1,7 +1,6 @@
package core_test
import (
"context"
"testing"
. "dappco.re/go/core"
@ -11,26 +10,26 @@ import (
// --- Service Registration ---
func TestService_Register_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Service("auth", Service{})
assert.True(t, r.OK)
}
func TestService_Register_Duplicate_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Service("auth", Service{})
r := c.Service("auth", Service{})
assert.False(t, r.OK)
}
func TestService_Register_Empty_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Service("", Service{})
assert.False(t, r.OK)
}
func TestService_Get_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Service("brain", Service{OnStart: func() Result { return Result{OK: true} }})
r := c.Service("brain")
assert.True(t, r.OK)
@ -38,13 +37,13 @@ func TestService_Get_Good(t *testing.T) {
}
func TestService_Get_Bad(t *testing.T) {
c := New()
c := New().Value.(*Core)
r := c.Service("nonexistent")
assert.False(t, r.OK)
}
func TestService_Names_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
c.Service("a", Service{})
c.Service("b", Service{})
names := c.Services()
@ -56,7 +55,7 @@ func TestService_Names_Good(t *testing.T) {
// --- Service Lifecycle ---
func TestService_Lifecycle_Good(t *testing.T) {
c := New()
c := New().Value.(*Core)
started := false
stopped := false
c.Service("lifecycle", Service{
@ -78,113 +77,3 @@ func TestService_Lifecycle_Good(t *testing.T) {
stoppables[0].OnStop()
assert.True(t, stopped)
}
type autoLifecycleService struct {
started bool
stopped bool
messages []Message
}
func (s *autoLifecycleService) OnStartup(_ context.Context) Result {
s.started = true
return Result{OK: true}
}
func (s *autoLifecycleService) OnShutdown(_ context.Context) Result {
s.stopped = true
return Result{OK: true}
}
func (s *autoLifecycleService) HandleIPCEvents(_ *Core, msg Message) Result {
s.messages = append(s.messages, msg)
return Result{OK: true}
}
func TestService_RegisterService_Bad(t *testing.T) {
t.Run("EmptyName", func(t *testing.T) {
c := New()
r := c.RegisterService("", "value")
assert.False(t, r.OK)
err, ok := r.Value.(error)
if assert.True(t, ok) {
assert.Equal(t, "core.RegisterService", Operation(err))
}
})
t.Run("DuplicateName", func(t *testing.T) {
c := New()
assert.True(t, c.RegisterService("svc", "first").OK)
r := c.RegisterService("svc", "second")
assert.False(t, r.OK)
})
t.Run("LockedRegistry", func(t *testing.T) {
c := New()
c.LockEnable()
c.LockApply()
r := c.RegisterService("blocked", "value")
assert.False(t, r.OK)
})
}
func TestService_RegisterService_Ugly(t *testing.T) {
t.Run("AutoDiscoversLifecycleAndIPCHandlers", func(t *testing.T) {
c := New()
svc := &autoLifecycleService{}
r := c.RegisterService("auto", svc)
assert.True(t, r.OK)
assert.True(t, c.ServiceStartup(context.Background(), nil).OK)
assert.True(t, c.ACTION("ping").OK)
assert.True(t, c.ServiceShutdown(context.Background()).OK)
assert.True(t, svc.started)
assert.True(t, svc.stopped)
assert.Contains(t, svc.messages, Message("ping"))
})
t.Run("NilInstanceReturnsServiceDTO", func(t *testing.T) {
c := New()
assert.True(t, c.RegisterService("nil", nil).OK)
r := c.Service("nil")
if assert.True(t, r.OK) {
svc, ok := r.Value.(*Service)
if assert.True(t, ok) {
assert.Equal(t, "nil", svc.Name)
assert.Nil(t, svc.Instance)
}
}
})
}
func TestService_ServiceFor_Bad(t *testing.T) {
typed, ok := ServiceFor[string](New(), "missing")
assert.False(t, ok)
assert.Equal(t, "", typed)
}
func TestService_ServiceFor_Ugly(t *testing.T) {
c := New()
assert.True(t, c.RegisterService("value", "hello").OK)
typed, ok := ServiceFor[int](c, "value")
assert.False(t, ok)
assert.Equal(t, 0, typed)
}
func TestService_MustServiceFor_Bad(t *testing.T) {
c := New()
assert.PanicsWithError(t, `core.MustServiceFor: service "missing" not found or wrong type`, func() {
_ = MustServiceFor[string](c, "missing")
})
}
func TestService_MustServiceFor_Ugly(t *testing.T) {
var c *Core
assert.Panics(t, func() {
_ = MustServiceFor[string](c, "missing")
})
}

View file

@ -1,35 +0,0 @@
package core_test
import (
. "dappco.re/go/core"
)
func ExampleContains() {
Println(Contains("hello world", "world"))
Println(Contains("hello world", "mars"))
// Output:
// true
// false
}
func ExampleSplit() {
parts := Split("deploy/to/homelab", "/")
Println(parts)
// Output: [deploy to homelab]
}
func ExampleJoin() {
Println(Join("/", "deploy", "to", "homelab"))
// Output: deploy/to/homelab
}
func ExampleConcat() {
Println(Concat("hello", " ", "world"))
// Output: hello world
}
func ExampleTrim() {
Println(Trim(" spaced "))
// Output: spaced
}

View file

@ -9,61 +9,61 @@ import (
// --- String Operations ---
func TestString_HasPrefix_Good(t *testing.T) {
func TestHasPrefix_Good(t *testing.T) {
assert.True(t, HasPrefix("--verbose", "--"))
assert.True(t, HasPrefix("-v", "-"))
assert.False(t, HasPrefix("hello", "-"))
}
func TestString_HasSuffix_Good(t *testing.T) {
func TestHasSuffix_Good(t *testing.T) {
assert.True(t, HasSuffix("test.go", ".go"))
assert.False(t, HasSuffix("test.go", ".py"))
}
func TestString_TrimPrefix_Good(t *testing.T) {
func TestTrimPrefix_Good(t *testing.T) {
assert.Equal(t, "verbose", TrimPrefix("--verbose", "--"))
assert.Equal(t, "hello", TrimPrefix("hello", "--"))
}
func TestString_TrimSuffix_Good(t *testing.T) {
func TestTrimSuffix_Good(t *testing.T) {
assert.Equal(t, "test", TrimSuffix("test.go", ".go"))
assert.Equal(t, "test.go", TrimSuffix("test.go", ".py"))
}
func TestString_Contains_Good(t *testing.T) {
func TestContains_Good(t *testing.T) {
assert.True(t, Contains("hello world", "world"))
assert.False(t, Contains("hello world", "mars"))
}
func TestString_Split_Good(t *testing.T) {
func TestSplit_Good(t *testing.T) {
assert.Equal(t, []string{"a", "b", "c"}, Split("a/b/c", "/"))
}
func TestString_SplitN_Good(t *testing.T) {
func TestSplitN_Good(t *testing.T) {
assert.Equal(t, []string{"key", "value=extra"}, SplitN("key=value=extra", "=", 2))
}
func TestString_Join_Good(t *testing.T) {
func TestJoin_Good(t *testing.T) {
assert.Equal(t, "a/b/c", Join("/", "a", "b", "c"))
}
func TestString_Replace_Good(t *testing.T) {
func TestReplace_Good(t *testing.T) {
assert.Equal(t, "deploy.to.homelab", Replace("deploy/to/homelab", "/", "."))
}
func TestString_Lower_Good(t *testing.T) {
func TestLower_Good(t *testing.T) {
assert.Equal(t, "hello", Lower("HELLO"))
}
func TestString_Upper_Good(t *testing.T) {
func TestUpper_Good(t *testing.T) {
assert.Equal(t, "HELLO", Upper("hello"))
}
func TestString_Trim_Good(t *testing.T) {
func TestTrim_Good(t *testing.T) {
assert.Equal(t, "hello", Trim(" hello "))
}
func TestString_RuneCount_Good(t *testing.T) {
func TestRuneCount_Good(t *testing.T) {
assert.Equal(t, 5, RuneCount("hello"))
assert.Equal(t, 1, RuneCount("🔥"))
assert.Equal(t, 0, RuneCount(""))

111
task.go
View file

@ -1,61 +1,92 @@
// SPDX-License-Identifier: EUPL-1.2
// Background action dispatch for the Core framework.
// PerformAsync runs a named Action in a background goroutine with
// panic recovery and progress broadcasting.
// Background task dispatch for the Core framework.
package core
import "context"
import (
"reflect"
"slices"
"strconv"
)
// PerformAsync dispatches a named action in a background goroutine.
// Broadcasts ActionTaskStarted, ActionTaskProgress, and ActionTaskCompleted
// as IPC messages so other services can track progress.
//
// r := c.PerformAsync("agentic.dispatch", opts)
// taskID := r.Value.(string)
func (c *Core) PerformAsync(action string, opts Options) Result {
// TaskState holds background task state.
type TaskState struct {
Identifier string
Task Task
Result any
Error error
}
// PerformAsync dispatches a task in a background goroutine.
func (c *Core) PerformAsync(t Task) Result {
if c.shutdown.Load() {
return Result{}
}
taskID := ID()
c.ACTION(ActionTaskStarted{TaskIdentifier: taskID, Action: action, Options: opts})
taskID := Concat("task-", strconv.FormatUint(c.taskIDCounter.Add(1), 10))
if tid, ok := t.(TaskWithIdentifier); ok {
tid.SetTaskIdentifier(taskID)
}
c.ACTION(ActionTaskStarted{TaskIdentifier: taskID, Task: t})
c.waitGroup.Go(func() {
defer func() {
if rec := recover(); rec != nil {
c.ACTION(ActionTaskCompleted{
TaskIdentifier: taskID,
Action: action,
Result: Result{E("core.PerformAsync", Sprint("panic: ", rec), nil), false},
})
err := E("core.PerformAsync", Sprint("panic: ", rec), nil)
c.ACTION(ActionTaskCompleted{TaskIdentifier: taskID, Task: t, Result: nil, Error: err})
}
}()
r := c.Action(action).Run(context.Background(), opts)
c.ACTION(ActionTaskCompleted{
TaskIdentifier: taskID,
Action: action,
Result: r,
})
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 Result{taskID, true}
}
// Progress broadcasts a progress update for a background task.
//
// c.Progress(taskID, 0.5, "halfway done", "agentic.dispatch")
func (c *Core) Progress(taskID string, progress float64, message string, action string) {
c.ACTION(ActionTaskProgress{
TaskIdentifier: taskID,
Action: action,
Progress: progress,
Message: message,
})
func (c *Core) Progress(taskID string, progress float64, message string, t Task) {
c.ACTION(ActionTaskProgress{TaskIdentifier: taskID, Task: t, Progress: progress, Message: message})
}
// Registration methods (RegisterAction, RegisterActions)
// are in ipc.go — registration is IPC's responsibility.
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 {
r := h(c, t)
if r.OK {
return r
}
}
return Result{}
}
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) Result) {
c.ipc.ipcMu.Lock()
c.ipc.ipcHandlers = append(c.ipc.ipcHandlers, handlers...)
c.ipc.ipcMu.Unlock()
}
func (c *Core) RegisterTask(handler TaskHandler) {
c.ipc.taskMu.Lock()
c.ipc.taskHandlers = append(c.ipc.taskHandlers, handler)
c.ipc.taskMu.Unlock()
}

View file

@ -1,50 +0,0 @@
package core_test
import (
"context"
. "dappco.re/go/core"
)
func ExampleTask_Run() {
c := New()
var order string
c.Action("step.a", func(_ context.Context, _ Options) Result {
order += "a"
return Result{Value: "from-a", OK: true}
})
c.Action("step.b", func(_ context.Context, opts Options) Result {
order += "b"
input := opts.Get("_input")
if input.OK {
return Result{Value: "got:" + input.Value.(string), OK: true}
}
return Result{OK: true}
})
c.Task("pipe", Task{
Steps: []Step{
{Action: "step.a"},
{Action: "step.b", Input: "previous"},
},
})
r := c.Task("pipe").Run(context.Background(), c, NewOptions())
Println(order)
Println(r.Value)
// Output:
// ab
// got:from-a
}
func ExampleCore_PerformAsync() {
c := New()
c.Action("bg.work", func(_ context.Context, _ Options) Result {
return Result{Value: "done", OK: true}
})
r := c.PerformAsync("bg.work", NewOptions())
Println(HasPrefix(r.Value.(string), "id-"))
// Output: true
}

View file

@ -12,21 +12,22 @@ import (
// --- PerformAsync ---
func TestTask_PerformAsync_Good(t *testing.T) {
c := New()
func TestPerformAsync_Good(t *testing.T) {
c := New().Value.(*Core)
var mu sync.Mutex
var result string
c.Action("work", func(_ context.Context, _ Options) Result {
c.RegisterTask(func(_ *Core, task Task) Result {
mu.Lock()
result = "done"
mu.Unlock()
return Result{Value: "done", OK: true}
return Result{"completed", true}
})
r := c.PerformAsync("work", NewOptions())
r := c.PerformAsync("work")
assert.True(t, r.OK)
assert.True(t, HasPrefix(r.Value.(string), "id-"), "should return task ID")
taskID := r.Value.(string)
assert.NotEmpty(t, taskID)
time.Sleep(100 * time.Millisecond)
@ -35,25 +36,24 @@ func TestTask_PerformAsync_Good(t *testing.T) {
mu.Unlock()
}
func TestTask_PerformAsync_Good_Progress(t *testing.T) {
c := New()
c.Action("tracked", func(_ context.Context, _ Options) Result {
func TestPerformAsync_Progress_Good(t *testing.T) {
c := New().Value.(*Core)
c.RegisterTask(func(_ *Core, task Task) Result {
return Result{OK: true}
})
r := c.PerformAsync("tracked", NewOptions())
r := c.PerformAsync("work")
taskID := r.Value.(string)
c.Progress(taskID, 0.5, "halfway", "tracked")
c.Progress(taskID, 0.5, "halfway", "work")
}
func TestTask_PerformAsync_Good_Completion(t *testing.T) {
c := New()
func TestPerformAsync_Completion_Good(t *testing.T) {
c := New().Value.(*Core)
completed := make(chan ActionTaskCompleted, 1)
c.Action("completable", func(_ context.Context, _ Options) Result {
return Result{Value: "output", OK: true}
c.RegisterTask(func(_ *Core, task Task) Result {
return Result{Value: "result", OK: true}
})
c.RegisterAction(func(_ *Core, msg Message) Result {
if evt, ok := msg.(ActionTaskCompleted); ok {
completed <- evt
@ -61,19 +61,19 @@ func TestTask_PerformAsync_Good_Completion(t *testing.T) {
return Result{OK: true}
})
c.PerformAsync("completable", NewOptions())
c.PerformAsync("work")
select {
case evt := <-completed:
assert.True(t, evt.Result.OK)
assert.Equal(t, "output", evt.Result.Value)
assert.Nil(t, evt.Error)
assert.Equal(t, "result", evt.Result)
case <-time.After(2 * time.Second):
t.Fatal("timed out waiting for completion")
}
}
func TestTask_PerformAsync_Bad_ActionNotRegistered(t *testing.T) {
c := New()
func TestPerformAsync_NoHandler_Good(t *testing.T) {
c := New().Value.(*Core)
completed := make(chan ActionTaskCompleted, 1)
c.RegisterAction(func(_ *Core, msg Message) Result {
@ -83,45 +83,43 @@ func TestTask_PerformAsync_Bad_ActionNotRegistered(t *testing.T) {
return Result{OK: true}
})
c.PerformAsync("nonexistent", NewOptions())
c.PerformAsync("unhandled")
select {
case evt := <-completed:
assert.False(t, evt.Result.OK, "unregistered action should fail")
assert.NotNil(t, evt.Error)
case <-time.After(2 * time.Second):
t.Fatal("timed out")
}
}
func TestTask_PerformAsync_Bad_AfterShutdown(t *testing.T) {
c := New()
c.Action("work", func(_ context.Context, _ Options) Result { return Result{OK: true} })
func TestPerformAsync_AfterShutdown_Bad(t *testing.T) {
c := New().Value.(*Core)
c.ServiceStartup(context.Background(), nil)
c.ServiceShutdown(context.Background())
r := c.PerformAsync("work", NewOptions())
r := c.PerformAsync("should not run")
assert.False(t, r.OK)
}
// --- RegisterAction + RegisterActions (broadcast handlers) ---
// --- RegisterAction + RegisterActions ---
func TestTask_RegisterAction_Good(t *testing.T) {
c := New()
func TestRegisterAction_Good(t *testing.T) {
c := New().Value.(*Core)
called := false
c.RegisterAction(func(_ *Core, _ Message) Result {
called = true
return Result{OK: true}
})
c.ACTION(nil)
c.Action(nil)
assert.True(t, called)
}
func TestTask_RegisterActions_Good(t *testing.T) {
c := New()
func TestRegisterActions_Good(t *testing.T) {
c := New().Value.(*Core)
count := 0
h := func(_ *Core, _ Message) Result { count++; return Result{OK: true} }
c.RegisterActions(h, h)
c.ACTION(nil)
c.Action(nil)
assert.Equal(t, 2, count)
}

View file

@ -6,75 +6,11 @@
package core
import (
crand "crypto/rand"
"encoding/hex"
"fmt"
"io"
"os"
"strconv"
"sync/atomic"
)
// --- ID Generation ---
var idCounter atomic.Uint64
// ID returns a unique identifier. Format: "id-{counter}-{random}".
// Counter is process-wide atomic. Random suffix prevents collision across restarts.
//
// id := core.ID() // "id-1-a3f2b1"
// id2 := core.ID() // "id-2-c7e4d9"
func ID() string {
return Concat("id-", strconv.FormatUint(idCounter.Add(1), 10), "-", shortRand())
}
func shortRand() string {
b := make([]byte, 3)
crand.Read(b)
return hex.EncodeToString(b)
}
// --- Validation ---
// ValidateName checks that a string is a valid service/action/command name.
// Rejects empty, ".", "..", and names containing path separators.
//
// r := core.ValidateName("brain") // Result{"brain", true}
// r := core.ValidateName("") // Result{error, false}
// r := core.ValidateName("../escape") // Result{error, false}
func ValidateName(name string) Result {
if name == "" || name == "." || name == ".." {
return Result{E("validate", Concat("invalid name: ", name), nil), false}
}
if Contains(name, "/") || Contains(name, "\\") {
return Result{E("validate", Concat("name contains path separator: ", name), nil), false}
}
return Result{name, true}
}
// SanitisePath extracts the base filename and rejects traversal attempts.
// Returns "invalid" for dangerous inputs.
//
// core.SanitisePath("../../etc/passwd") // "passwd"
// core.SanitisePath("") // "invalid"
// core.SanitisePath("..") // "invalid"
func SanitisePath(path string) string {
safe := PathBase(path)
if safe == "." || safe == ".." || safe == "" {
return "invalid"
}
return safe
}
// --- I/O ---
// Println prints values to stdout with a newline. Replaces fmt.Println.
//
// core.Println("hello", 42, true)
func Println(args ...any) {
fmt.Println(args...)
}
// Print writes a formatted line to a writer, defaulting to os.Stdout.
//
// core.Print(nil, "hello %s", "world") // → stdout

View file

@ -1,112 +1,29 @@
package core_test
import (
"errors"
"testing"
. "dappco.re/go/core"
"github.com/stretchr/testify/assert"
)
// --- ID ---
func TestUtils_ID_Good(t *testing.T) {
id := ID()
assert.True(t, HasPrefix(id, "id-"))
assert.True(t, len(id) > 5, "ID should have counter + random suffix")
}
func TestUtils_ID_Good_Unique(t *testing.T) {
seen := make(map[string]bool)
for i := 0; i < 1000; i++ {
id := ID()
assert.False(t, seen[id], "ID collision: %s", id)
seen[id] = true
}
}
func TestUtils_ID_Ugly_CounterMonotonic(t *testing.T) {
// IDs should contain increasing counter values
id1 := ID()
id2 := ID()
// Both should start with "id-" and have different counter parts
assert.NotEqual(t, id1, id2)
assert.True(t, HasPrefix(id1, "id-"))
assert.True(t, HasPrefix(id2, "id-"))
}
// --- ValidateName ---
func TestUtils_ValidateName_Good(t *testing.T) {
r := ValidateName("brain")
assert.True(t, r.OK)
assert.Equal(t, "brain", r.Value)
}
func TestUtils_ValidateName_Good_WithDots(t *testing.T) {
r := ValidateName("process.run")
assert.True(t, r.OK, "dots in names are valid — used for action namespacing")
}
func TestUtils_ValidateName_Bad_Empty(t *testing.T) {
r := ValidateName("")
assert.False(t, r.OK)
}
func TestUtils_ValidateName_Bad_Dot(t *testing.T) {
r := ValidateName(".")
assert.False(t, r.OK)
}
func TestUtils_ValidateName_Bad_DotDot(t *testing.T) {
r := ValidateName("..")
assert.False(t, r.OK)
}
func TestUtils_ValidateName_Bad_Slash(t *testing.T) {
r := ValidateName("../escape")
assert.False(t, r.OK)
}
func TestUtils_ValidateName_Ugly_Backslash(t *testing.T) {
r := ValidateName("windows\\path")
assert.False(t, r.OK)
}
// --- SanitisePath ---
func TestUtils_SanitisePath_Good(t *testing.T) {
assert.Equal(t, "file.txt", SanitisePath("/some/path/file.txt"))
}
func TestUtils_SanitisePath_Bad_Empty(t *testing.T) {
assert.Equal(t, "invalid", SanitisePath(""))
}
func TestUtils_SanitisePath_Bad_DotDot(t *testing.T) {
assert.Equal(t, "invalid", SanitisePath(".."))
}
func TestUtils_SanitisePath_Ugly_Traversal(t *testing.T) {
// PathBase extracts "passwd" — the traversal is stripped
assert.Equal(t, "passwd", SanitisePath("../../etc/passwd"))
}
// --- FilterArgs ---
func TestUtils_FilterArgs_Good(t *testing.T) {
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 TestUtils_FilterArgs_Empty_Good(t *testing.T) {
func TestFilterArgs_Empty_Good(t *testing.T) {
clean := FilterArgs(nil)
assert.Nil(t, clean)
}
// --- ParseFlag ---
func TestUtils_ParseFlag_ShortValid_Good(t *testing.T) {
func TestParseFlag_ShortValid_Good(t *testing.T) {
// Single letter
k, v, ok := ParseFlag("-v")
assert.True(t, ok)
@ -126,7 +43,7 @@ func TestUtils_ParseFlag_ShortValid_Good(t *testing.T) {
assert.Equal(t, "8080", v)
}
func TestUtils_ParseFlag_ShortInvalid_Bad(t *testing.T) {
func TestParseFlag_ShortInvalid_Bad(t *testing.T) {
// Multiple chars with single dash — invalid
_, _, ok := ParseFlag("-verbose")
assert.False(t, ok)
@ -135,7 +52,7 @@ func TestUtils_ParseFlag_ShortInvalid_Bad(t *testing.T) {
assert.False(t, ok)
}
func TestUtils_ParseFlag_LongValid_Good(t *testing.T) {
func TestParseFlag_LongValid_Good(t *testing.T) {
k, v, ok := ParseFlag("--verbose")
assert.True(t, ok)
assert.Equal(t, "verbose", k)
@ -147,13 +64,13 @@ func TestUtils_ParseFlag_LongValid_Good(t *testing.T) {
assert.Equal(t, "8080", v)
}
func TestUtils_ParseFlag_LongInvalid_Bad(t *testing.T) {
func TestParseFlag_LongInvalid_Bad(t *testing.T) {
// Single char with double dash — invalid
_, _, ok := ParseFlag("--v")
assert.False(t, ok)
}
func TestUtils_ParseFlag_NotAFlag_Bad(t *testing.T) {
func TestParseFlag_NotAFlag_Bad(t *testing.T) {
_, _, ok := ParseFlag("hello")
assert.False(t, ok)
@ -163,57 +80,57 @@ func TestUtils_ParseFlag_NotAFlag_Bad(t *testing.T) {
// --- IsFlag ---
func TestUtils_IsFlag_Good(t *testing.T) {
func TestIsFlag_Good(t *testing.T) {
assert.True(t, IsFlag("-v"))
assert.True(t, IsFlag("--verbose"))
assert.True(t, IsFlag("-"))
}
func TestUtils_IsFlag_Bad(t *testing.T) {
func TestIsFlag_Bad(t *testing.T) {
assert.False(t, IsFlag("hello"))
assert.False(t, IsFlag(""))
}
// --- Arg ---
func TestUtils_Arg_String_Good(t *testing.T) {
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 TestUtils_Arg_Int_Good(t *testing.T) {
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 TestUtils_Arg_Bool_Good(t *testing.T) {
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 TestUtils_Arg_UnsupportedType_Good(t *testing.T) {
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 TestUtils_Arg_OutOfBounds_Bad(t *testing.T) {
func TestArg_OutOfBounds_Bad(t *testing.T) {
r := Arg(5, "only", "two")
assert.False(t, r.OK)
assert.Nil(t, r.Value)
}
func TestUtils_Arg_NoArgs_Bad(t *testing.T) {
func TestArg_NoArgs_Bad(t *testing.T) {
r := Arg(0)
assert.False(t, r.OK)
assert.Nil(t, r.Value)
}
func TestUtils_Arg_ErrorDetection_Good(t *testing.T) {
err := NewError("fail")
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)
@ -221,78 +138,78 @@ func TestUtils_Arg_ErrorDetection_Good(t *testing.T) {
// --- ArgString ---
func TestUtils_ArgString_Good(t *testing.T) {
func TestArgString_Good(t *testing.T) {
assert.Equal(t, "hello", ArgString(0, "hello", 42))
assert.Equal(t, "world", ArgString(1, "hello", "world"))
}
func TestUtils_ArgString_WrongType_Bad(t *testing.T) {
func TestArgString_WrongType_Bad(t *testing.T) {
assert.Equal(t, "", ArgString(0, 42))
}
func TestUtils_ArgString_OutOfBounds_Bad(t *testing.T) {
func TestArgString_OutOfBounds_Bad(t *testing.T) {
assert.Equal(t, "", ArgString(3, "only"))
}
// --- ArgInt ---
func TestUtils_ArgInt_Good(t *testing.T) {
func TestArgInt_Good(t *testing.T) {
assert.Equal(t, 42, ArgInt(0, 42, "hello"))
assert.Equal(t, 99, ArgInt(1, 0, 99))
}
func TestUtils_ArgInt_WrongType_Bad(t *testing.T) {
func TestArgInt_WrongType_Bad(t *testing.T) {
assert.Equal(t, 0, ArgInt(0, "not an int"))
}
func TestUtils_ArgInt_OutOfBounds_Bad(t *testing.T) {
func TestArgInt_OutOfBounds_Bad(t *testing.T) {
assert.Equal(t, 0, ArgInt(5, 1, 2))
}
// --- ArgBool ---
func TestUtils_ArgBool_Good(t *testing.T) {
func TestArgBool_Good(t *testing.T) {
assert.Equal(t, true, ArgBool(0, true, "hello"))
assert.Equal(t, false, ArgBool(1, true, false))
}
func TestUtils_ArgBool_WrongType_Bad(t *testing.T) {
func TestArgBool_WrongType_Bad(t *testing.T) {
assert.Equal(t, false, ArgBool(0, "not a bool"))
}
func TestUtils_ArgBool_OutOfBounds_Bad(t *testing.T) {
func TestArgBool_OutOfBounds_Bad(t *testing.T) {
assert.Equal(t, false, ArgBool(5, true))
}
// --- Result.Result() ---
func TestUtils_Result_Result_SingleArg_Good(t *testing.T) {
func TestResult_Result_SingleArg_Good(t *testing.T) {
r := Result{}.Result("value")
assert.True(t, r.OK)
assert.Equal(t, "value", r.Value)
}
func TestUtils_Result_Result_NilError_Good(t *testing.T) {
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 TestUtils_Result_Result_WithError_Bad(t *testing.T) {
err := NewError("fail")
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 TestUtils_Result_Result_ZeroArgs_Good(t *testing.T) {
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 TestUtils_Result_Result_ZeroArgs_Empty_Good(t *testing.T) {
func TestResult_Result_ZeroArgs_Empty_Good(t *testing.T) {
r := Result{}
got := r.Result()
assert.Nil(t, got.Value)