fix(ide): replace fmt.Errorf with coreerr.E(), add unit tests
Some checks failed
Security Scan / security (pull_request) Successful in 11s
Test / test (pull_request) Failing after 1m56s

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 <virgil@lethean.io>
This commit is contained in:
Snider 2026-03-17 09:00:18 +00:00
parent 5efd2c69d4
commit e1024744a4
5 changed files with 259 additions and 6 deletions

5
go.mod
View file

@ -11,12 +11,15 @@ require (
forge.lthn.ai/core/go-ws v0.2.3 forge.lthn.ai/core/go-ws v0.2.3
forge.lthn.ai/core/gui v0.1.3 forge.lthn.ai/core/gui v0.1.3
forge.lthn.ai/core/mcp v0.3.2 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 github.com/wailsapp/wails/v3 v3.0.0-alpha.74
) )
require ( require (
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
github.com/dustin/go-humanize v1.0.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect
github.com/ncruces/go-strftime v1.0.0 // 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 github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
modernc.org/libc v1.70.0 // indirect modernc.org/libc v1.70.0 // indirect
modernc.org/mathutil v1.7.1 // indirect modernc.org/mathutil v1.7.1 // indirect
@ -28,7 +31,7 @@ require (
dario.cat/mergo v1.0.2 // indirect dario.cat/mergo v1.0.2 // indirect
forge.lthn.ai/core/go-ai v0.1.11 // 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-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-rag v0.1.9 // indirect
forge.lthn.ai/core/go-webview v0.1.5 // indirect forge.lthn.ai/core/go-webview v0.1.5 // indirect
github.com/99designs/gqlgen v0.17.88 // indirect github.com/99designs/gqlgen v0.17.88 // indirect

33
main_test.go Normal file
View file

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

86
providers_test.go Normal file
View file

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

View file

@ -15,6 +15,7 @@ import (
"forge.lthn.ai/core/api" "forge.lthn.ai/core/api"
"forge.lthn.ai/core/api/pkg/provider" "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/manifest"
"forge.lthn.ai/core/go-scm/marketplace" "forge.lthn.ai/core/go-scm/marketplace"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
@ -63,7 +64,7 @@ func (rm *RuntimeManager) StartAll(ctx context.Context) error {
dir := defaultProvidersDir() dir := defaultProvidersDir()
discovered, err := marketplace.DiscoverProviders(dir) discovered, err := marketplace.DiscoverProviders(dir)
if err != nil { if err != nil {
return fmt.Errorf("runtime: discover providers: %w", err) return coreerr.E("runtime.StartAll", "discover providers", err)
} }
if len(discovered) == 0 { if len(discovered) == 0 {
@ -93,7 +94,7 @@ func (rm *RuntimeManager) startProvider(ctx context.Context, dp marketplace.Disc
// Assign a free port. // Assign a free port.
port, err := findFreePort() port, err := findFreePort()
if err != nil { 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. // Resolve binary path.
@ -114,7 +115,7 @@ func (rm *RuntimeManager) startProvider(ctx context.Context, dp marketplace.Disc
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
if err := cmd.Start(); err != nil { 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. // 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 { if err := waitForHealth(healthURL, 10*time.Second); err != nil {
// Kill the process if health check fails. // Kill the process if health check fails.
_ = cmd.Process.Kill() _ = 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. // Register proxy provider.
@ -248,7 +249,7 @@ func waitForHealth(url string, timeout time.Duration) error {
time.Sleep(100 * time.Millisecond) 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. // staticAssetGroup is a simple RouteGroup that serves static files.

130
runtime_test.go Normal file
View file

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