From 3d7ec7efce035a9fcda7b7b52a4a64fe77d767db Mon Sep 17 00:00:00 2001 From: Virgil Date: Mon, 30 Mar 2026 00:00:33 +0000 Subject: [PATCH] fix(ax): make core-agent startup explicit Co-Authored-By: Virgil --- cmd/core-agent/commands.go | 56 ++++++++++++++++++ cmd/core-agent/commands_example_test.go | 7 +++ cmd/core-agent/commands_test.go | 78 ++++++++++++++++++++----- cmd/core-agent/main.go | 53 ++++++++++++++++- cmd/core-agent/main_test.go | 21 ++++++- cmd/core-agent/mcp_service.go | 7 +++ cmd/core-agent/mcp_service_test.go | 9 ++- cmd/core-agent/update_test.go | 25 ++++++-- 8 files changed, 231 insertions(+), 25 deletions(-) diff --git a/cmd/core-agent/commands.go b/cmd/core-agent/commands.go index 6a7f475..6cd1f14 100644 --- a/cmd/core-agent/commands.go +++ b/cmd/core-agent/commands.go @@ -3,6 +3,9 @@ package main import ( + "bytes" + "flag" + "dappco.re/go/core" ) @@ -10,6 +13,59 @@ type appCommandSet struct { core *core.Core } +// startupArgs applies early log flags, then returns args for c.Cli().Run(). +// +// args := startupArgs() +// _ = c.Cli().Run(args...) +func startupArgs() []string { + previous := flag.CommandLine + commandLine := flag.NewFlagSet("core-agent", flag.ContinueOnError) + commandLine.SetOutput(&bytes.Buffer{}) + commandLine.BoolFunc("quiet", "", func(string) error { + core.SetLevel(core.LevelError) + return nil + }) + commandLine.BoolFunc("q", "", func(string) error { + core.SetLevel(core.LevelError) + return nil + }) + commandLine.BoolFunc("debug", "", func(string) error { + core.SetLevel(core.LevelDebug) + return nil + }) + commandLine.BoolFunc("d", "", func(string) error { + core.SetLevel(core.LevelDebug) + return nil + }) + + flag.CommandLine = commandLine + defer func() { + flag.CommandLine = previous + }() + + flag.Parse() + return applyLogLevel(commandLine.Args()) +} + +// applyLogLevel strips log-level flags from args and applies the level in-order. +// +// args := applyLogLevel([]string{"version", "-q"}) +// args := applyLogLevel([]string{"--debug", "mcp"}) +func applyLogLevel(args []string) []string { + var cleaned []string + for _, arg := range args { + switch arg { + case "--quiet", "-q": + core.SetLevel(core.LevelError) + case "--debug", "-d": + core.SetLevel(core.LevelDebug) + default: + cleaned = append(cleaned, arg) + } + } + return cleaned +} + // registerAppCommands adds app-level CLI commands (version, check, env). // These are not owned by any service — they're the binary's own commands. // diff --git a/cmd/core-agent/commands_example_test.go b/cmd/core-agent/commands_example_test.go index e91f0ba..5a4d046 100644 --- a/cmd/core-agent/commands_example_test.go +++ b/cmd/core-agent/commands_example_test.go @@ -13,3 +13,10 @@ func Example_registerAppCommands() { core.Println(len(c.Commands())) // Output: 3 } + +func Example_applyLogLevel() { + args := applyLogLevel([]string{"--debug", "status"}) + + core.Println(args[0]) + // Output: status +} diff --git a/cmd/core-agent/commands_test.go b/cmd/core-agent/commands_test.go index 274874b..a8497c1 100644 --- a/cmd/core-agent/commands_test.go +++ b/cmd/core-agent/commands_test.go @@ -3,6 +3,8 @@ package main import ( + "bytes" + "os" "testing" "dappco.re/go/core" @@ -15,10 +17,63 @@ func newTestCore(t *testing.T) *core.Core { c := core.New(core.WithOption("name", "core-agent")) c.App().Version = "test" registerAppCommands(c) + c.Cli().SetOutput(&bytes.Buffer{}) return c } -// --- registerAppCommands --- +func withArgs(t *testing.T, args ...string) { + t.Helper() + previous := os.Args + os.Args = append([]string(nil), args...) + t.Cleanup(func() { + os.Args = previous + }) +} + +func TestCommands_ApplyLogLevel_Good(t *testing.T) { + defer core.SetLevel(core.LevelInfo) + + args := applyLogLevel([]string{"--quiet", "version"}) + assert.Equal(t, []string{"version"}, args) +} + +func TestCommands_ApplyLogLevel_Bad(t *testing.T) { + defer core.SetLevel(core.LevelInfo) + + args := applyLogLevel([]string{"status"}) + assert.Equal(t, []string{"status"}, args) +} + +func TestCommands_ApplyLogLevel_Ugly(t *testing.T) { + defer core.SetLevel(core.LevelInfo) + + args := applyLogLevel([]string{"version", "-q"}) + assert.Equal(t, []string{"version"}, args) +} + +func TestCommands_StartupArgs_Good(t *testing.T) { + defer core.SetLevel(core.LevelInfo) + + withArgs(t, "core-agent", "--debug", "check") + args := startupArgs() + assert.Equal(t, []string{"check"}, args) +} + +func TestCommands_StartupArgs_Bad(t *testing.T) { + defer core.SetLevel(core.LevelInfo) + + withArgs(t, "core-agent", "status") + args := startupArgs() + assert.Equal(t, []string{"status"}, args) +} + +func TestCommands_StartupArgs_Ugly(t *testing.T) { + defer core.SetLevel(core.LevelInfo) + + withArgs(t, "core-agent", "version", "-q") + args := startupArgs() + assert.Equal(t, []string{"version"}, args) +} func TestCommands_RegisterAppCommands_Good(t *testing.T) { c := newTestCore(t) @@ -28,17 +83,18 @@ func TestCommands_RegisterAppCommands_Good(t *testing.T) { assert.Contains(t, cmds, "env") } -// --- version command --- - func TestCommands_Version_Good(t *testing.T) { c := newTestCore(t) version = "0.8.0" + t.Cleanup(func() { + version = "" + }) r := c.Cli().Run("version") assert.True(t, r.OK) } -func TestCommands_Version_Bad_DevVersion(t *testing.T) { +func TestCommands_VersionDev_Bad(t *testing.T) { c := newTestCore(t) version = "" c.App().Version = "dev" @@ -47,8 +103,6 @@ func TestCommands_Version_Bad_DevVersion(t *testing.T) { assert.True(t, r.OK) } -// --- check command --- - func TestCommands_Check_Good(t *testing.T) { c := newTestCore(t) @@ -56,8 +110,6 @@ func TestCommands_Check_Good(t *testing.T) { assert.True(t, r.OK) } -// --- env command --- - func TestCommands_Env_Good(t *testing.T) { c := newTestCore(t) @@ -65,27 +117,23 @@ func TestCommands_Env_Good(t *testing.T) { assert.True(t, r.OK) } -// --- CLI resolution --- - -func TestCommands_Cli_Bad_UnknownCommand(t *testing.T) { +func TestCommands_CliUnknown_Bad(t *testing.T) { c := newTestCore(t) r := c.Cli().Run("nonexistent") assert.False(t, r.OK) } -func TestCommands_Cli_Good_Banner(t *testing.T) { +func TestCommands_CliBanner_Good(t *testing.T) { c := newTestCore(t) c.Cli().SetBanner(func(_ *core.Cli) string { return "core-agent test" }) - // No args — shows banner, returns empty Result r := c.Cli().Run() _ = r } -func TestCommands_Cli_Ugly_EmptyArgs(t *testing.T) { +func TestCommands_CliEmptyArgs_Ugly(t *testing.T) { c := newTestCore(t) - // Explicit empty slice r := c.Cli().Run() _ = r } diff --git a/cmd/core-agent/main.go b/cmd/core-agent/main.go index 2681f92..8c9530b 100644 --- a/cmd/core-agent/main.go +++ b/cmd/core-agent/main.go @@ -3,6 +3,9 @@ package main import ( + "context" + "syscall" + "dappco.re/go/core" "dappco.re/go/agent/pkg/agentic" @@ -12,7 +15,10 @@ import ( ) func main() { - newCoreAgent().Run() + if err := runCoreAgent(); err != nil { + core.Error(err.Error()) + syscall.Exit(1) + } } // newCoreAgent builds the Core app with services and CLI commands wired for startup. @@ -51,3 +57,48 @@ func appVersion() string { } return "dev" } + +// runCoreAgent builds the runtime and executes the CLI with startup flags applied. +// +// err := runCoreAgent() +func runCoreAgent() error { + return runApp(newCoreAgent(), startupArgs()) +} + +// runApp starts services, runs the CLI with explicit args, then shuts down. +// +// err := runApp(c, []string{"version"}) +func runApp(c *core.Core, cliArgs []string) error { + if c == nil { + return core.E("main.runApp", "core is required", nil) + } + + defer c.ServiceShutdown(context.Background()) + + result := c.ServiceStartup(c.Context(), nil) + if !result.OK { + return resultError("main.runApp", "startup failed", result) + } + + if cli := c.Cli(); cli != nil { + result = cli.Run(cliArgs...) + if !result.OK { + return resultError("main.runApp", "cli failed", result) + } + } + + return nil +} + +// resultError extracts the error from a Result or wraps the failure in core.E(). +// +// err := resultError("main.runApp", "startup failed", result) +func resultError(op, msg string, result core.Result) error { + if result.OK { + return nil + } + if err, ok := result.Value.(error); ok && err != nil { + return err + } + return core.E(op, msg, nil) +} diff --git a/cmd/core-agent/main_test.go b/cmd/core-agent/main_test.go index 775fc77..7900f5c 100644 --- a/cmd/core-agent/main_test.go +++ b/cmd/core-agent/main_test.go @@ -21,7 +21,7 @@ func withVersion(t *testing.T, value string) { t.Cleanup(func() { version = oldVersion }) } -func TestMain_NewCoreAgent_Good_RegistersRuntime(t *testing.T) { +func TestMain_NewCoreAgent_Good(t *testing.T) { withVersion(t, "0.15.0") c := newCoreAgent() @@ -51,7 +51,7 @@ func TestMain_NewCoreAgent_Good_RegistersRuntime(t *testing.T) { assert.True(t, ok) } -func TestMain_NewCoreAgent_Good_BannerUsesVersion(t *testing.T) { +func TestMain_NewCoreAgentBanner_Good(t *testing.T) { withVersion(t, "0.15.0") c := newCoreAgent() @@ -59,7 +59,22 @@ func TestMain_NewCoreAgent_Good_BannerUsesVersion(t *testing.T) { assert.Equal(t, "core-agent 0.15.0 — agentic orchestration for the Core ecosystem", c.Cli().Banner()) } -func TestMain_NewCoreAgent_Ugly_DevVersionFallback(t *testing.T) { +func TestMain_RunApp_Good(t *testing.T) { + withVersion(t, "0.15.0") + + assert.NoError(t, runApp(newTestCore(t), []string{"version"})) +} + +func TestMain_RunApp_Bad(t *testing.T) { + assert.EqualError(t, runApp(nil, []string{"version"}), "main.runApp: core is required") +} + +func TestMain_ResultError_Ugly(t *testing.T) { + err := resultError("main.runApp", "cli failed", core.Result{}) + assert.EqualError(t, err, "main.runApp: cli failed") +} + +func TestMain_NewCoreAgentFallback_Ugly(t *testing.T) { withVersion(t, "") c := newCoreAgent() diff --git a/cmd/core-agent/mcp_service.go b/cmd/core-agent/mcp_service.go index 34d24f4..d214ed3 100644 --- a/cmd/core-agent/mcp_service.go +++ b/cmd/core-agent/mcp_service.go @@ -10,7 +10,14 @@ import ( "forge.lthn.ai/core/mcp/pkg/mcp" ) +// registerMCPService builds the MCP service from registered AX subsystems. +// +// r := registerMCPService(c) func registerMCPService(c *core.Core) core.Result { + if c == nil { + return core.Result{Value: core.E("main.registerMCPService", "core is required", nil), OK: false} + } + var subsystems []mcp.Subsystem if prep, ok := core.ServiceFor[*agentic.PrepSubsystem](c, "agentic"); ok { diff --git a/cmd/core-agent/mcp_service_test.go b/cmd/core-agent/mcp_service_test.go index d6725de..cbf06bc 100644 --- a/cmd/core-agent/mcp_service_test.go +++ b/cmd/core-agent/mcp_service_test.go @@ -22,7 +22,14 @@ func TestMCP_RegisterMCPService_Good(t *testing.T) { assert.True(t, ok) } -func TestMCP_RegisterMCPService_Good_WithRegisteredSubsystems(t *testing.T) { +func TestMCP_RegisterMCPService_Bad(t *testing.T) { + result := registerMCPService(nil) + + require.False(t, result.OK) + assert.EqualError(t, result.Value.(error), "main.registerMCPService: core is required") +} + +func TestMCP_RegisterMCPService_Ugly(t *testing.T) { c := core.New( core.WithOption("name", "core-agent"), core.WithService(agentic.ProcessRegister), diff --git a/cmd/core-agent/update_test.go b/cmd/core-agent/update_test.go index 633c294..3200da8 100644 --- a/cmd/core-agent/update_test.go +++ b/cmd/core-agent/update_test.go @@ -10,36 +10,51 @@ import ( func TestUpdate_UpdateChannel_Good(t *testing.T) { version = "1.0.0" + t.Cleanup(func() { + version = "" + }) assert.Equal(t, "stable", updateChannel()) } -func TestUpdate_UpdateChannel_Good_Dev(t *testing.T) { +func TestUpdate_UpdateChannelDev_Good(t *testing.T) { version = "dev" + t.Cleanup(func() { + version = "" + }) assert.Equal(t, "dev", updateChannel()) } -func TestUpdate_UpdateChannel_Good_Empty(t *testing.T) { +func TestUpdate_UpdateChannelEmpty_Bad(t *testing.T) { version = "" assert.Equal(t, "dev", updateChannel()) } -func TestUpdate_UpdateChannel_Good_Prerelease(t *testing.T) { +func TestUpdate_UpdateChannelPrerelease_Ugly(t *testing.T) { version = "0.8.0-alpha" + t.Cleanup(func() { + version = "" + }) assert.Equal(t, "prerelease", updateChannel()) } -func TestUpdate_UpdateChannel_Ugly(t *testing.T) { +func TestUpdate_UpdateChannelNumericSuffix_Ugly(t *testing.T) { version = "0.8.0-beta.1" + t.Cleanup(func() { + version = "" + }) // Ends in '1' which is < 'a', so stable assert.Equal(t, "stable", updateChannel()) } func TestUpdate_AppVersion_Good(t *testing.T) { version = "1.2.3" + t.Cleanup(func() { + version = "" + }) assert.Equal(t, "1.2.3", appVersion()) } -func TestUpdate_AppVersion_Good_Empty(t *testing.T) { +func TestUpdate_AppVersion_Bad(t *testing.T) { version = "" assert.Equal(t, "dev", appVersion()) }