mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
e613258fa5
Emit gateway.start, gateway.ready, and gateway.shutdown on the shared runtime event bus, while keeping reload events on the same helper path. Update subturn architecture docs to refer to runtime event kinds instead of the removed agent EventBus names. Validation: GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/gateway ./pkg/events; GOCACHE=/tmp/picoclaw-go-cache go test ./pkg/bus ./pkg/channels ./pkg/mcp ./pkg/tools/integration ./pkg/events ./pkg/gateway; make lint
175 lines
4.6 KiB
Go
175 lines
4.6 KiB
Go
package gateway
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/sipeed/picoclaw/pkg/agent"
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
|
runtimeevents "github.com/sipeed/picoclaw/pkg/events"
|
|
)
|
|
|
|
func TestRun_StartupFailuresReturnErrorAndEmitStructuredLog(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
tests := []struct {
|
|
name string
|
|
prepare func(t *testing.T, dir string) string
|
|
wantErr string
|
|
wantLogSub string
|
|
}{
|
|
{
|
|
name: "invalid config returns load error",
|
|
prepare: func(t *testing.T, dir string) string {
|
|
t.Helper()
|
|
cfgPath := filepath.Join(dir, "invalid-config.json")
|
|
if err := os.WriteFile(cfgPath, []byte("{invalid-json"), 0o644); err != nil {
|
|
t.Fatalf("WriteFile(invalid config) error = %v", err)
|
|
}
|
|
return cfgPath
|
|
},
|
|
wantErr: "error loading config:",
|
|
wantLogSub: "error loading config:",
|
|
},
|
|
{
|
|
name: "invalid config returns pre-check error",
|
|
prepare: func(t *testing.T, dir string) string {
|
|
t.Helper()
|
|
cfg := config.DefaultConfig()
|
|
cfg.Gateway.Port = 0
|
|
cfgPath := filepath.Join(dir, "config.json")
|
|
if err := config.SaveConfig(cfgPath, cfg); err != nil {
|
|
t.Fatalf("SaveConfig() error = %v", err)
|
|
}
|
|
return cfgPath
|
|
},
|
|
wantErr: "config pre-check failed: invalid gateway port: 0",
|
|
wantLogSub: "config pre-check failed: invalid gateway port: 0",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
homeDir := t.TempDir()
|
|
configPath := tt.prepare(t, homeDir)
|
|
|
|
cmd := exec.Command(os.Args[0], "-test.run=TestGatewayRunStartupFailureHelper")
|
|
cmd.Env = append(os.Environ(),
|
|
"GO_WANT_GATEWAY_RUN_HELPER=1",
|
|
"PICO_TEST_HOME="+homeDir,
|
|
"PICO_TEST_CONFIG="+configPath,
|
|
)
|
|
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("helper exited unexpectedly: %v\noutput:\n%s", err, string(output))
|
|
}
|
|
|
|
out := string(output)
|
|
if !strings.Contains(out, tt.wantErr) {
|
|
t.Fatalf("helper output missing expected error substring %q:\n%s", tt.wantErr, out)
|
|
}
|
|
|
|
logData, readErr := os.ReadFile(filepath.Join(homeDir, logPath, logFile))
|
|
if readErr != nil {
|
|
t.Fatalf("ReadFile(gateway.log) error = %v", readErr)
|
|
}
|
|
logText := string(logData)
|
|
if !strings.Contains(logText, "Gateway startup failed") {
|
|
t.Fatalf("gateway.log missing structured startup failure log:\n%s", logText)
|
|
}
|
|
if !strings.Contains(logText, tt.wantLogSub) {
|
|
t.Fatalf("gateway.log missing expected failure detail %q:\n%s", tt.wantLogSub, logText)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestGatewayRunStartupFailureHelper(t *testing.T) {
|
|
if os.Getenv("GO_WANT_GATEWAY_RUN_HELPER") != "1" {
|
|
return
|
|
}
|
|
|
|
homeDir := os.Getenv("PICO_TEST_HOME")
|
|
configPath := os.Getenv("PICO_TEST_CONFIG")
|
|
|
|
err := Run(false, homeDir, configPath, false)
|
|
if err == nil {
|
|
fmt.Fprintln(os.Stdout, "expected startup error, got nil")
|
|
os.Exit(2)
|
|
}
|
|
|
|
fmt.Fprintln(os.Stdout, err.Error())
|
|
os.Exit(0)
|
|
}
|
|
|
|
func TestPublishGatewayEvent(t *testing.T) {
|
|
eventBus := runtimeevents.NewBus()
|
|
t.Cleanup(func() {
|
|
if err := eventBus.Close(); err != nil {
|
|
t.Fatalf("Close runtime event bus: %v", err)
|
|
}
|
|
})
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
t.Cleanup(cancel)
|
|
sub, eventsCh, err := eventBus.Channel().OfKind(runtimeevents.KindGatewayStart).SubscribeChan(
|
|
ctx,
|
|
runtimeevents.SubscribeOptions{Name: "gateway-test", Buffer: 4},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("SubscribeChan() error = %v", err)
|
|
}
|
|
t.Cleanup(func() {
|
|
if err := sub.Close(); err != nil {
|
|
t.Fatalf("Close subscription: %v", err)
|
|
}
|
|
})
|
|
|
|
al := agent.NewAgentLoop(
|
|
config.DefaultConfig(),
|
|
bus.NewMessageBus(),
|
|
&startupBlockedProvider{reason: "not used"},
|
|
agent.WithRuntimeEvents(eventBus),
|
|
)
|
|
t.Cleanup(al.Close)
|
|
|
|
startedAt := time.Now().Add(-1500 * time.Millisecond)
|
|
publishGatewayEvent(al, runtimeevents.KindGatewayStart, startedAt, nil)
|
|
|
|
evt := receiveGatewayRuntimeEvent(t, eventsCh)
|
|
if evt.Kind != runtimeevents.KindGatewayStart ||
|
|
evt.Source.Component != "gateway" ||
|
|
evt.Severity != runtimeevents.SeverityInfo {
|
|
t.Fatalf("gateway event = %+v", evt)
|
|
}
|
|
payload, ok := evt.Payload.(gatewayEventPayload)
|
|
if !ok {
|
|
t.Fatalf("payload type = %T, want gatewayEventPayload", evt.Payload)
|
|
}
|
|
if payload.DurationMS <= 0 {
|
|
t.Fatalf("DurationMS = %d, want positive", payload.DurationMS)
|
|
}
|
|
}
|
|
|
|
func receiveGatewayRuntimeEvent(t *testing.T, ch <-chan runtimeevents.Event) runtimeevents.Event {
|
|
t.Helper()
|
|
|
|
select {
|
|
case evt := <-ch:
|
|
return evt
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for gateway runtime event")
|
|
return runtimeevents.Event{}
|
|
}
|
|
}
|