From e1024744a4854dfdc87bb3994bebba2215c07d54 Mon Sep 17 00:00:00 2001 From: Snider Date: Tue, 17 Mar 2026 09:00:18 +0000 Subject: [PATCH 1/2] fix(ide): replace fmt.Errorf with coreerr.E(), add unit tests MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace all fmt.Errorf calls in runtime.go with structured errors via coreerr.E() from go-log, ensuring every error carries operation context for structured logging and tracing. Add unit tests for runtime utilities (findFreePort, waitForHealth, defaultProvidersDir), RuntimeManager (List, StopAll, StartAll), ProvidersAPI (Name, BasePath, list endpoint), guiEnabled, and staticAssetGroup. Coverage: 27.1%. No os.ReadFile/os.WriteFile violations found. CLAUDE.md reviewed — no outdated commands. Co-Authored-By: Virgil --- go.mod | 5 +- main_test.go | 33 ++++++++++++ providers_test.go | 86 ++++++++++++++++++++++++++++++ runtime.go | 11 ++-- runtime_test.go | 130 ++++++++++++++++++++++++++++++++++++++++++++++ 5 files changed, 259 insertions(+), 6 deletions(-) create mode 100644 main_test.go create mode 100644 providers_test.go create mode 100644 runtime_test.go diff --git a/go.mod b/go.mod index 77c97ad..9ab29c9 100644 --- a/go.mod +++ b/go.mod @@ -11,12 +11,15 @@ require ( forge.lthn.ai/core/go-ws v0.2.3 forge.lthn.ai/core/gui v0.1.3 forge.lthn.ai/core/mcp v0.3.2 + github.com/stretchr/testify v1.11.1 github.com/wailsapp/wails/v3 v3.0.0-alpha.74 ) require ( + github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/ncruces/go-strftime v1.0.0 // indirect + github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect modernc.org/libc v1.70.0 // indirect modernc.org/mathutil v1.7.1 // indirect @@ -28,7 +31,7 @@ require ( dario.cat/mergo v1.0.2 // indirect forge.lthn.ai/core/go-ai v0.1.11 // indirect forge.lthn.ai/core/go-io v0.1.5 // indirect - forge.lthn.ai/core/go-log v0.0.4 // indirect + forge.lthn.ai/core/go-log v0.0.4 forge.lthn.ai/core/go-rag v0.1.9 // indirect forge.lthn.ai/core/go-webview v0.1.5 // indirect github.com/99designs/gqlgen v0.17.88 // indirect diff --git a/main_test.go b/main_test.go new file mode 100644 index 0000000..fd69ccd --- /dev/null +++ b/main_test.go @@ -0,0 +1,33 @@ +package main + +import ( + "testing" + + "forge.lthn.ai/core/config" + "github.com/stretchr/testify/assert" +) + +func TestGuiEnabled_Good_NilConfig(t *testing.T) { + // nil config should fall through to display detection. + result := guiEnabled(nil) + // On macOS/Windows this returns true; on Linux it depends on DISPLAY. + // Just verify it doesn't panic. + _ = result +} + +func TestGuiEnabled_Good_WithConfig(t *testing.T) { + cfg, _ := config.New() + // Fresh config has no gui.enabled key — should fall through to OS detection. + result := guiEnabled(cfg) + _ = result +} + +func TestStaticAssetGroup_Good(t *testing.T) { + s := &staticAssetGroup{ + name: "test-assets", + basePath: "/assets/test", + dir: "/tmp", + } + assert.Equal(t, "test-assets", s.Name()) + assert.Equal(t, "/assets/test", s.BasePath()) +} diff --git a/providers_test.go b/providers_test.go new file mode 100644 index 0000000..36ab5c4 --- /dev/null +++ b/providers_test.go @@ -0,0 +1,86 @@ +package main + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "forge.lthn.ai/core/api/pkg/provider" + "forge.lthn.ai/core/go-scm/manifest" + "github.com/gin-gonic/gin" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestProvidersAPI_Name(t *testing.T) { + api := NewProvidersAPI(nil, nil) + assert.Equal(t, "providers-api", api.Name()) +} + +func TestProvidersAPI_BasePath(t *testing.T) { + api := NewProvidersAPI(nil, nil) + assert.Equal(t, "/api/v1/providers", api.BasePath()) +} + +func TestProvidersAPI_List_Good_Empty(t *testing.T) { + gin.SetMode(gin.TestMode) + + reg := provider.NewRegistry() + rm := NewRuntimeManager(nil) + api := NewProvidersAPI(reg, rm) + + router := gin.New() + rg := router.Group(api.BasePath()) + api.RegisterRoutes(rg) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/providers", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp providersResponse + err := json.Unmarshal(w.Body.Bytes(), &resp) + require.NoError(t, err) + assert.Empty(t, resp.Providers) +} + +func TestProvidersAPI_List_Good_WithRuntimeProviders(t *testing.T) { + gin.SetMode(gin.TestMode) + + reg := provider.NewRegistry() + rm := NewRuntimeManager(nil) + + // Simulate a runtime provider. + rm.providers = append(rm.providers, &RuntimeProvider{ + Dir: "/tmp/test", + Port: 9999, + Manifest: &manifest.Manifest{ + Code: "test-provider", + Name: "Test Provider", + Version: "0.1.0", + Namespace: "test", + }, + }) + + api := NewProvidersAPI(reg, rm) + + router := gin.New() + rg := router.Group(api.BasePath()) + api.RegisterRoutes(rg) + + w := httptest.NewRecorder() + req, _ := http.NewRequest("GET", "/api/v1/providers", nil) + router.ServeHTTP(w, req) + + assert.Equal(t, http.StatusOK, w.Code) + + var resp providersResponse + err := json.Unmarshal(w.Body.Bytes(), &resp) + require.NoError(t, err) + require.Len(t, resp.Providers, 1) + assert.Equal(t, "test-provider", resp.Providers[0].Name) + assert.Equal(t, "test", resp.Providers[0].BasePath) + assert.Equal(t, "active", resp.Providers[0].Status) +} diff --git a/runtime.go b/runtime.go index e7b35e3..36358a4 100644 --- a/runtime.go +++ b/runtime.go @@ -15,6 +15,7 @@ import ( "forge.lthn.ai/core/api" "forge.lthn.ai/core/api/pkg/provider" + coreerr "forge.lthn.ai/core/go-log" "forge.lthn.ai/core/go-scm/manifest" "forge.lthn.ai/core/go-scm/marketplace" "github.com/gin-gonic/gin" @@ -63,7 +64,7 @@ func (rm *RuntimeManager) StartAll(ctx context.Context) error { dir := defaultProvidersDir() discovered, err := marketplace.DiscoverProviders(dir) if err != nil { - return fmt.Errorf("runtime: discover providers: %w", err) + return coreerr.E("runtime.StartAll", "discover providers", err) } if len(discovered) == 0 { @@ -93,7 +94,7 @@ func (rm *RuntimeManager) startProvider(ctx context.Context, dp marketplace.Disc // Assign a free port. port, err := findFreePort() if err != nil { - return nil, fmt.Errorf("find free port: %w", err) + return nil, coreerr.E("runtime.startProvider", "find free port", err) } // Resolve binary path. @@ -114,7 +115,7 @@ func (rm *RuntimeManager) startProvider(ctx context.Context, dp marketplace.Disc cmd.Stderr = os.Stderr if err := cmd.Start(); err != nil { - return nil, fmt.Errorf("start binary %s: %w", binaryPath, err) + return nil, coreerr.E("runtime.startProvider", fmt.Sprintf("start binary %s", binaryPath), err) } // Wait for health check. @@ -122,7 +123,7 @@ func (rm *RuntimeManager) startProvider(ctx context.Context, dp marketplace.Disc if err := waitForHealth(healthURL, 10*time.Second); err != nil { // Kill the process if health check fails. _ = cmd.Process.Kill() - return nil, fmt.Errorf("health check failed for %s: %w", m.Code, err) + return nil, coreerr.E("runtime.startProvider", fmt.Sprintf("health check failed for %s", m.Code), err) } // Register proxy provider. @@ -248,7 +249,7 @@ func waitForHealth(url string, timeout time.Duration) error { time.Sleep(100 * time.Millisecond) } - return fmt.Errorf("health check timed out after %s: %s", timeout, url) + return coreerr.E("runtime.waitForHealth", fmt.Sprintf("timed out after %s: %s", timeout, url), nil) } // staticAssetGroup is a simple RouteGroup that serves static files. diff --git a/runtime_test.go b/runtime_test.go new file mode 100644 index 0000000..ae596eb --- /dev/null +++ b/runtime_test.go @@ -0,0 +1,130 @@ +package main + +import ( + "context" + "net/http" + "net/http/httptest" + "os/exec" + "testing" + "time" + + "forge.lthn.ai/core/go-scm/manifest" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestFindFreePort_Good(t *testing.T) { + port, err := findFreePort() + require.NoError(t, err) + assert.Greater(t, port, 0) + assert.Less(t, port, 65536) +} + +func TestFindFreePort_UniquePerCall(t *testing.T) { + port1, err := findFreePort() + require.NoError(t, err) + port2, err := findFreePort() + require.NoError(t, err) + // Two consecutive calls should very likely return different ports. + // (Not guaranteed, but effectively always true.) + assert.NotEqual(t, port1, port2) +} + +func TestWaitForHealth_Good(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusOK) + })) + defer srv.Close() + + err := waitForHealth(srv.URL, 5*time.Second) + assert.NoError(t, err) +} + +func TestWaitForHealth_Bad_Timeout(t *testing.T) { + srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + })) + defer srv.Close() + + err := waitForHealth(srv.URL, 500*time.Millisecond) + require.Error(t, err) + assert.Contains(t, err.Error(), "timed out") +} + +func TestWaitForHealth_Bad_NoServer(t *testing.T) { + err := waitForHealth("http://127.0.0.1:1", 500*time.Millisecond) + require.Error(t, err) + assert.Contains(t, err.Error(), "timed out") +} + +func TestDefaultProvidersDir_Good(t *testing.T) { + dir := defaultProvidersDir() + assert.Contains(t, dir, ".core") + assert.Contains(t, dir, "providers") +} + +func TestRuntimeManager_List_Good_Empty(t *testing.T) { + rm := NewRuntimeManager(nil) + infos := rm.List() + assert.Empty(t, infos) +} + +func TestRuntimeManager_List_Good_WithProviders(t *testing.T) { + rm := NewRuntimeManager(nil) + rm.providers = []*RuntimeProvider{ + { + Dir: "/tmp/test-provider", + Port: 12345, + Manifest: &manifest.Manifest{ + Code: "test-svc", + Name: "Test Service", + Version: "1.0.0", + Namespace: "test", + }, + }, + } + + infos := rm.List() + require.Len(t, infos, 1) + assert.Equal(t, "test-svc", infos[0].Code) + assert.Equal(t, "Test Service", infos[0].Name) + assert.Equal(t, "1.0.0", infos[0].Version) + assert.Equal(t, "test", infos[0].Namespace) + assert.Equal(t, 12345, infos[0].Port) + assert.Equal(t, "/tmp/test-provider", infos[0].Dir) +} + +func TestRuntimeManager_StopAll_Good_Empty(t *testing.T) { + rm := NewRuntimeManager(nil) + // Should not panic with no providers. + rm.StopAll() + assert.Empty(t, rm.providers) +} + +func TestRuntimeManager_StopAll_Good_WithProcess(t *testing.T) { + // Start a real process so we can test graceful stop. + cmd := exec.CommandContext(context.Background(), "sleep", "60") + require.NoError(t, cmd.Start()) + + rm := NewRuntimeManager(nil) + rm.providers = []*RuntimeProvider{ + { + Manifest: &manifest.Manifest{Code: "sleeper"}, + Cmd: cmd, + }, + } + + rm.StopAll() + assert.Nil(t, rm.providers) +} + +func TestRuntimeManager_StartAll_Good_EmptyDir(t *testing.T) { + rm := NewRuntimeManager(nil) + // StartAll with a non-existent providers dir should return an error + // because the default dir won't have providers (at most it logs and returns nil). + err := rm.StartAll(context.Background()) + // Depending on whether ~/.core/providers/ exists, this either returns + // nil (no providers found) or an error (dir doesn't exist). + // Either outcome is acceptable — no panic. + _ = err +} From cc7504892fa0abc40b13471d60e3b72ab94607fe Mon Sep 17 00:00:00 2001 From: Claude Date: Sat, 21 Mar 2026 23:55:49 +0000 Subject: [PATCH 2/2] chore: migrate to dappco.re vanity import path Change module declaration from forge.lthn.ai/core/ide to dappco.re/go/core/ide. Update the self-referencing icons import accordingly. External dependency imports remain on forge.lthn.ai paths until those sibling modules publish dappco.re-aware versions, avoiding dual-path conflicts in the Go module graph. Co-Authored-By: Claude Opus 4.6 (1M context) --- go.mod | 2 +- main.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index 9ab29c9..98055c3 100644 --- a/go.mod +++ b/go.mod @@ -1,4 +1,4 @@ -module forge.lthn.ai/core/ide +module dappco.re/go/core/ide go 1.26.0 diff --git a/main.go b/main.go index 7637e6b..9220d4a 100644 --- a/main.go +++ b/main.go @@ -20,7 +20,7 @@ import ( "forge.lthn.ai/core/go/pkg/core" guiMCP "forge.lthn.ai/core/gui/pkg/mcp" "forge.lthn.ai/core/gui/pkg/display" - "forge.lthn.ai/core/ide/icons" + "dappco.re/go/core/ide/icons" "forge.lthn.ai/core/mcp/pkg/mcp" "forge.lthn.ai/core/mcp/pkg/mcp/agentic" "forge.lthn.ai/core/mcp/pkg/mcp/brain"