Skip to content

Commit 800b75e

Browse files
committed
Merge remote-tracking branch 'origin/main'
2 parents 72ab026 + cc75048 commit 800b75e

6 files changed

Lines changed: 259 additions & 7 deletions

File tree

go.mod

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
module forge.lthn.ai/core/ide
1+
module dappco.re/go/core/ide
22

33
go 1.26.0
44

@@ -15,8 +15,10 @@ require (
1515
)
1616

1717
require (
18+
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect
1819
github.com/dustin/go-humanize v1.0.1 // indirect
1920
github.com/ncruces/go-strftime v1.0.0 // indirect
21+
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect
2022
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
2123
modernc.org/libc v1.70.0 // indirect
2224
modernc.org/mathutil v1.7.1 // indirect

main.go

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ import (
2020
"forge.lthn.ai/core/go/pkg/core"
2121
guiMCP "forge.lthn.ai/core/gui/pkg/mcp"
2222
"forge.lthn.ai/core/gui/pkg/display"
23-
"forge.lthn.ai/core/ide/icons"
23+
"dappco.re/go/core/ide/icons"
2424
"forge.lthn.ai/core/mcp/pkg/mcp"
2525
"forge.lthn.ai/core/mcp/pkg/mcp/agentic"
2626
"forge.lthn.ai/core/mcp/pkg/mcp/brain"

main_test.go

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
package main
2+
3+
import (
4+
"testing"
5+
6+
"forge.lthn.ai/core/config"
7+
"github.com/stretchr/testify/assert"
8+
)
9+
10+
func TestGuiEnabled_Good_NilConfig(t *testing.T) {
11+
// nil config should fall through to display detection.
12+
result := guiEnabled(nil)
13+
// On macOS/Windows this returns true; on Linux it depends on DISPLAY.
14+
// Just verify it doesn't panic.
15+
_ = result
16+
}
17+
18+
func TestGuiEnabled_Good_WithConfig(t *testing.T) {
19+
cfg, _ := config.New()
20+
// Fresh config has no gui.enabled key — should fall through to OS detection.
21+
result := guiEnabled(cfg)
22+
_ = result
23+
}
24+
25+
func TestStaticAssetGroup_Good(t *testing.T) {
26+
s := &staticAssetGroup{
27+
name: "test-assets",
28+
basePath: "/assets/test",
29+
dir: "/tmp",
30+
}
31+
assert.Equal(t, "test-assets", s.Name())
32+
assert.Equal(t, "/assets/test", s.BasePath())
33+
}

providers_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package main
2+
3+
import (
4+
"encoding/json"
5+
"net/http"
6+
"net/http/httptest"
7+
"testing"
8+
9+
"forge.lthn.ai/core/api/pkg/provider"
10+
"forge.lthn.ai/core/go-scm/manifest"
11+
"github.com/gin-gonic/gin"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestProvidersAPI_Name(t *testing.T) {
17+
api := NewProvidersAPI(nil, nil)
18+
assert.Equal(t, "providers-api", api.Name())
19+
}
20+
21+
func TestProvidersAPI_BasePath(t *testing.T) {
22+
api := NewProvidersAPI(nil, nil)
23+
assert.Equal(t, "/api/v1/providers", api.BasePath())
24+
}
25+
26+
func TestProvidersAPI_List_Good_Empty(t *testing.T) {
27+
gin.SetMode(gin.TestMode)
28+
29+
reg := provider.NewRegistry()
30+
rm := NewRuntimeManager(nil)
31+
api := NewProvidersAPI(reg, rm)
32+
33+
router := gin.New()
34+
rg := router.Group(api.BasePath())
35+
api.RegisterRoutes(rg)
36+
37+
w := httptest.NewRecorder()
38+
req, _ := http.NewRequest("GET", "/api/v1/providers", nil)
39+
router.ServeHTTP(w, req)
40+
41+
assert.Equal(t, http.StatusOK, w.Code)
42+
43+
var resp providersResponse
44+
err := json.Unmarshal(w.Body.Bytes(), &resp)
45+
require.NoError(t, err)
46+
assert.Empty(t, resp.Providers)
47+
}
48+
49+
func TestProvidersAPI_List_Good_WithRuntimeProviders(t *testing.T) {
50+
gin.SetMode(gin.TestMode)
51+
52+
reg := provider.NewRegistry()
53+
rm := NewRuntimeManager(nil)
54+
55+
// Simulate a runtime provider.
56+
rm.providers = append(rm.providers, &RuntimeProvider{
57+
Dir: "/tmp/test",
58+
Port: 9999,
59+
Manifest: &manifest.Manifest{
60+
Code: "test-provider",
61+
Name: "Test Provider",
62+
Version: "0.1.0",
63+
Namespace: "test",
64+
},
65+
})
66+
67+
api := NewProvidersAPI(reg, rm)
68+
69+
router := gin.New()
70+
rg := router.Group(api.BasePath())
71+
api.RegisterRoutes(rg)
72+
73+
w := httptest.NewRecorder()
74+
req, _ := http.NewRequest("GET", "/api/v1/providers", nil)
75+
router.ServeHTTP(w, req)
76+
77+
assert.Equal(t, http.StatusOK, w.Code)
78+
79+
var resp providersResponse
80+
err := json.Unmarshal(w.Body.Bytes(), &resp)
81+
require.NoError(t, err)
82+
require.Len(t, resp.Providers, 1)
83+
assert.Equal(t, "test-provider", resp.Providers[0].Name)
84+
assert.Equal(t, "test", resp.Providers[0].BasePath)
85+
assert.Equal(t, "active", resp.Providers[0].Status)
86+
}

runtime.go

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import (
1515

1616
"forge.lthn.ai/core/api"
1717
"forge.lthn.ai/core/api/pkg/provider"
18+
coreerr "forge.lthn.ai/core/go-log"
1819
"forge.lthn.ai/core/go-scm/manifest"
1920
"forge.lthn.ai/core/go-scm/marketplace"
2021
"github.com/gin-gonic/gin"
@@ -63,7 +64,7 @@ func (rm *RuntimeManager) StartAll(ctx context.Context) error {
6364
dir := defaultProvidersDir()
6465
discovered, err := marketplace.DiscoverProviders(dir)
6566
if err != nil {
66-
return fmt.Errorf("runtime: discover providers: %w", err)
67+
return coreerr.E("runtime.StartAll", "discover providers", err)
6768
}
6869

6970
if len(discovered) == 0 {
@@ -93,7 +94,7 @@ func (rm *RuntimeManager) startProvider(ctx context.Context, dp marketplace.Disc
9394
// Assign a free port.
9495
port, err := findFreePort()
9596
if err != nil {
96-
return nil, fmt.Errorf("find free port: %w", err)
97+
return nil, coreerr.E("runtime.startProvider", "find free port", err)
9798
}
9899

99100
// Resolve binary path.
@@ -114,15 +115,15 @@ func (rm *RuntimeManager) startProvider(ctx context.Context, dp marketplace.Disc
114115
cmd.Stderr = os.Stderr
115116

116117
if err := cmd.Start(); err != nil {
117-
return nil, fmt.Errorf("start binary %s: %w", binaryPath, err)
118+
return nil, coreerr.E("runtime.startProvider", fmt.Sprintf("start binary %s", binaryPath), err)
118119
}
119120

120121
// Wait for health check.
121122
healthURL := fmt.Sprintf("http://127.0.0.1:%d/health", port)
122123
if err := waitForHealth(healthURL, 10*time.Second); err != nil {
123124
// Kill the process if health check fails.
124125
_ = cmd.Process.Kill()
125-
return nil, fmt.Errorf("health check failed for %s: %w", m.Code, err)
126+
return nil, coreerr.E("runtime.startProvider", fmt.Sprintf("health check failed for %s", m.Code), err)
126127
}
127128

128129
// Register proxy provider.
@@ -248,7 +249,7 @@ func waitForHealth(url string, timeout time.Duration) error {
248249
time.Sleep(100 * time.Millisecond)
249250
}
250251

251-
return fmt.Errorf("health check timed out after %s: %s", timeout, url)
252+
return coreerr.E("runtime.waitForHealth", fmt.Sprintf("timed out after %s: %s", timeout, url), nil)
252253
}
253254

254255
// staticAssetGroup is a simple RouteGroup that serves static files.

runtime_test.go

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
package main
2+
3+
import (
4+
"context"
5+
"net/http"
6+
"net/http/httptest"
7+
"os/exec"
8+
"testing"
9+
"time"
10+
11+
"forge.lthn.ai/core/go-scm/manifest"
12+
"github.com/stretchr/testify/assert"
13+
"github.com/stretchr/testify/require"
14+
)
15+
16+
func TestFindFreePort_Good(t *testing.T) {
17+
port, err := findFreePort()
18+
require.NoError(t, err)
19+
assert.Greater(t, port, 0)
20+
assert.Less(t, port, 65536)
21+
}
22+
23+
func TestFindFreePort_UniquePerCall(t *testing.T) {
24+
port1, err := findFreePort()
25+
require.NoError(t, err)
26+
port2, err := findFreePort()
27+
require.NoError(t, err)
28+
// Two consecutive calls should very likely return different ports.
29+
// (Not guaranteed, but effectively always true.)
30+
assert.NotEqual(t, port1, port2)
31+
}
32+
33+
func TestWaitForHealth_Good(t *testing.T) {
34+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
35+
w.WriteHeader(http.StatusOK)
36+
}))
37+
defer srv.Close()
38+
39+
err := waitForHealth(srv.URL, 5*time.Second)
40+
assert.NoError(t, err)
41+
}
42+
43+
func TestWaitForHealth_Bad_Timeout(t *testing.T) {
44+
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
45+
w.WriteHeader(http.StatusServiceUnavailable)
46+
}))
47+
defer srv.Close()
48+
49+
err := waitForHealth(srv.URL, 500*time.Millisecond)
50+
require.Error(t, err)
51+
assert.Contains(t, err.Error(), "timed out")
52+
}
53+
54+
func TestWaitForHealth_Bad_NoServer(t *testing.T) {
55+
err := waitForHealth("http://127.0.0.1:1", 500*time.Millisecond)
56+
require.Error(t, err)
57+
assert.Contains(t, err.Error(), "timed out")
58+
}
59+
60+
func TestDefaultProvidersDir_Good(t *testing.T) {
61+
dir := defaultProvidersDir()
62+
assert.Contains(t, dir, ".core")
63+
assert.Contains(t, dir, "providers")
64+
}
65+
66+
func TestRuntimeManager_List_Good_Empty(t *testing.T) {
67+
rm := NewRuntimeManager(nil)
68+
infos := rm.List()
69+
assert.Empty(t, infos)
70+
}
71+
72+
func TestRuntimeManager_List_Good_WithProviders(t *testing.T) {
73+
rm := NewRuntimeManager(nil)
74+
rm.providers = []*RuntimeProvider{
75+
{
76+
Dir: "/tmp/test-provider",
77+
Port: 12345,
78+
Manifest: &manifest.Manifest{
79+
Code: "test-svc",
80+
Name: "Test Service",
81+
Version: "1.0.0",
82+
Namespace: "test",
83+
},
84+
},
85+
}
86+
87+
infos := rm.List()
88+
require.Len(t, infos, 1)
89+
assert.Equal(t, "test-svc", infos[0].Code)
90+
assert.Equal(t, "Test Service", infos[0].Name)
91+
assert.Equal(t, "1.0.0", infos[0].Version)
92+
assert.Equal(t, "test", infos[0].Namespace)
93+
assert.Equal(t, 12345, infos[0].Port)
94+
assert.Equal(t, "/tmp/test-provider", infos[0].Dir)
95+
}
96+
97+
func TestRuntimeManager_StopAll_Good_Empty(t *testing.T) {
98+
rm := NewRuntimeManager(nil)
99+
// Should not panic with no providers.
100+
rm.StopAll()
101+
assert.Empty(t, rm.providers)
102+
}
103+
104+
func TestRuntimeManager_StopAll_Good_WithProcess(t *testing.T) {
105+
// Start a real process so we can test graceful stop.
106+
cmd := exec.CommandContext(context.Background(), "sleep", "60")
107+
require.NoError(t, cmd.Start())
108+
109+
rm := NewRuntimeManager(nil)
110+
rm.providers = []*RuntimeProvider{
111+
{
112+
Manifest: &manifest.Manifest{Code: "sleeper"},
113+
Cmd: cmd,
114+
},
115+
}
116+
117+
rm.StopAll()
118+
assert.Nil(t, rm.providers)
119+
}
120+
121+
func TestRuntimeManager_StartAll_Good_EmptyDir(t *testing.T) {
122+
rm := NewRuntimeManager(nil)
123+
// StartAll with a non-existent providers dir should return an error
124+
// because the default dir won't have providers (at most it logs and returns nil).
125+
err := rm.StartAll(context.Background())
126+
// Depending on whether ~/.core/providers/ exists, this either returns
127+
// nil (no providers found) or an error (dir doesn't exist).
128+
// Either outcome is acceptable — no panic.
129+
_ = err
130+
}

0 commit comments

Comments
 (0)