From c45b22849f300530f6c6cd63a03c2e9133a7e76f Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 24 Mar 2026 16:23:33 +0000 Subject: [PATCH] feat: restore functional option pattern for New() MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New() returns Result, accepts CoreOption functionals. Restores v0.3.3 service registration contract: - WithService(factory func(*Core) Result) — service factory receives Core - WithOptions(Options) — key-value configuration - WithServiceLock() — immutable after construction Services registered in New() form the application conclave with shared IPC access. Each Core instance has its own bus scope. Co-Authored-By: Virgil --- app_test.go | 6 ++-- cli_test.go | 18 +++++------ command_test.go | 34 ++++++++++---------- config_test.go | 16 +++++----- contract.go | 83 +++++++++++++++++++++++++++++++++++++++---------- core_test.go | 60 ++++++++++++++++++++++++++++------- data_test.go | 28 ++++++++--------- drive_test.go | 14 ++++----- error_test.go | 24 +++++++------- fs_test.go | 46 +++++++++++++-------------- i18n_test.go | 18 +++++------ info_test.go | 2 +- ipc_test.go | 14 ++++----- lock_test.go | 12 +++---- log_test.go | 2 +- runtime.go | 6 +++- runtime_test.go | 6 ++-- service_test.go | 14 ++++----- task_test.go | 14 ++++----- 19 files changed, 255 insertions(+), 162 deletions(-) diff --git a/app_test.go b/app_test.go index dc85bff..406cdb4 100644 --- a/app_test.go +++ b/app_test.go @@ -10,18 +10,18 @@ import ( // --- App --- func TestApp_Good(t *testing.T) { - c := New(Options{{Key: "name", Value: "myapp"}}) + c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core) assert.Equal(t, "myapp", c.App().Name) } func TestApp_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) } diff --git a/cli_test.go b/cli_test.go index 46f01de..f29a467 100644 --- a/cli_test.go +++ b/cli_test.go @@ -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(Options{{Key: "name", Value: "myapp"}}) + c := New(WithOptions(Options{{Key: "name", Value: "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(Options{{Key: "name", Value: "myapp"}}) + c := New(WithOptions(Options{{Key: "name", Value: "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") diff --git a/command_test.go b/command_test.go index 57966a5..3e0bb91 100644 --- a/command_test.go +++ b/command_test.go @@ -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(Options{}) @@ -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,21 +82,21 @@ 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()) @@ -105,7 +105,7 @@ func TestCommand_I18nKey_Simple_Good(t *testing.T) { // --- Lifecycle --- func TestCommand_Lifecycle_NoImpl_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Command("serve", Command{Action: func(_ Options) Result { return Result{Value: "running", OK: true} }}) @@ -153,7 +153,7 @@ func (l *testLifecycle) Signal(sig string) Result { } func TestCommand_Lifecycle_WithImpl_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) lc := &testLifecycle{} c.Command("daemon", Command{Lifecycle: lc}) cmd := c.Command("daemon").Value.(*Command) @@ -177,14 +177,14 @@ func TestCommand_Lifecycle_WithImpl_Good(t *testing.T) { } 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) @@ -193,7 +193,7 @@ func TestCommand_InvalidPath_Bad(t *testing.T) { // --- Cli Run with Lifecycle --- func TestCli_Run_Lifecycle_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) lc := &testLifecycle{} c.Command("serve", Command{Lifecycle: lc}) r := c.Cli().Run("serve") @@ -202,7 +202,7 @@ func TestCli_Run_Lifecycle_Good(t *testing.T) { } func TestCli_Run_NoActionNoLifecycle_Bad(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Command("empty", Command{}) r := c.Cli().Run("empty") assert.False(t, r.OK) @@ -211,7 +211,7 @@ func TestCli_Run_NoActionNoLifecycle_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) } diff --git a/config_test.go b/config_test.go index 47a5836..b669e60 100644 --- a/config_test.go +++ b/config_test.go @@ -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") diff --git a/contract.go b/contract.go index b5fd099..ddd9904 100644 --- a/contract.go +++ b/contract.go @@ -66,12 +66,28 @@ type ActionTaskCompleted struct { // --- Constructor --- -// New creates a Core instance. +// CoreOption is a functional option applied during Core construction. +// Returns Result — if !OK, New() stops and returns the error. // -// c := core.New(core.Options{ -// {Key: "name", Value: "myapp"}, -// }) -func New(opts ...Options) *Core { +// core.New( +// core.WithService(agentic.Register), +// core.WithService(monitor.Register), +// core.WithServiceLock(), +// ) +type CoreOption func(*Core) Result + +// New initialises a Core instance by applying options in order. +// Services registered here form the application conclave — they share +// IPC access and participate in the lifecycle (ServiceStartup/ServiceShutdown). +// +// r := core.New( +// core.WithOptions(core.Options{{Key: "name", Value: "myapp"}}), +// core.WithService(auth.Register), +// core.WithServiceLock(), +// ) +// if !r.OK { log.Fatal(r.Value) } +// c := r.Value.(*Core) +func New(opts ...CoreOption) Result { c := &Core{ app: &App{}, data: &Data{}, @@ -88,19 +104,54 @@ func New(opts ...Options) *Core { commands: &commandRegistry{commands: make(map[string]*Command)}, } c.context, c.cancel = context.WithCancel(context.Background()) + c.cli = &Cli{core: c} - if len(opts) > 0 { - cp := make(Options, len(opts[0])) - copy(cp, opts[0]) - c.options = &cp - name := cp.String("name") - if name != "" { - c.app.Name = name + for _, opt := range opts { + if r := opt(c); !r.OK { + return r } } - // Init Cli surface with Core reference - c.cli = &Cli{core: c} - - return c + return Result{c, true} +} + +// WithOptions applies key-value configuration to Core. +// +// core.WithOptions(core.Options{{Key: "name", Value: "myapp"}}) +func WithOptions(opts Options) CoreOption { + return func(c *Core) Result { + c.options = &opts + if name := opts.String("name"); name != "" { + c.app.Name = name + } + return Result{OK: true} + } +} + +// WithService registers a service via its factory function. +// The factory receives *Core so the service can wire IPC handlers +// and access other subsystems during construction. +// Service name is auto-discovered from the package path. +// If the service implements HandleIPCEvents, it is auto-registered. +// +// core.WithService(agentic.Register) +// core.WithService(display.Register(nil)) +func WithService(factory func(*Core) Result) CoreOption { + return func(c *Core) Result { + return factory(c) + } +} + +// WithServiceLock prevents further service registration after construction. +// +// core.New( +// core.WithService(auth.Register), +// core.WithServiceLock(), +// ) +func WithServiceLock() CoreOption { + return func(c *Core) Result { + c.LockEnable() + c.LockApply() + return Result{OK: true} + } } diff --git a/core_test.go b/core_test.go index 3b5a68c..4cf6c29 100644 --- a/core_test.go +++ b/core_test.go @@ -1,6 +1,7 @@ package core_test import ( + "context" "testing" . "dappco.re/go/core" @@ -10,26 +11,63 @@ import ( // --- New --- func TestNew_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) assert.NotNil(t, c) } func TestNew_WithOptions_Good(t *testing.T) { - c := New(Options{{Key: "name", Value: "myapp"}}) + c := New(WithOptions(Options{{Key: "name", Value: "myapp"}})).Value.(*Core) assert.NotNil(t, c) assert.Equal(t, "myapp", c.App().Name) } func TestNew_WithOptions_Bad(t *testing.T) { // Empty options — should still create a valid Core - c := New(Options{}) + c := New(WithOptions(Options{})).Value.(*Core) assert.NotNil(t, c) } +func TestNew_WithService_Good(t *testing.T) { + started := false + r := New( + WithOptions(Options{{Key: "name", Value: "myapp"}}), + WithService(func(c *Core) Result { + c.Service("test", Service{ + OnStart: func() Result { started = true; return Result{OK: true} }, + }) + return Result{OK: true} + }), + ) + assert.True(t, r.OK) + c := r.Value.(*Core) + + svc := c.Service("test") + assert.True(t, svc.OK) + + c.ServiceStartup(context.Background(), nil) + assert.True(t, started) +} + +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) +} + // --- Accessors --- func TestAccessors_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) assert.NotNil(t, c.App()) assert.NotNil(t, c.Data()) assert.NotNil(t, c.Drive()) @@ -44,11 +82,11 @@ func TestAccessors_Good(t *testing.T) { } func TestOptions_Accessor_Good(t *testing.T) { - c := New(Options{ + c := New(WithOptions(Options{ {Key: "name", Value: "testapp"}, {Key: "port", Value: 8080}, {Key: "debug", Value: true}, - }) + })).Value.(*Core) opts := c.Options() assert.NotNil(t, opts) assert.Equal(t, "testapp", opts.String("name")) @@ -57,7 +95,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()) } @@ -65,7 +103,7 @@ 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) @@ -75,7 +113,7 @@ func TestCore_LogError_Good(t *testing.T) { } 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) @@ -83,14 +121,14 @@ func TestCore_LogWarn_Good(t *testing.T) { } 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") }) diff --git a/data_test.go b/data_test.go index ffbccea..81ade81 100644 --- a/data_test.go +++ b/data_test.go @@ -15,7 +15,7 @@ var testFS embed.FS // --- Data (Embedded Content Mounts) --- func TestData_New_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) r := c.Data().New(Options{ {Key: "name", Value: "test"}, {Key: "source", Value: testFS}, @@ -26,7 +26,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(Options{{Key: "source", Value: testFS}}) assert.False(t, r.OK) @@ -39,7 +39,7 @@ func TestData_New_Bad(t *testing.T) { } func TestData_ReadString_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) r := c.Data().ReadString("app/test.txt") assert.True(t, r.OK) @@ -47,13 +47,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) c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) r := c.Data().ReadFile("app/test.txt") assert.True(t, r.OK) @@ -61,7 +61,7 @@ func TestData_ReadFile_Good(t *testing.T) { } func TestData_Get_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Data().New(Options{{Key: "name", Value: "brain"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) gr := c.Data().Get("brain") assert.True(t, gr.OK) @@ -76,13 +76,13 @@ func TestData_Get_Good(t *testing.T) { } 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) c.Data().New(Options{{Key: "name", Value: "a"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) c.Data().New(Options{{Key: "name", Value: "b"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) mounts := c.Data().Mounts() @@ -90,26 +90,26 @@ func TestData_Mounts_Good(t *testing.T) { } func TestEmbed_Legacy_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "testdata"}}) assert.NotNil(t, c.Embed()) } func TestData_List_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}}) r := c.Data().List("app/testdata") assert.True(t, r.OK) } func TestData_List_Bad(t *testing.T) { - c := New() + 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) c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}}) r := c.Data().ListNames("app/testdata") assert.True(t, r.OK) @@ -117,14 +117,14 @@ func TestData_ListNames_Good(t *testing.T) { } func TestData_Extract_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Data().New(Options{{Key: "name", Value: "app"}, {Key: "source", Value: testFS}, {Key: "path", Value: "."}}) r := c.Data().Extract("app/testdata", t.TempDir(), nil) 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) } diff --git a/drive_test.go b/drive_test.go index 8152876..afd6a34 100644 --- a/drive_test.go +++ b/drive_test.go @@ -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(Options{ {Key: "name", Value: "api"}, {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(Options{ {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(Options{ {Key: "name", Value: "ssh"}, {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(Options{{Key: "name", Value: "mcp"}, {Key: "transport", Value: "mcp://mcp.lthn.sh"}}) assert.True(t, c.Drive().Has("mcp")) assert.False(t, c.Drive().Has("missing")) } func TestDrive_Names_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Drive().New(Options{{Key: "name", Value: "api"}, {Key: "transport", Value: "https://api.lthn.ai"}}) c.Drive().New(Options{{Key: "name", Value: "ssh"}, {Key: "transport", Value: "ssh://claude@10.69.69.165"}}) c.Drive().New(Options{{Key: "name", Value: "mcp"}, {Key: "transport", Value: "mcp://mcp.lthn.sh"}}) @@ -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(Options{ {Key: "name", Value: "api"}, {Key: "transport", Value: "https://api.lthn.ai"}, diff --git a/error_test.go b/error_test.go index 7213486..1f36264 100644 --- a/error_test.go +++ b/error_test.go @@ -102,7 +102,7 @@ func TestFormatStackTrace_Good(t *testing.T) { // --- ErrorLog --- func TestErrorLog_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) cause := errors.New("boom") r := c.Log().Error(cause, "test.Operation", "something broke") assert.False(t, r.OK) @@ -110,27 +110,27 @@ func TestErrorLog_Good(t *testing.T) { } func TestErrorLog_Nil_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) r := c.Log().Error(nil, "test.Operation", "no error") assert.True(t, r.OK) } func TestErrorLog_Warn_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) cause := errors.New("warning") r := c.Log().Warn(cause, "test.Operation", "heads up") assert.False(t, r.OK) } func TestErrorLog_Must_Ugly(t *testing.T) { - c := New() + c := New().Value.(*Core) assert.Panics(t, func() { c.Log().Must(errors.New("fatal"), "test.Operation", "must fail") }) } func TestErrorLog_Must_Nil_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) assert.NotPanics(t, func() { c.Log().Must(nil, "test.Operation", "no error") }) @@ -139,7 +139,7 @@ func TestErrorLog_Must_Nil_Good(t *testing.T) { // --- ErrorPanic --- func TestErrorPanic_Recover_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) // Should not panic — Recover catches it assert.NotPanics(t, func() { defer c.Error().Recover() @@ -148,7 +148,7 @@ func TestErrorPanic_Recover_Good(t *testing.T) { } func TestErrorPanic_SafeGo_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) done := make(chan bool, 1) c.Error().SafeGo(func() { done <- true @@ -157,7 +157,7 @@ func TestErrorPanic_SafeGo_Good(t *testing.T) { } func TestErrorPanic_SafeGo_Panic_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) done := make(chan bool, 1) c.Error().SafeGo(func() { defer func() { done <- true }() @@ -202,7 +202,7 @@ func TestErrorPanic_Reports_Good(t *testing.T) { 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 @@ -221,7 +221,7 @@ func TestErrorPanic_CrashFile_Good(t *testing.T) { // 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) @@ -260,13 +260,13 @@ func TestWrap_PreservesCode_Good(t *testing.T) { } func TestErrorLog_Warn_Nil_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) r := c.LogWarn(nil, "op", "msg") assert.True(t, r.OK) } func TestErrorLog_Error_Nil_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) r := c.LogError(nil, "op", "msg") assert.True(t, r.OK) } diff --git a/fs_test.go b/fs_test.go index 99160b9..7982802 100644 --- a/fs_test.go +++ b/fs_test.go @@ -15,7 +15,7 @@ import ( func TestFs_WriteRead_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "test.txt") assert.True(t, c.Fs().Write(path, "hello core").OK) @@ -26,21 +26,21 @@ 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() + 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(filepath.Join(dir, "nonexistent"))) @@ -49,7 +49,7 @@ func TestFs_IsDir_Good(t *testing.T) { func TestFs_IsFile_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "test.txt") c.Fs().Write(path, "data") assert.True(t, c.Fs().IsFile(path)) @@ -59,7 +59,7 @@ func TestFs_IsFile_Good(t *testing.T) { func TestFs_Exists_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "exists.txt") c.Fs().Write(path, "yes") assert.True(t, c.Fs().Exists(path)) @@ -69,7 +69,7 @@ func TestFs_Exists_Good(t *testing.T) { func TestFs_List_Good(t *testing.T) { dir := t.TempDir() - c := New() + 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) @@ -79,7 +79,7 @@ func TestFs_List_Good(t *testing.T) { func TestFs_Stat_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "stat.txt") c.Fs().Write(path, "data") r := c.Fs().Stat(path) @@ -89,7 +89,7 @@ func TestFs_Stat_Good(t *testing.T) { func TestFs_Open_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "open.txt") c.Fs().Write(path, "content") r := c.Fs().Open(path) @@ -99,7 +99,7 @@ func TestFs_Open_Good(t *testing.T) { func TestFs_Create_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "sub", "created.txt") r := c.Fs().Create(path) assert.True(t, r.OK) @@ -112,7 +112,7 @@ func TestFs_Create_Good(t *testing.T) { func TestFs_Append_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "append.txt") c.Fs().Write(path, "first") r := c.Fs().Append(path) @@ -126,7 +126,7 @@ func TestFs_Append_Good(t *testing.T) { func TestFs_ReadStream_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "stream.txt") c.Fs().Write(path, "streamed") r := c.Fs().ReadStream(path) @@ -136,7 +136,7 @@ func TestFs_ReadStream_Good(t *testing.T) { func TestFs_WriteStream_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "sub", "ws.txt") r := c.Fs().WriteStream(path) assert.True(t, r.OK) @@ -147,7 +147,7 @@ func TestFs_WriteStream_Good(t *testing.T) { func TestFs_Delete_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) path := filepath.Join(dir, "delete.txt") c.Fs().Write(path, "gone") assert.True(t, c.Fs().Delete(path).OK) @@ -156,7 +156,7 @@ func TestFs_Delete_Good(t *testing.T) { func TestFs_DeleteAll_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) sub := filepath.Join(dir, "deep", "nested") c.Fs().EnsureDir(sub) c.Fs().Write(filepath.Join(sub, "file.txt"), "data") @@ -166,7 +166,7 @@ func TestFs_DeleteAll_Good(t *testing.T) { func TestFs_Rename_Good(t *testing.T) { dir := t.TempDir() - c := New() + c := New().Value.(*Core) old := filepath.Join(dir, "old.txt") nw := filepath.Join(dir, "new.txt") c.Fs().Write(old, "data") @@ -177,7 +177,7 @@ func TestFs_Rename_Good(t *testing.T) { func TestFs_WriteMode_Good(t *testing.T) { dir := t.TempDir() - c := New() + 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) @@ -213,39 +213,39 @@ 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() + c := New().Value.(*Core) path := filepath.Join(dir, "stream.txt") c.Fs().Write(path, "streamed") diff --git a/i18n_test.go b/i18n_test.go index 4520721..3e6d8ca 100644 --- a/i18n_test.go +++ b/i18n_test.go @@ -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(Options{ {Key: "name", Value: "lang"}, {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) } @@ -82,7 +82,7 @@ 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) diff --git a/info_test.go b/info_test.go index 5f09db7..2a03369 100644 --- a/info_test.go +++ b/info_test.go @@ -89,7 +89,7 @@ func TestEnv_Unknown(t *testing.T) { } func TestEnv_CoreInstance(t *testing.T) { - c := core.New() + 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")) } diff --git a/ipc_test.go b/ipc_test.go index 7977bbb..005ef6a 100644 --- a/ipc_test.go +++ b/ipc_test.go @@ -12,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 @@ -24,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) @@ -33,7 +33,7 @@ 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) @@ -42,7 +42,7 @@ func TestAction_None_Good(t *testing.T) { // --- IPC: Queries --- func TestQuery_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.RegisterQuery(func(_ *Core, q Query) Result { if q == "ping" { return Result{Value: "pong", OK: true} @@ -55,7 +55,7 @@ func TestQuery_Good(t *testing.T) { } func TestQuery_Unhandled_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.RegisterQuery(func(_ *Core, q Query) Result { return Result{} }) @@ -64,7 +64,7 @@ func TestQuery_Unhandled_Good(t *testing.T) { } func TestQueryAll_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.RegisterQuery(func(_ *Core, _ Query) Result { return Result{Value: "a", OK: true} }) @@ -82,7 +82,7 @@ func TestQueryAll_Good(t *testing.T) { // --- IPC: Tasks --- func TestPerform_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.RegisterTask(func(_ *Core, t Task) Result { if t == "compute" { return Result{Value: 42, OK: true} diff --git a/lock_test.go b/lock_test.go index 1c96e42..93b574a 100644 --- a/lock_test.go +++ b/lock_test.go @@ -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 TestLockEnable_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Service("early", Service{}) c.LockEnable() c.LockApply() @@ -39,7 +39,7 @@ func TestLockEnable_Good(t *testing.T) { } func TestStartables_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Service("s", Service{OnStart: func() Result { return Result{OK: true} }}) r := c.Startables() assert.True(t, r.OK) @@ -47,7 +47,7 @@ func TestStartables_Good(t *testing.T) { } func TestStoppables_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.Service("s", Service{OnStop: func() Result { return Result{OK: true} }}) r := c.Stoppables() assert.True(t, r.OK) diff --git a/log_test.go b/log_test.go index 70e6103..60b6f6c 100644 --- a/log_test.go +++ b/log_test.go @@ -54,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()) } diff --git a/runtime.go b/runtime.go index 952001d..3e48afb 100644 --- a/runtime.go +++ b/runtime.go @@ -106,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(Options{{Key: "name", Value: "core"}}) + r := New(WithOptions(Options{{Key: "name", Value: "core"}})) + if !r.OK { + return r + } + c := r.Value.(*Core) c.app.Runtime = app names := slices.Sorted(maps.Keys(factories)) diff --git a/runtime_test.go b/runtime_test.go index 2d18f56..3da01e5 100644 --- a/runtime_test.go +++ b/runtime_test.go @@ -16,7 +16,7 @@ type testOpts struct { } func TestServiceRuntime_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) opts := testOpts{URL: "https://api.lthn.ai", Timeout: 30} rt := NewServiceRuntime(c, opts) @@ -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()) diff --git a/service_test.go b/service_test.go index 0b1a609..ddd32fd 100644 --- a/service_test.go +++ b/service_test.go @@ -10,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) @@ -37,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() @@ -55,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{ diff --git a/task_test.go b/task_test.go index 37876ad..5e70efd 100644 --- a/task_test.go +++ b/task_test.go @@ -13,7 +13,7 @@ import ( // --- PerformAsync --- func TestPerformAsync_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) var mu sync.Mutex var result string @@ -37,7 +37,7 @@ func TestPerformAsync_Good(t *testing.T) { } func TestPerformAsync_Progress_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) c.RegisterTask(func(_ *Core, task Task) Result { return Result{OK: true} }) @@ -48,7 +48,7 @@ func TestPerformAsync_Progress_Good(t *testing.T) { } func TestPerformAsync_Completion_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) completed := make(chan ActionTaskCompleted, 1) c.RegisterTask(func(_ *Core, task Task) Result { @@ -73,7 +73,7 @@ func TestPerformAsync_Completion_Good(t *testing.T) { } func TestPerformAsync_NoHandler_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) completed := make(chan ActionTaskCompleted, 1) c.RegisterAction(func(_ *Core, msg Message) Result { @@ -94,7 +94,7 @@ func TestPerformAsync_NoHandler_Good(t *testing.T) { } func TestPerformAsync_AfterShutdown_Bad(t *testing.T) { - c := New() + c := New().Value.(*Core) c.ServiceStartup(context.Background(), nil) c.ServiceShutdown(context.Background()) @@ -105,7 +105,7 @@ func TestPerformAsync_AfterShutdown_Bad(t *testing.T) { // --- RegisterAction + RegisterActions --- func TestRegisterAction_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) called := false c.RegisterAction(func(_ *Core, _ Message) Result { called = true @@ -116,7 +116,7 @@ func TestRegisterAction_Good(t *testing.T) { } func TestRegisterActions_Good(t *testing.T) { - c := New() + c := New().Value.(*Core) count := 0 h := func(_ *Core, _ Message) Result { count++; return Result{OK: true} } c.RegisterActions(h, h)