diff --git a/ai/ai_test.go b/ai/ai_test.go index 4ddcfe3..8714bb3 100644 --- a/ai/ai_test.go +++ b/ai/ai_test.go @@ -6,7 +6,7 @@ import ( "testing" "time" - "dappco.re/go/core" + "dappco.re/go" coreio "dappco.re/go/io" ) diff --git a/ai/ax7_test.go b/ai/ax7_test.go new file mode 100644 index 0000000..ed8f6ae --- /dev/null +++ b/ai/ax7_test.go @@ -0,0 +1,137 @@ +package ai + +import ( + "context" + "time" + + . "dappco.re/go" + rag "dappco.re/go/rag" +) + +func TestAI_Record_Good(t *T) { + withTempMetricsHome(t) + err := Record(Event{Type: "security.scan", Repo: "core/go-ai"}) + events, readErr := ReadEvents(time.Now().Add(-time.Minute)) + + AssertNoError(t, err) + AssertNoError(t, readErr) + AssertLen(t, events, 1) +} + +func TestAI_Record_Bad(t *T) { + withTempMetricsHome(t) + err := Record(Event{Type: "security.scan", Data: map[string]any{"bad": make(chan int)}}) + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "record event") +} + +func TestAI_Record_Ugly(t *T) { + withTempMetricsHome(t) + err := Record(Event{}) + events, readErr := ReadEvents(time.Now().Add(-time.Minute)) + + AssertNoError(t, err) + AssertNoError(t, readErr) + AssertLen(t, events, 1) +} + +func TestAI_ReadEvents_Good(t *T) { + withTempMetricsHome(t) + recordErr := Record(Event{Type: "scan", Timestamp: time.Now().Add(-time.Second)}) + events, err := ReadEvents(time.Now().Add(-time.Minute)) + + AssertNoError(t, recordErr) + AssertNoError(t, err) + AssertLen(t, events, 1) +} + +func TestAI_ReadEvents_Bad(t *T) { + withTempMetricsHome(t) + events, err := ReadEvents(time.Now().Add(-time.Minute)) + got := len(events) + + AssertNoError(t, err) + AssertEqual(t, 0, got) +} + +func TestAI_ReadEvents_Ugly(t *T) { + withTempMetricsHome(t) + recordErr := Record(Event{Type: "scan", Timestamp: time.Now().Add(-time.Hour)}) + events, err := ReadEvents(time.Now().Add(time.Hour)) + + AssertNoError(t, recordErr) + AssertNoError(t, err) + AssertLen(t, events, 0) +} + +func TestAI_Summary_Good(t *T) { + events := []Event{{Type: "scan", Repo: "core/go-ai", AgentID: "agent-1"}} + summary := Summary(events) + byType := summary["by_type"].(map[string]int) + + AssertEqual(t, 1, byType["scan"]) + AssertLen(t, summary["recent"].([]Event), 1) +} + +func TestAI_Summary_Bad(t *T) { + summary := Summary(nil) + byType := summary["by_type"].(map[string]int) + recent := summary["recent"].([]Event) + + AssertEmpty(t, byType) + AssertEmpty(t, recent) +} + +func TestAI_Summary_Ugly(t *T) { + events := []Event{{Type: "scan", Data: map[string]any{"nested": []any{"x"}}}} + summary := Summary(events) + recent := summary["recent"].([]Event) + + recent[0].Data["nested"].([]any)[0] = "changed" + AssertEqual(t, "x", events[0].Data["nested"].([]any)[0]) +} + +func TestAI_QueryRAGForTask_Good(t *T) { + origNewQdrantClient := newQdrantClient + origNewOllamaClient := newOllamaClient + origRunRAGQuery := runRAGQuery + t.Cleanup(func() { + newQdrantClient = origNewQdrantClient + newOllamaClient = origNewOllamaClient + runRAGQuery = origRunRAGQuery + }) + + newQdrantClient = func(rag.QdrantConfig) (*rag.QdrantClient, error) { return nil, nil } + newOllamaClient = func(rag.OllamaConfig) (*rag.OllamaClient, error) { return nil, nil } + runRAGQuery = func(_ context.Context, _ rag.VectorStore, _ rag.Embedder, _ string, _ rag.QueryConfig) ([]rag.QueryResult, error) { + return []rag.QueryResult{{Text: "Runbook", Source: "docs/build.md", Score: 0.9}}, nil + } + + got, err := QueryRAGForTask(TaskInfo{Title: "Investigate", Description: "failure"}) + AssertNoError(t, err) + AssertContains(t, got, "Runbook") +} + +func TestAI_QueryRAGForTask_Bad(t *T) { + got, err := QueryRAGForTask(TaskInfo{}) + want := "" + + AssertNoError(t, err) + AssertEqual(t, want, got) +} + +func TestAI_QueryRAGForTask_Ugly(t *T) { + origNewQdrantClient := newQdrantClient + t.Cleanup(func() { + newQdrantClient = origNewQdrantClient + }) + newQdrantClient = func(rag.QdrantConfig) (*rag.QdrantClient, error) { + return nil, NewError("qdrant unavailable") + } + + got, err := QueryRAGForTask(TaskInfo{Title: "Investigate"}) + AssertNoError(t, err) + AssertEqual(t, "", got) +} diff --git a/ai/metrics.go b/ai/metrics.go index 63d414c..0408acd 100644 --- a/ai/metrics.go +++ b/ai/metrics.go @@ -10,7 +10,7 @@ import ( "syscall" "time" - "dappco.re/go/core" + "dappco.re/go" coreio "dappco.re/go/io" coreerr "dappco.re/go/log" ) diff --git a/ai/metrics_test.go b/ai/metrics_test.go index b8d636c..8585e62 100644 --- a/ai/metrics_test.go +++ b/ai/metrics_test.go @@ -8,7 +8,7 @@ import ( "testing" // Note: intrinsic — Go test entry points and assertions "time" // Note: test-only — fixes timestamps and read windows for metrics behavior - "dappco.re/go/core" + "dappco.re/go" coreio "dappco.re/go/io" ) diff --git a/ai/rag.go b/ai/rag.go index d7cad80..d0f87c8 100644 --- a/ai/rag.go +++ b/ai/rag.go @@ -5,7 +5,7 @@ import ( "context" "time" - "dappco.re/go/core" + "dappco.re/go" rag "dappco.re/go/rag" ) diff --git a/cmd/ai/ax7_test.go b/cmd/ai/ax7_test.go new file mode 100644 index 0000000..5c7e1a3 --- /dev/null +++ b/cmd/ai/ax7_test.go @@ -0,0 +1,33 @@ +package ai + +import ( + . "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func TestAI_AddAICommands_Good(t *T) { + root := &cli.Command{Use: "core"} + AddAICommands(root) + cmd, _, err := root.Find([]string{"ai"}) + + AssertNoError(t, err) + AssertEqual(t, "ai", cmd.Name()) +} + +func TestAI_AddAICommands_Bad(t *T) { + root := &cli.Command{Use: "core"} + AddAICommands(root) + AddAICommands(root) + + AssertLen(t, root.Commands(), 1) + AssertEqual(t, "ai", root.Commands()[0].Name()) +} + +func TestAI_AddAICommands_Ugly(t *T) { + root := &cli.Command{Use: "core"} + root.AddCommand(&cli.Command{Use: "ai"}) + AddAICommands(root) + + AssertLen(t, root.Commands(), 1) + AssertEqual(t, "ai", root.Commands()[0].Name()) +} diff --git a/cmd/daemon/main.go b/cmd/daemon/main.go index 720b2e6..c053ea8 100644 --- a/cmd/daemon/main.go +++ b/cmd/daemon/main.go @@ -48,7 +48,9 @@ func runWithContext(ctx context.Context, cfg Config) error { } defer func() { if cfg.PIDFile != "" { - _ = os.Remove(cfg.PIDFile) + if err := os.Remove(cfg.PIDFile); err != nil && !errors.Is(err, os.ErrNotExist) { + fmt.Fprintln(os.Stderr, "daemon pid cleanup:", err) + } } }() @@ -59,7 +61,9 @@ func runWithContext(ctx context.Context, cfg Config) error { defer func() { shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel() - _ = svc.Shutdown(shutdownCtx) + if err := svc.Shutdown(shutdownCtx); err != nil { + fmt.Fprintln(os.Stderr, "daemon mcp shutdown:", err) + } }() errCh := make(chan error, 4) @@ -95,7 +99,9 @@ func runWithContext(ctx context.Context, cfg Config) error { defer func() { shutdownCtx, cancel := context.WithTimeout(context.Background(), 2*time.Second) defer cancel() - _ = healthServer.Shutdown(shutdownCtx) + if err := healthServer.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Fprintln(os.Stderr, "daemon health shutdown:", err) + } }() } @@ -176,7 +182,9 @@ func startHealth(ctx context.Context, addr string) (*http.Server, error) { server := &http.Server{Handler: mux} go func() { <-ctx.Done() - _ = server.Shutdown(context.Background()) + if err := server.Shutdown(context.Background()); err != nil && !errors.Is(err, http.ErrServerClosed) { + fmt.Fprintln(os.Stderr, "daemon health shutdown:", err) + } }() go func() { if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) { diff --git a/cmd/embed-bench/main.go b/cmd/embed-bench/main.go index 67bb35b..a98223b 100644 --- a/cmd/embed-bench/main.go +++ b/cmd/embed-bench/main.go @@ -19,7 +19,7 @@ import ( "slices" "time" - "dappco.re/go/core" + "dappco.re/go" coreerr "dappco.re/go/log" ) diff --git a/cmd/lem-desktop/ax7_test.go b/cmd/lem-desktop/ax7_test.go new file mode 100644 index 0000000..6763654 --- /dev/null +++ b/cmd/lem-desktop/ax7_test.go @@ -0,0 +1,1079 @@ +package main + +import ( + "time" + + . "dappco.re/go" + "github.com/wailsapp/wails/v3/pkg/application" +) + +func TestDesktop_NewAgentRunner_Good(t *T) { + runner := NewAgentRunner("api", "influx", "db", "m3", "model", "work") + name := runner.ServiceName() + running := runner.IsRunning() + + AssertEqual(t, "AgentRunner", name) + AssertFalse(t, running) +} + +func TestDesktop_NewAgentRunner_Bad(t *T) { + runner := NewAgentRunner("", "", "", "", "", "") + task := runner.CurrentTask() + running := runner.IsRunning() + + AssertEqual(t, "", task) + AssertFalse(t, running) +} + +func TestDesktop_NewAgentRunner_Ugly(t *T) { + runner := NewAgentRunner("api", "influx", "db", "m3", "model", "work") + runner.running = true + running := runner.IsRunning() + + AssertTrue(t, running) + AssertEqual(t, "AgentRunner", runner.ServiceName()) +} + +func TestDesktop_AgentRunner_ServiceName_Good(t *T) { + runner := &AgentRunner{} + got := runner.ServiceName() + want := "AgentRunner" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_AgentRunner_ServiceName_Bad(t *T) { + var runner *AgentRunner + got := runner.ServiceName() + want := "AgentRunner" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_AgentRunner_ServiceName_Ugly(t *T) { + runner := NewAgentRunner("", "", "", "", "", "") + first := runner.ServiceName() + second := runner.ServiceName() + + AssertEqual(t, first, second) + AssertEqual(t, "AgentRunner", first) +} + +func TestDesktop_AgentRunner_ServiceStartup_Good(t *T) { + runner := &AgentRunner{} + err := runner.ServiceStartup(Background(), application.ServiceOptions{}) + got := runner.ServiceName() + + AssertNoError(t, err) + AssertEqual(t, "AgentRunner", got) +} + +func TestDesktop_AgentRunner_ServiceStartup_Bad(t *T) { + runner := &AgentRunner{} + ctx, cancel := WithCancel(Background()) + cancel() + + err := runner.ServiceStartup(ctx, application.ServiceOptions{}) + AssertNoError(t, err) + AssertFalse(t, runner.IsRunning()) +} + +func TestDesktop_AgentRunner_ServiceStartup_Ugly(t *T) { + var runner AgentRunner + err := runner.ServiceStartup(Background(), application.ServiceOptions{}) + task := runner.CurrentTask() + + AssertNoError(t, err) + AssertEqual(t, "", task) +} + +func TestDesktop_AgentRunner_IsRunning_Good(t *T) { + runner := &AgentRunner{running: true} + got := runner.IsRunning() + want := true + + AssertEqual(t, want, got) + AssertTrue(t, got) +} + +func TestDesktop_AgentRunner_IsRunning_Bad(t *T) { + runner := &AgentRunner{} + got := runner.IsRunning() + want := false + + AssertEqual(t, want, got) + AssertFalse(t, got) +} + +func TestDesktop_AgentRunner_IsRunning_Ugly(t *T) { + runner := &AgentRunner{running: true} + runner.Stop() + got := runner.IsRunning() + + AssertFalse(t, got) + AssertEqual(t, "", runner.CurrentTask()) +} + +func TestDesktop_AgentRunner_CurrentTask_Good(t *T) { + runner := &AgentRunner{task: "scoring"} + got := runner.CurrentTask() + want := "scoring" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_AgentRunner_CurrentTask_Bad(t *T) { + runner := &AgentRunner{} + got := runner.CurrentTask() + want := "" + + AssertEqual(t, want, got) + AssertEmpty(t, got) +} + +func TestDesktop_AgentRunner_CurrentTask_Ugly(t *T) { + runner := &AgentRunner{running: true, task: "stopping"} + runner.Stop() + got := runner.CurrentTask() + + AssertEqual(t, "", got) + AssertFalse(t, runner.IsRunning()) +} + +func TestDesktop_AgentRunner_Start_Good(t *T) { + runner := &AgentRunner{running: true} + err := runner.Start() + running := runner.IsRunning() + + AssertNoError(t, err) + AssertTrue(t, running) +} + +func TestDesktop_AgentRunner_Start_Bad(t *T) { + runner := &AgentRunner{running: true, task: "already running"} + err := runner.Start() + task := runner.CurrentTask() + + AssertNoError(t, err) + AssertEqual(t, "already running", task) +} + +func TestDesktop_AgentRunner_Start_Ugly(t *T) { + runner := NewAgentRunner("", "", "", "", "", "") + runner.running = true + err := runner.Start() + + AssertNoError(t, err) + AssertTrue(t, runner.IsRunning()) +} + +func TestDesktop_AgentRunner_Stop_Good(t *T) { + _, cancel := WithCancel(Background()) + runner := &AgentRunner{running: true, task: "scoring", cancel: cancel} + runner.Stop() + + AssertFalse(t, runner.IsRunning()) + AssertEqual(t, "", runner.CurrentTask()) +} + +func TestDesktop_AgentRunner_Stop_Bad(t *T) { + runner := &AgentRunner{} + runner.Stop() + got := runner.IsRunning() + + AssertFalse(t, got) + AssertEqual(t, "", runner.CurrentTask()) +} + +func TestDesktop_AgentRunner_Stop_Ugly(t *T) { + runner := &AgentRunner{running: true, task: "queued"} + runner.Stop() + got := runner.CurrentTask() + + AssertEqual(t, "", got) + AssertFalse(t, runner.IsRunning()) +} + +func TestDesktop_NewDashboardService_Good(t *T) { + service := NewDashboardService("http://127.0.0.1:1", "training", "/tmp/db.duckdb") + name := service.ServiceName() + snapshot := service.GetSnapshot() + + AssertEqual(t, "DashboardService", name) + AssertEqual(t, "/tmp/db.duckdb", snapshot.DBPath) +} + +func TestDesktop_NewDashboardService_Bad(t *T) { + service := NewDashboardService("", "", "") + snapshot := service.GetSnapshot() + models := service.GetModels() + + AssertEqual(t, "", snapshot.DBPath) + AssertEmpty(t, models) +} + +func TestDesktop_NewDashboardService_Ugly(t *T) { + service := NewDashboardService("http://127.0.0.1:1", "training", "") + generation := service.GetGeneration() + training := service.GetTraining() + + AssertEqual(t, GenerationStats{}, generation) + AssertEmpty(t, training) +} + +func TestDesktop_DashboardService_ServiceName_Good(t *T) { + service := &DashboardService{} + got := service.ServiceName() + want := "DashboardService" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_DashboardService_ServiceName_Bad(t *T) { + var service *DashboardService + got := service.ServiceName() + want := "DashboardService" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_DashboardService_ServiceName_Ugly(t *T) { + service := NewDashboardService("", "", "") + first := service.ServiceName() + second := service.ServiceName() + + AssertEqual(t, first, second) + AssertEqual(t, "DashboardService", first) +} + +func TestDesktop_DashboardService_ServiceStartup_Good(t *T) { + service := NewDashboardService("http://127.0.0.1:1", "training", "") + ctx, cancel := WithCancel(Background()) + cancel() + + err := service.ServiceStartup(ctx, application.ServiceOptions{}) + AssertNoError(t, err) + AssertEqual(t, "DashboardService", service.ServiceName()) +} + +func TestDesktop_DashboardService_ServiceStartup_Bad(t *T) { + service := NewDashboardService("http://127.0.0.1:1", "training", "") + ctx, cancel := WithCancel(Background()) + cancel() + + err := service.ServiceStartup(ctx, application.ServiceOptions{}) + AssertNoError(t, err) + AssertEmpty(t, service.GetModels()) +} + +func TestDesktop_DashboardService_ServiceStartup_Ugly(t *T) { + service := NewDashboardService("http://127.0.0.1:1", "training", "") + err := service.ServiceStartup(Background(), application.ServiceOptions{}) + snapshot := service.GetSnapshot() + + AssertNoError(t, err) + AssertEqual(t, "", snapshot.DBPath) +} + +func TestDesktop_DashboardService_GetSnapshot_Good(t *T) { + service := &DashboardService{dbPath: "/tmp/db.duckdb", lastRefresh: time.Unix(1, 0)} + service.modelInventory = []ModelInfo{{Name: "model"}} + snapshot := service.GetSnapshot() + + AssertEqual(t, "/tmp/db.duckdb", snapshot.DBPath) + AssertLen(t, snapshot.Models, 1) +} + +func TestDesktop_DashboardService_GetSnapshot_Bad(t *T) { + service := &DashboardService{} + snapshot := service.GetSnapshot() + got := snapshot.UpdatedAt + + AssertEqual(t, "", snapshot.DBPath) + AssertNotEqual(t, "", got) +} + +func TestDesktop_DashboardService_GetSnapshot_Ugly(t *T) { + service := &DashboardService{generationStats: GenerationStats{GoldenCompleted: 1}} + snapshot := service.GetSnapshot() + got := snapshot.Generation.GoldenCompleted + + AssertEqual(t, 1, got) + AssertEmpty(t, snapshot.Training) +} + +func TestDesktop_DashboardService_GetTraining_Good(t *T) { + service := &DashboardService{trainingStatus: []TrainingRow{{Model: "m"}}} + training := service.GetTraining() + got := training[0].Model + + AssertLen(t, training, 1) + AssertEqual(t, "m", got) +} + +func TestDesktop_DashboardService_GetTraining_Bad(t *T) { + service := &DashboardService{} + training := service.GetTraining() + got := len(training) + + AssertEqual(t, 0, got) + AssertEmpty(t, training) +} + +func TestDesktop_DashboardService_GetTraining_Ugly(t *T) { + service := &DashboardService{trainingStatus: []TrainingRow{{Model: "m", Loss: 0.5}}} + training := service.GetTraining() + training[0].Loss = 0.1 + + AssertEqual(t, 0.1, training[0].Loss) + AssertEqual(t, 0.1, service.trainingStatus[0].Loss) +} + +func TestDesktop_DashboardService_GetGeneration_Good(t *T) { + service := &DashboardService{generationStats: GenerationStats{GoldenCompleted: 3, GoldenTarget: 10}} + generation := service.GetGeneration() + got := generation.GoldenCompleted + + AssertEqual(t, 3, got) + AssertEqual(t, 10, generation.GoldenTarget) +} + +func TestDesktop_DashboardService_GetGeneration_Bad(t *T) { + service := &DashboardService{} + generation := service.GetGeneration() + got := generation.GoldenTarget + + AssertEqual(t, 0, got) + AssertEqual(t, GenerationStats{}, generation) +} + +func TestDesktop_DashboardService_GetGeneration_Ugly(t *T) { + service := &DashboardService{generationStats: GenerationStats{ExpansionPct: 99.5}} + generation := service.GetGeneration() + got := generation.ExpansionPct + + AssertEqual(t, 99.5, got) + AssertEqual(t, 0, generation.GoldenCompleted) +} + +func TestDesktop_DashboardService_GetModels_Good(t *T) { + service := &DashboardService{modelInventory: []ModelInfo{{Name: "m", Status: "scored"}}} + models := service.GetModels() + got := models[0].Status + + AssertLen(t, models, 1) + AssertEqual(t, "scored", got) +} + +func TestDesktop_DashboardService_GetModels_Bad(t *T) { + service := &DashboardService{} + models := service.GetModels() + got := len(models) + + AssertEqual(t, 0, got) + AssertEmpty(t, models) +} + +func TestDesktop_DashboardService_GetModels_Ugly(t *T) { + service := &DashboardService{modelInventory: []ModelInfo{{Name: "m", Accuracy: 0.9}}} + models := service.GetModels() + models[0].Accuracy = 0.1 + + AssertEqual(t, 0.1, models[0].Accuracy) + AssertEqual(t, 0.1, service.modelInventory[0].Accuracy) +} + +func TestDesktop_DashboardService_Refresh_Good(t *T) { + service := NewDashboardService("http://127.0.0.1:1", "training", "") + err := service.Refresh() + snapshot := service.GetSnapshot() + + AssertNoError(t, err) + AssertNotEqual(t, "", snapshot.UpdatedAt) +} + +func TestDesktop_DashboardService_Refresh_Bad(t *T) { + var service DashboardService + AssertPanics(t, func() { + _ = service.Refresh() + }) + AssertEqual(t, "", service.dbPath) +} + +func TestDesktop_DashboardService_Refresh_Ugly(t *T) { + service := NewDashboardService("http://127.0.0.1:1", "", "") + err := service.Refresh() + generation := service.GetGeneration() + + AssertNoError(t, err) + AssertEqual(t, GenerationStats{}, generation) +} + +func TestDesktop_DashboardService_RunQuery_Good(t *T) { + service := &DashboardService{} + rows, err := service.RunQuery("select 1") + got := ErrorMessage(err) + + AssertNil(t, rows) + AssertError(t, err) + AssertContains(t, got, "no database configured") +} + +func TestDesktop_DashboardService_RunQuery_Bad(t *T) { + service := &DashboardService{dbPath: ""} + rows, err := service.RunQuery("") + got := ErrorMessage(err) + + AssertNil(t, rows) + AssertError(t, err) + AssertContains(t, got, "no database configured") +} + +func TestDesktop_DashboardService_RunQuery_Ugly(t *T) { + service := &DashboardService{dbPath: "/path/that/does/not/exist.duckdb"} + rows, err := service.RunQuery("select 1") + got := ErrorMessage(err) + + AssertNil(t, rows) + AssertError(t, err) + AssertContains(t, got, "open db") +} + +func TestDesktop_NewDockerService_Good(t *T) { + service := NewDockerService("/tmp/deploy") + name := service.ServiceName() + status := service.GetStatus() + + AssertEqual(t, "DockerService", name) + AssertEqual(t, "/tmp/deploy", status.ComposeDir) +} + +func TestDesktop_NewDockerService_Bad(t *T) { + service := NewDockerService("") + status := service.GetStatus() + running := service.IsRunning() + + AssertFalse(t, running) + AssertNotEqual(t, "", status.ComposeDir) +} + +func TestDesktop_NewDockerService_Ugly(t *T) { + service := NewDockerService("/tmp/deploy") + service.services["db"] = ContainerStatus{Running: true} + status := service.GetStatus() + + AssertTrue(t, status.Running) + AssertLen(t, status.Services, 1) +} + +func TestDesktop_DockerService_ServiceName_Good(t *T) { + service := &DockerService{} + got := service.ServiceName() + want := "DockerService" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_DockerService_ServiceName_Bad(t *T) { + var service *DockerService + got := service.ServiceName() + want := "DockerService" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_DockerService_ServiceName_Ugly(t *T) { + service := NewDockerService("/tmp/deploy") + first := service.ServiceName() + second := service.ServiceName() + + AssertEqual(t, first, second) + AssertEqual(t, "DockerService", first) +} + +func TestDesktop_DockerService_ServiceStartup_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + ctx, cancel := WithCancel(Background()) + cancel() + + err := service.ServiceStartup(ctx, application.ServiceOptions{}) + AssertNoError(t, err) + AssertEqual(t, "DockerService", service.ServiceName()) +} + +func TestDesktop_DockerService_ServiceStartup_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + ctx, cancel := WithCancel(Background()) + cancel() + + err := service.ServiceStartup(ctx, application.ServiceOptions{}) + AssertNoError(t, err) + AssertFalse(t, service.IsRunning()) +} + +func TestDesktop_DockerService_ServiceStartup_Ugly(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + err := service.ServiceStartup(Background(), application.ServiceOptions{}) + status := service.GetStatus() + + AssertNoError(t, err) + AssertFalse(t, status.Running) +} + +func TestDesktop_DockerService_Start_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + err := service.Start() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Start_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + err := service.Start() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Start_Ugly(t *T) { + t.Setenv("PATH", "") + service := &DockerService{} + err := service.Start() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Stop_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + err := service.Stop() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Stop_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + err := service.Stop() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Stop_Ugly(t *T) { + t.Setenv("PATH", "") + service := &DockerService{} + err := service.Stop() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Restart_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + err := service.Restart() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Restart_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + err := service.Restart() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Restart_Ugly(t *T) { + t.Setenv("PATH", "") + service := &DockerService{} + err := service.Restart() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_StartService_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + err := service.StartService("db") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_StartService_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + err := service.StartService("") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_StartService_Ugly(t *T) { + t.Setenv("PATH", "") + service := &DockerService{} + err := service.StartService("db") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_StopService_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + err := service.StopService("db") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_StopService_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + err := service.StopService("") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_StopService_Ugly(t *T) { + t.Setenv("PATH", "") + service := &DockerService{} + err := service.StopService("db") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_RestartService_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + err := service.RestartService("db") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_RestartService_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + err := service.RestartService("") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_RestartService_Ugly(t *T) { + t.Setenv("PATH", "") + service := &DockerService{} + err := service.RestartService("db") + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Logs_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + logs, err := service.Logs("db", 10) + + AssertEqual(t, "", logs) + AssertError(t, err) +} + +func TestDesktop_DockerService_Logs_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + logs, err := service.Logs("", 0) + + AssertEqual(t, "", logs) + AssertError(t, err) +} + +func TestDesktop_DockerService_Logs_Ugly(t *T) { + t.Setenv("PATH", "") + service := &DockerService{} + logs, err := service.Logs("db", -1) + + AssertEqual(t, "", logs) + AssertError(t, err) +} + +func TestDesktop_DockerService_GetStatus_Good(t *T) { + service := NewDockerService("/tmp/deploy") + service.services["db"] = ContainerStatus{Running: true} + status := service.GetStatus() + + AssertTrue(t, status.Running) + AssertLen(t, status.Services, 1) +} + +func TestDesktop_DockerService_GetStatus_Bad(t *T) { + service := NewDockerService("/tmp/deploy") + status := service.GetStatus() + got := status.Running + + AssertFalse(t, got) + AssertEmpty(t, status.Services) +} + +func TestDesktop_DockerService_GetStatus_Ugly(t *T) { + service := NewDockerService("") + service.services["db"] = ContainerStatus{Running: false} + status := service.GetStatus() + + AssertFalse(t, status.Running) + AssertLen(t, status.Services, 1) +} + +func TestDesktop_DockerService_IsRunning_Good(t *T) { + service := NewDockerService("/tmp/deploy") + service.services["db"] = ContainerStatus{Running: true} + got := service.IsRunning() + + AssertTrue(t, got) + AssertEqual(t, true, got) +} + +func TestDesktop_DockerService_IsRunning_Bad(t *T) { + service := NewDockerService("/tmp/deploy") + service.services["db"] = ContainerStatus{Running: false} + got := service.IsRunning() + + AssertFalse(t, got) + AssertEqual(t, false, got) +} + +func TestDesktop_DockerService_IsRunning_Ugly(t *T) { + service := NewDockerService("/tmp/deploy") + service.services["db"] = ContainerStatus{Running: false} + service.services["api"] = ContainerStatus{Running: true} + + AssertTrue(t, service.IsRunning()) + AssertLen(t, service.services, 2) +} + +func TestDesktop_DockerService_Pull_Good(t *T) { + t.Setenv("PATH", "") + service := NewDockerService(t.TempDir()) + err := service.Pull() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Pull_Bad(t *T) { + t.Setenv("PATH", "") + service := NewDockerService("") + err := service.Pull() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_DockerService_Pull_Ugly(t *T) { + t.Setenv("PATH", "") + service := &DockerService{} + err := service.Pull() + + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_NewTrayService_Good(t *T) { + service := NewTrayService(nil) + name := service.ServiceName() + snapshot := service.GetSnapshot() + + AssertEqual(t, "TrayService", name) + AssertEqual(t, TraySnapshot{}, snapshot) +} + +func TestDesktop_NewTrayService_Bad(t *T) { + service := NewTrayService(nil) + err := service.StartStack() + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "docker service") +} + +func TestDesktop_NewTrayService_Ugly(t *T) { + service := NewTrayService(nil) + service.SetServices(&DashboardService{}, &DockerService{services: map[string]ContainerStatus{}}, &AgentRunner{}) + snapshot := service.GetSnapshot() + + AssertEqual(t, "TrayService", service.ServiceName()) + AssertFalse(t, snapshot.StackRunning) +} + +func TestDesktop_TrayService_SetServices_Good(t *T) { + tray := NewTrayService(nil) + dashboard := &DashboardService{} + docker := NewDockerService("/tmp/deploy") + + tray.SetServices(dashboard, docker, &AgentRunner{}) + AssertNotNil(t, tray.dashboard) + AssertNotNil(t, tray.docker) +} + +func TestDesktop_TrayService_SetServices_Bad(t *T) { + tray := NewTrayService(nil) + tray.SetServices(nil, nil, nil) + snapshot := tray.GetSnapshot() + + AssertNil(t, tray.dashboard) + AssertEqual(t, TraySnapshot{}, snapshot) +} + +func TestDesktop_TrayService_SetServices_Ugly(t *T) { + tray := NewTrayService(nil) + tray.SetServices(&DashboardService{dbPath: "db"}, NewDockerService("/tmp/deploy"), &AgentRunner{task: "queued"}) + snapshot := tray.GetSnapshot() + + AssertEqual(t, "queued", snapshot.AgentTask) + AssertEqual(t, "db", tray.dashboard.dbPath) +} + +func TestDesktop_TrayService_ServiceName_Good(t *T) { + tray := &TrayService{} + got := tray.ServiceName() + want := "TrayService" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_TrayService_ServiceName_Bad(t *T) { + var tray *TrayService + got := tray.ServiceName() + want := "TrayService" + + AssertEqual(t, want, got) + AssertNotEqual(t, "", got) +} + +func TestDesktop_TrayService_ServiceName_Ugly(t *T) { + tray := NewTrayService(nil) + first := tray.ServiceName() + second := tray.ServiceName() + + AssertEqual(t, first, second) + AssertEqual(t, "TrayService", first) +} + +func TestDesktop_TrayService_ServiceStartup_Good(t *T) { + tray := &TrayService{} + err := tray.ServiceStartup(Background(), application.ServiceOptions{}) + got := tray.ServiceName() + + AssertNoError(t, err) + AssertEqual(t, "TrayService", got) +} + +func TestDesktop_TrayService_ServiceStartup_Bad(t *T) { + tray := &TrayService{} + ctx, cancel := WithCancel(Background()) + cancel() + + err := tray.ServiceStartup(ctx, application.ServiceOptions{}) + AssertNoError(t, err) + AssertEqual(t, TraySnapshot{}, tray.GetSnapshot()) +} + +func TestDesktop_TrayService_ServiceStartup_Ugly(t *T) { + var tray TrayService + err := tray.ServiceStartup(Background(), application.ServiceOptions{}) + snapshot := tray.GetSnapshot() + + AssertNoError(t, err) + AssertEqual(t, TraySnapshot{}, snapshot) +} + +func TestDesktop_TrayService_ServiceShutdown_Good(t *T) { + tray := &TrayService{} + err := tray.ServiceShutdown() + got := tray.ServiceName() + + AssertNoError(t, err) + AssertEqual(t, "TrayService", got) +} + +func TestDesktop_TrayService_ServiceShutdown_Bad(t *T) { + var tray TrayService + err := tray.ServiceShutdown() + snapshot := tray.GetSnapshot() + + AssertNoError(t, err) + AssertEqual(t, TraySnapshot{}, snapshot) +} + +func TestDesktop_TrayService_ServiceShutdown_Ugly(t *T) { + tray := NewTrayService(nil) + err := tray.ServiceShutdown() + got := tray.GetSnapshot() + + AssertNoError(t, err) + AssertEqual(t, TraySnapshot{}, got) +} + +func TestDesktop_TrayService_GetSnapshot_Good(t *T) { + tray := NewTrayService(nil) + tray.SetServices(&DashboardService{modelInventory: []ModelInfo{{Name: "m"}}}, NewDockerService("/tmp/deploy"), &AgentRunner{task: "queued"}) + snapshot := tray.GetSnapshot() + + AssertLen(t, snapshot.Models, 1) + AssertEqual(t, "queued", snapshot.AgentTask) +} + +func TestDesktop_TrayService_GetSnapshot_Bad(t *T) { + tray := NewTrayService(nil) + snapshot := tray.GetSnapshot() + got := snapshot.DockerServices + + AssertEqual(t, 0, got) + AssertFalse(t, snapshot.StackRunning) +} + +func TestDesktop_TrayService_GetSnapshot_Ugly(t *T) { + docker := NewDockerService("/tmp/deploy") + docker.services["db"] = ContainerStatus{Running: true} + tray := NewTrayService(nil) + tray.SetServices(nil, docker, &AgentRunner{running: true}) + + snapshot := tray.GetSnapshot() + AssertTrue(t, snapshot.StackRunning) + AssertTrue(t, snapshot.AgentRunning) +} + +func TestDesktop_TrayService_StartStack_Good(t *T) { + tray := NewTrayService(nil) + err := tray.StartStack() + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "docker service") +} + +func TestDesktop_TrayService_StartStack_Bad(t *T) { + t.Setenv("PATH", "") + tray := NewTrayService(nil) + tray.SetServices(nil, NewDockerService(t.TempDir()), nil) + + err := tray.StartStack() + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_TrayService_StartStack_Ugly(t *T) { + t.Setenv("PATH", "") + tray := NewTrayService(nil) + tray.SetServices(nil, &DockerService{}, nil) + + err := tray.StartStack() + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_TrayService_StopStack_Good(t *T) { + tray := NewTrayService(nil) + err := tray.StopStack() + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "docker service") +} + +func TestDesktop_TrayService_StopStack_Bad(t *T) { + t.Setenv("PATH", "") + tray := NewTrayService(nil) + tray.SetServices(nil, NewDockerService(t.TempDir()), nil) + + err := tray.StopStack() + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_TrayService_StopStack_Ugly(t *T) { + t.Setenv("PATH", "") + tray := NewTrayService(nil) + tray.SetServices(nil, &DockerService{}, nil) + + err := tray.StopStack() + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "docker") +} + +func TestDesktop_TrayService_StartAgent_Good(t *T) { + tray := NewTrayService(nil) + err := tray.StartAgent() + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "agent service") +} + +func TestDesktop_TrayService_StartAgent_Bad(t *T) { + tray := NewTrayService(nil) + tray.SetServices(nil, nil, &AgentRunner{running: true}) + err := tray.StartAgent() + + AssertNoError(t, err) + AssertTrue(t, tray.agent.IsRunning()) +} + +func TestDesktop_TrayService_StartAgent_Ugly(t *T) { + tray := NewTrayService(nil) + tray.SetServices(nil, nil, &AgentRunner{running: true, task: "queued"}) + err := tray.StartAgent() + + AssertNoError(t, err) + AssertEqual(t, "queued", tray.agent.CurrentTask()) +} + +func TestDesktop_TrayService_StopAgent_Good(t *T) { + tray := NewTrayService(nil) + tray.SetServices(nil, nil, &AgentRunner{running: true, task: "queued"}) + tray.StopAgent() + + AssertFalse(t, tray.agent.IsRunning()) + AssertEqual(t, "", tray.agent.CurrentTask()) +} + +func TestDesktop_TrayService_StopAgent_Bad(t *T) { + tray := NewTrayService(nil) + tray.StopAgent() + snapshot := tray.GetSnapshot() + + AssertEqual(t, TraySnapshot{}, snapshot) + AssertNil(t, tray.agent) +} + +func TestDesktop_TrayService_StopAgent_Ugly(t *T) { + tray := NewTrayService(nil) + tray.SetServices(nil, nil, &AgentRunner{}) + tray.StopAgent() + + AssertFalse(t, tray.agent.IsRunning()) + AssertEqual(t, "", tray.agent.CurrentTask()) +} diff --git a/cmd/lem-desktop/icons/ax7_test.go b/cmd/lem-desktop/icons/ax7_test.go new file mode 100644 index 0000000..c9e7cd1 --- /dev/null +++ b/cmd/lem-desktop/icons/ax7_test.go @@ -0,0 +1,30 @@ +package icons + +import . "dappco.re/go" + +func TestIcons_Placeholder_Good(t *T) { + icon := Placeholder() + signature := []byte{0x89, 0x50, 0x4e, 0x47} + got := icon[:4] + + AssertEqual(t, signature, got) + AssertTrue(t, len(icon) > 0) +} + +func TestIcons_Placeholder_Bad(t *T) { + icon := Placeholder() + got := len(icon) + want := 0 + + AssertTrue(t, got > want) + AssertNotEqual(t, want, got) +} + +func TestIcons_Placeholder_Ugly(t *T) { + first := Placeholder() + second := Placeholder() + first[0] = 0 + + AssertNotEqual(t, first[0], second[0]) + AssertEqual(t, byte(0x89), second[0]) +} diff --git a/cmd/lem-desktop/tray.go b/cmd/lem-desktop/tray.go index 5644551..c8355aa 100644 --- a/cmd/lem-desktop/tray.go +++ b/cmd/lem-desktop/tray.go @@ -6,7 +6,7 @@ import ( "os/exec" "runtime" - "dappco.re/go/core" + "dappco.re/go" "github.com/wailsapp/wails/v3/pkg/application" ) diff --git a/cmd/metrics/ax7_test.go b/cmd/metrics/ax7_test.go new file mode 100644 index 0000000..c0e14a6 --- /dev/null +++ b/cmd/metrics/ax7_test.go @@ -0,0 +1,111 @@ +package metrics + +import ( + "time" + + . "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func TestMetrics_DurationFlagValue_String_Good(t *T) { + value := 2 * time.Hour + flag := &sinceDurationFlagValue{target: &value} + got := flag.String() + + AssertEqual(t, "2h0m0s", got) +} + +func TestMetrics_DurationFlagValue_String_Bad(t *T) { + flag := &sinceDurationFlagValue{} + got := flag.String() + want := "" + + AssertEqual(t, want, got) +} + +func TestMetrics_DurationFlagValue_String_Ugly(t *T) { + var flag *sinceDurationFlagValue + got := flag.String() + want := "" + + AssertEqual(t, want, got) +} + +func TestMetrics_DurationFlagValue_Set_Good(t *T) { + value := time.Duration(0) + flag := &sinceDurationFlagValue{target: &value} + err := flag.Set("2d") + + AssertNoError(t, err) + AssertEqual(t, 48*time.Hour, value) +} + +func TestMetrics_DurationFlagValue_Set_Bad(t *T) { + value := time.Hour + flag := &sinceDurationFlagValue{target: &value} + err := flag.Set("bad") + + AssertError(t, err) + AssertEqual(t, time.Hour, value) +} + +func TestMetrics_DurationFlagValue_Set_Ugly(t *T) { + value := time.Hour + flag := &sinceDurationFlagValue{target: &value} + err := flag.Set("0s") + + AssertError(t, err) + AssertEqual(t, time.Hour, value) +} + +func TestMetrics_DurationFlagValue_Type_Good(t *T) { + value := time.Minute + flag := &sinceDurationFlagValue{target: &value} + got := flag.Type() + + AssertEqual(t, "duration", got) +} + +func TestMetrics_DurationFlagValue_Type_Bad(t *T) { + flag := &sinceDurationFlagValue{} + got := flag.Type() + want := "duration" + + AssertEqual(t, want, got) +} + +func TestMetrics_DurationFlagValue_Type_Ugly(t *T) { + var flag *sinceDurationFlagValue + got := flag.Type() + want := "duration" + + AssertEqual(t, want, got) +} + +func TestMetrics_AddMetricsCommand_Good(t *T) { + root := &cli.Command{Use: "core"} + AddMetricsCommand(root) + cmd, _, err := root.Find([]string{"metrics"}) + + AssertNoError(t, err) + AssertEqual(t, "metrics", cmd.Name()) +} + +func TestMetrics_AddMetricsCommand_Bad(t *T) { + root := &cli.Command{Use: "core"} + AddMetricsCommand(root) + AddMetricsCommand(root) + + AssertLen(t, root.Commands(), 1) + AssertEqual(t, "metrics", root.Commands()[0].Name()) +} + +func TestMetrics_AddMetricsCommand_Ugly(t *T) { + first := &cli.Command{Use: "core"} + second := &cli.Command{Use: "core"} + AddMetricsCommand(first) + AddMetricsCommand(second) + + AssertLen(t, first.Commands(), 1) + AssertLen(t, second.Commands(), 1) +} diff --git a/cmd/metrics/cmd.go b/cmd/metrics/cmd.go index 349e009..9f7f87f 100644 --- a/cmd/metrics/cmd.go +++ b/cmd/metrics/cmd.go @@ -9,9 +9,9 @@ import ( "slices" "time" + "dappco.re/go" "dappco.re/go/ai/ai" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" "dappco.re/go/i18n" coreerr "dappco.re/go/log" ) diff --git a/cmd/security/ax7_test.go b/cmd/security/ax7_test.go new file mode 100644 index 0000000..048bef0 --- /dev/null +++ b/cmd/security/ax7_test.go @@ -0,0 +1,138 @@ +package security + +import ( + . "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func TestSecurity_AlertSummary_Add_Good(t *T) { + summary := &AlertSummary{} + summary.Add("critical") + summary.Add("high") + + AssertEqual(t, 2, summary.Total) + AssertEqual(t, 1, summary.Critical) + AssertEqual(t, 1, summary.High) +} + +func TestSecurity_AlertSummary_Add_Bad(t *T) { + summary := &AlertSummary{} + summary.Add("unknown-severity") + got := summary.Unknown + + AssertEqual(t, 1, got) + AssertEqual(t, 1, summary.Total) +} + +func TestSecurity_AlertSummary_Add_Ugly(t *T) { + summary := &AlertSummary{} + summary.Add("HIGH") + summary.Add("") + + AssertEqual(t, 1, summary.High) + AssertEqual(t, 1, summary.Unknown) +} + +func TestSecurity_AlertSummary_String_Good(t *T) { + summary := &AlertSummary{} + summary.Add("critical") + got := summary.String() + + AssertContains(t, got, "critical") + AssertContains(t, got, "1") +} + +func TestSecurity_AlertSummary_String_Bad(t *T) { + summary := &AlertSummary{} + got := summary.String() + want := "No alerts" + + AssertContains(t, got, want) +} + +func TestSecurity_AlertSummary_String_Ugly(t *T) { + summary := &AlertSummary{Low: 2, Unknown: 1, Total: 3} + got := summary.String() + plain := summary.PlainString() + + AssertContains(t, got, "low") + AssertEqual(t, "2 low | 1 unknown", plain) +} + +func TestSecurity_AlertSummary_PlainString_Good(t *T) { + summary := &AlertSummary{Critical: 1, High: 2, Total: 3} + got := summary.PlainString() + want := "1 critical | 2 high" + + AssertEqual(t, want, got) +} + +func TestSecurity_AlertSummary_PlainString_Bad(t *T) { + summary := &AlertSummary{} + got := summary.PlainString() + want := "No alerts" + + AssertEqual(t, want, got) +} + +func TestSecurity_AlertSummary_PlainString_Ugly(t *T) { + summary := &AlertSummary{Medium: 1, Low: 1, Unknown: 1, Total: 3} + got := summary.PlainString() + want := "1 medium | 1 low | 1 unknown" + + AssertEqual(t, want, got) +} + +func TestSecurity_AddSecurityCommands_Good(t *T) { + root := &cli.Command{Use: "core"} + AddSecurityCommands(root) + cmd, _, err := root.Find([]string{"security"}) + + AssertNoError(t, err) + AssertEqual(t, "security", cmd.Name()) +} + +func TestSecurity_AddSecurityCommands_Bad(t *T) { + root := &cli.Command{Use: "core"} + AddSecurityCommands(root) + AddSecurityCommands(root) + + AssertLen(t, root.Commands(), 1) + AssertEqual(t, "security", root.Commands()[0].Name()) +} + +func TestSecurity_AddSecurityCommands_Ugly(t *T) { + root := &cli.Command{Use: "core"} + root.AddCommand(&cli.Command{Use: "security"}) + AddSecurityCommands(root) + + AssertLen(t, root.Commands(), 1) + AssertEqual(t, "security", root.Commands()[0].Name()) +} + +func TestSecurity_RawMessage_UnmarshalJSON_Good(t *T) { + var raw githubRawMessage + err := raw.UnmarshalJSON([]byte(`{"ok":true}`)) + got := string(raw) + + AssertNoError(t, err) + AssertEqual(t, `{"ok":true}`, got) +} + +func TestSecurity_RawMessage_UnmarshalJSON_Bad(t *T) { + var raw *githubRawMessage + err := raw.UnmarshalJSON([]byte(`{"ok":true}`)) + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "nil raw message") +} + +func TestSecurity_RawMessage_UnmarshalJSON_Ugly(t *T) { + raw := githubRawMessage(`old`) + err := raw.UnmarshalJSON([]byte(`null`)) + got := string(raw) + + AssertNoError(t, err) + AssertEqual(t, "null", got) +} diff --git a/cmd/security/cmd_alerts.go b/cmd/security/cmd_alerts.go index 6fb7845..ef2c305 100644 --- a/cmd/security/cmd_alerts.go +++ b/cmd/security/cmd_alerts.go @@ -3,8 +3,8 @@ package security import ( "time" + "dappco.re/go" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" "dappco.re/go/i18n" ) diff --git a/cmd/security/cmd_deps.go b/cmd/security/cmd_deps.go index a397e70..9f71432 100644 --- a/cmd/security/cmd_deps.go +++ b/cmd/security/cmd_deps.go @@ -3,8 +3,8 @@ package security import ( "time" + "dappco.re/go" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" "dappco.re/go/i18n" ) diff --git a/cmd/security/cmd_jobs.go b/cmd/security/cmd_jobs.go index ca96ba5..c1975d5 100644 --- a/cmd/security/cmd_jobs.go +++ b/cmd/security/cmd_jobs.go @@ -2,13 +2,13 @@ package security import ( "cmp" - "context" + "os/exec" "slices" "time" + "dappco.re/go" "dappco.re/go/ai/ai" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" "dappco.re/go/i18n" coreerr "dappco.re/go/log" "dappco.re/go/scm/repos" @@ -18,7 +18,6 @@ var ( collectDependabotAlertsForJobs = collectDepAlerts collectCodeScanningAlertsForJobs = collectScanAlerts collectSecretScanningAlertsForJobs = collectSecretAlerts - jobsProcessCore = cli.Core ) const maxSecurityJobWorkers = 32 @@ -457,37 +456,22 @@ func buildJobsMetricsEvent(commandOptions JobsCommandOptions, summary *AlertSumm } func createJobsIssue(issueRepo, title, body string) (string, error) { - c := jobsProcessCore() - result := c.Process().Run(context.Background(), "gh", + cmd := exec.Command("gh", "issue", "create", "--repo", issueRepo, "--title", title, "--body", body, "--label", "type:security-scan", ) - - output := processResultOutput(result.Value) - if !result.OK { + output, err := cmd.CombinedOutput() + if err != nil { message := "create summary issue" - if output != "" { - message += ": " + output + if text := core.Trim(string(output)); text != "" { + message += ": " + text } - return "", cli.Wrap(coreResultError(result), message) - } - return core.Trim(output), nil -} - -func processResultOutput(value any) string { - switch output := value.(type) { - case string: - return output - case []byte: - return string(output) - case nil: - return "" - default: - return core.Sprint(output) + return "", cli.Wrap(err, message) } + return core.Trim(string(output)), nil } func buildJobsIssueBody(summary *AlertSummary, repos []jobRepoResult) string { diff --git a/cmd/security/cmd_scan.go b/cmd/security/cmd_scan.go index d63a3b1..8806f59 100644 --- a/cmd/security/cmd_scan.go +++ b/cmd/security/cmd_scan.go @@ -3,8 +3,8 @@ package security import ( "time" + "dappco.re/go" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" "dappco.re/go/i18n" ) diff --git a/cmd/security/cmd_secrets.go b/cmd/security/cmd_secrets.go index 35d2447..7cb74e4 100644 --- a/cmd/security/cmd_secrets.go +++ b/cmd/security/cmd_secrets.go @@ -3,8 +3,8 @@ package security import ( "time" + "dappco.re/go" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" "dappco.re/go/i18n" ) diff --git a/cmd/security/cmd_security.go b/cmd/security/cmd_security.go index 544f79f..68657d3 100644 --- a/cmd/security/cmd_security.go +++ b/cmd/security/cmd_security.go @@ -7,9 +7,9 @@ import ( "slices" "time" + "dappco.re/go" "dappco.re/go/ai/ai" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" "dappco.re/go/i18n" "dappco.re/go/io" coreerr "dappco.re/go/log" @@ -31,7 +31,9 @@ var ( ) func recordSecurityMetricsEvent(event ai.Event) { - _ = ai.Record(event) + if err := ai.Record(event); err != nil { + return + } } // SecuritySelectionOptions{RepositoryName: "go-ai", SeverityFilter: "high", JSONOutput: true} @@ -160,6 +162,9 @@ type SecretScanningAlert struct { func loadRegistry(registryPath string) (*repos.Registry, error) { if registryPath != "" { + if !io.Local.Exists(registryPath) { + return nil, cli.Err("registry not found: %s", registryPath) + } registry, err := repos.LoadRegistry(io.Local, registryPath) if err != nil { return nil, cli.Wrap(err, "load registry") @@ -267,8 +272,8 @@ func trimGitHubJSONBytes(data []byte) []byte { return []byte(core.Trim(string(data))) } -func coreResultError(result core.Result) error { - if err, ok := result.Value.(error); ok { +func coreResultError(value any) error { + if err, ok := value.(error); ok { return err } return core.E("security.core.result", "operation failed", nil) @@ -282,7 +287,7 @@ func decodeGitHubArrayItems(output []byte) ([]githubRawMessage, error) { var pages []githubRawMessage if result := core.JSONUnmarshal(trimmed, &pages); !result.OK { - return nil, coreerr.E("security", "parse GitHub API response", coreResultError(result)) + return nil, coreerr.E("security", "parse GitHub API response", coreResultError(result.Value)) } items := make([]githubRawMessage, 0, len(pages)) @@ -299,7 +304,7 @@ func decodeGitHubArrayItems(output []byte) ([]githubRawMessage, error) { var pageItems []githubRawMessage if result := core.JSONUnmarshal(pageData, &pageItems); !result.OK { - return nil, coreerr.E("security", "parse GitHub API page", coreResultError(result)) + return nil, coreerr.E("security", "parse GitHub API page", coreResultError(result.Value)) } items = append(items, pageItems...) } @@ -317,7 +322,7 @@ func decodeDependabotAlerts(output []byte) ([]DependabotAlert, error) { for _, item := range items { var alert DependabotAlert if result := core.JSONUnmarshal(item, &alert); !result.OK { - return nil, coreerr.E("security", "parse dependabot alert", coreResultError(result)) + return nil, coreerr.E("security", "parse dependabot alert", coreResultError(result.Value)) } alerts = append(alerts, alert) } @@ -334,7 +339,7 @@ func decodeCodeScanningAlerts(output []byte) ([]CodeScanningAlert, error) { for _, item := range items { var alert CodeScanningAlert if result := core.JSONUnmarshal(item, &alert); !result.OK { - return nil, coreerr.E("security", "parse code scanning alert", coreResultError(result)) + return nil, coreerr.E("security", "parse code scanning alert", coreResultError(result.Value)) } alerts = append(alerts, alert) } @@ -351,7 +356,7 @@ func decodeSecretScanningAlerts(output []byte) ([]SecretScanningAlert, error) { for _, item := range items { var alert SecretScanningAlert if result := core.JSONUnmarshal(item, &alert); !result.OK { - return nil, coreerr.E("security", "parse secret scanning alert", coreResultError(result)) + return nil, coreerr.E("security", "parse secret scanning alert", coreResultError(result.Value)) } alerts = append(alerts, alert) } @@ -369,7 +374,7 @@ func decodeGitHubRepositoryNames(output []byte) ([]string, error) { for _, item := range items { var repository githubRepoResponse if result := core.JSONUnmarshal(item, &repository); !result.OK { - return nil, coreerr.E("security", "parse GitHub repository", coreResultError(result)) + return nil, coreerr.E("security", "parse GitHub repository", coreResultError(result.Value)) } if repository.FullName == "" { continue diff --git a/cmd/security/cmd_security_test.go b/cmd/security/cmd_security_test.go index e5f4d10..18c84a8 100644 --- a/cmd/security/cmd_security_test.go +++ b/cmd/security/cmd_security_test.go @@ -229,6 +229,13 @@ func TestCmdSecurity_recordSecurityMetricsEvent_Ugly_DoesNotPanic(t *testing.T) // recordSecurityMetricsEvent intentionally ignores write errors, so this test // only verifies that the wrapper stays no-op from the caller's perspective. recordSecurityMetricsEvent(ai.Event{Type: "security.alerts"}) + events, err := ai.ReadEvents(time.Now().Add(-time.Minute)) + if err != nil { + t.Fatalf("ReadEvents after recordSecurityMetricsEvent: %v", err) + } + if len(events) != 1 || events[0].Type != "security.alerts" { + t.Fatalf("unexpected metrics events: %+v", events) + } } func TestCmdSecurity_runGitHubAPI_Good_ReturnsStdout(t *testing.T) { diff --git a/cmd/security/security_targets.go b/cmd/security/security_targets.go index 4dd6f2d..f8dd343 100644 --- a/cmd/security/security_targets.go +++ b/cmd/security/security_targets.go @@ -1,8 +1,8 @@ package security import ( + "dappco.re/go" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" "dappco.re/go/scm/repos" ) @@ -88,11 +88,23 @@ func isSafeGitHubPathComponent(value string) bool { func selectRegistryRepos(registry *repos.Registry, repoFilter string) []*repos.Repo { if repoFilter != "" { if repository, ok := registry.Get(repoFilter); ok { - return []*repos.Repo{repository} + return []*repos.Repo{securityRepoView(repository)} } return nil } - return registry.List() + repositories := registry.List() + selected := make([]*repos.Repo, 0, len(repositories)) + for _, repository := range repositories { + selected = append(selected, securityRepoView(repository)) + } + return selected +} + +func securityRepoView(repository *repos.Repo) *repos.Repo { + if repository == nil { + return nil + } + return &repos.Repo{Name: repository.Name} } func securitySectionLabel(label, externalTarget string) string { diff --git a/go.mod b/go.mod index 01cefc7..d2e0273 100644 --- a/go.mod +++ b/go.mod @@ -3,22 +3,37 @@ module dappco.re/go/ai go 1.26.0 require ( + dappco.re/go v0.9.0 dappco.re/go/api v0.8.0-alpha.1 - dappco.re/go/core v0.8.0-alpha.1 + dappco.re/go/cli v0.8.0-alpha.1 dappco.re/go/i18n v0.8.0-alpha.1 dappco.re/go/io v0.8.0-alpha.1 dappco.re/go/log v0.8.0-alpha.1 - dappco.re/go/scm v0.8.0-alpha.1 dappco.re/go/rag v0.8.0-alpha.1 - dappco.re/go/cli v0.8.0-alpha.1 + dappco.re/go/scm v0.8.0-alpha.1 github.com/gin-gonic/gin v1.12.0 ) require ( + dappco.re/go/core v0.8.0-alpha.1 // indirect + dappco.re/go/core/i18n v0.2.3 // indirect + dappco.re/go/core/inference v0.2.1 // indirect + dappco.re/go/core/log v0.1.2 // indirect dappco.re/go/inference v0.8.0-alpha.1 // indirect + github.com/99designs/gqlgen v0.17.88 // indirect + github.com/KyleBanks/depth v1.2.1 // indirect + github.com/agnivade/levenshtein v1.2.1 // indirect + github.com/andybalholm/brotli v1.2.0 // indirect github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect + github.com/bytedance/gopkg v0.1.4 // indirect + github.com/bytedance/sonic v1.15.0 // indirect + github.com/bytedance/sonic/loader v0.5.0 // indirect + github.com/casbin/casbin/v2 v2.135.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/charmbracelet/bubbletea v1.3.10 // indirect github.com/charmbracelet/colorprofile v0.4.3 // indirect github.com/charmbracelet/lipgloss v1.1.1-0.20250404203927-76690c660834 // indirect @@ -27,35 +42,111 @@ require ( github.com/charmbracelet/x/term v0.2.2 // indirect github.com/clipperhouse/displaywidth v0.11.0 // indirect github.com/clipperhouse/uax29/v2 v2.7.0 // indirect + github.com/cloudwego/base64x v0.1.6 // indirect + github.com/coreos/go-oidc/v3 v3.17.0 // indirect github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f // indirect + github.com/gabriel-vasile/mimetype v1.4.13 // indirect + github.com/gin-contrib/authz v1.0.6 // indirect + github.com/gin-contrib/cors v1.7.6 // indirect + github.com/gin-contrib/expvar v1.0.3 // indirect + github.com/gin-contrib/gzip v1.2.5 // indirect + github.com/gin-contrib/httpsign v1.0.3 // indirect + github.com/gin-contrib/location/v2 v2.0.0 // indirect + github.com/gin-contrib/pprof v1.5.3 // indirect + github.com/gin-contrib/secure v1.1.2 // indirect + github.com/gin-contrib/sessions v1.0.4 // indirect + github.com/gin-contrib/slog v1.2.0 // indirect + github.com/gin-contrib/sse v1.1.0 // indirect + github.com/gin-contrib/static v1.1.5 // indirect + github.com/gin-contrib/timeout v1.1.0 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-logr/logr v1.4.3 // indirect + github.com/go-logr/stdr v1.2.2 // indirect + github.com/go-openapi/jsonpointer v0.22.5 // indirect + github.com/go-openapi/jsonreference v0.21.5 // indirect + github.com/go-openapi/spec v0.22.4 // indirect + github.com/go-openapi/swag/conv v0.25.5 // indirect + github.com/go-openapi/swag/jsonname v0.25.5 // indirect + github.com/go-openapi/swag/jsonutils v0.25.5 // indirect + github.com/go-openapi/swag/loading v0.25.5 // indirect + github.com/go-openapi/swag/stringutils v0.25.5 // indirect + github.com/go-openapi/swag/typeutils v0.25.5 // indirect + github.com/go-openapi/swag/yamlutils v0.25.5 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.30.1 // indirect + github.com/go-viper/mapstructure/v2 v2.5.0 // indirect + github.com/goccy/go-json v0.10.6 // indirect + github.com/goccy/go-yaml v1.19.2 // indirect github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/context v1.1.2 // indirect + github.com/gorilla/securecookie v1.1.2 // indirect + github.com/gorilla/sessions v1.4.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.3.0 // indirect github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 // indirect + github.com/leodido/go-urn v1.4.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mailru/easyjson v0.9.2 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-localereader v0.0.1 // indirect github.com/mattn/go-runewidth v0.0.21 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 // indirect github.com/muesli/cancelreader v0.2.2 // indirect github.com/muesli/termenv v0.16.0 // indirect github.com/ollama/ollama v0.18.1 // indirect + github.com/pelletier/go-toml/v2 v2.2.4 // indirect github.com/qdrant/go-client v1.17.1 // indirect + github.com/quic-go/qpack v0.6.0 // indirect + github.com/quic-go/quic-go v0.59.0 // indirect github.com/rivo/uniseg v0.4.7 // indirect + github.com/sosodev/duration v1.4.0 // indirect github.com/spf13/cobra v1.10.2 // indirect github.com/spf13/pflag v1.0.10 // indirect + github.com/swaggo/files v1.0.1 // indirect + github.com/swaggo/gin-swagger v1.6.1 // indirect + github.com/swaggo/swag v1.16.6 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.3.1 // indirect + github.com/vektah/gqlparser/v2 v2.5.32 // indirect github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect - golang.org/x/crypto v0.49.0 // indirect - golang.org/x/net v0.52.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.41.0 // indirect - golang.org/x/text v0.35.0 // indirect + go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect + go.opentelemetry.io/auto/sdk v1.2.1 // indirect + go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0 // indirect + go.opentelemetry.io/otel v1.42.0 // indirect + go.opentelemetry.io/otel/metric v1.42.0 // indirect + go.opentelemetry.io/otel/sdk v1.42.0 // indirect + go.opentelemetry.io/otel/trace v1.42.0 // indirect + go.yaml.in/yaml/v3 v3.0.4 // indirect + golang.org/x/arch v0.25.0 // indirect + golang.org/x/crypto v0.50.0 // indirect + golang.org/x/mod v0.34.0 // indirect + golang.org/x/net v0.53.0 // indirect + golang.org/x/oauth2 v0.36.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.43.0 // indirect + golang.org/x/term v0.42.0 // indirect + golang.org/x/text v0.36.0 // indirect + golang.org/x/tools v0.43.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 // indirect google.golang.org/grpc v1.79.2 // indirect google.golang.org/protobuf v1.36.11 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) -// TODO(#1015): Keep until dappco.re/go/api publishes go-import metadata; removing this returns 404 from dappco.re/go/api?go-get=1. -replace dappco.re/go/api => dappco.re/go/core/api v0.8.0-alpha.1 +replace ( + dappco.re/go/api => github.com/dappcore/api v0.8.0-alpha.1 + dappco.re/go/cli => dappco.re/go/core/cli v0.5.2 + dappco.re/go/i18n => github.com/dappcore/go-i18n v0.8.0-alpha.1 + dappco.re/go/inference => github.com/dappcore/go-inference v0.8.0-alpha.1 + dappco.re/go/io => github.com/dappcore/go-io v0.8.0-alpha.1 + dappco.re/go/log => github.com/dappcore/go-log v0.8.0-alpha.1 + dappco.re/go/rag => github.com/dappcore/go-rag v0.8.0-alpha.1 + dappco.re/go/scm => github.com/dappcore/go-scm v0.8.0-alpha.1 +) diff --git a/go.sum b/go.sum index 955a8e1..bc4edc6 100644 --- a/go.sum +++ b/go.sum @@ -1,31 +1,51 @@ +dappco.re/go v0.9.0 h1:4ruZRNqKDDva8o6g65tYggjGVe42E6/lMZfVKXtr3p0= +dappco.re/go v0.9.0/go.mod h1:xapr7fLK4/9Pu2iSCr4qZuIuatmtx1j56zS/oPDbGyQ= dappco.re/go/core v0.8.0-alpha.1 h1:gj7+Scv+L63Z7wMxbJYHhaRFkHJo2u4MMPuUSv/Dhtk= dappco.re/go/core v0.8.0-alpha.1/go.mod h1:f2/tBZ3+3IqDrg2F5F598llv0nmb/4gJVCFzM5geE4A= +dappco.re/go/core/cli v0.5.2 h1:mo+PERo3lUytE+r3ArHr8o2nTftXjgPPsU/rn3ETXDM= +dappco.re/go/core/cli v0.5.2/go.mod h1:D4zfn3ec/hb72AWX/JWDvkW+h2WDKQcxGUrzoss7q2s= dappco.re/go/core/i18n v0.2.3 h1:GqFaTR1I0SfSEc4WtsAkgao+jp8X5qcMPqrX0eMAOrY= dappco.re/go/core/i18n v0.2.3/go.mod h1:LoyX/4fIEJO/wiHY3Q682+4P0Ob7zPemcATfwp0JBUg= -dappco.re/go/core/inference v0.3.0 h1:ANFnlVO1LEYDipeDeBgqmb8CHvOTUFhMPyfyHGqO0IY= -dappco.re/go/core/inference v0.3.0/go.mod h1:wbRY0v6iwOoJCpTvcBFarAM08bMgpPcrF6yv3vccYoA= -dappco.re/go/core/io v0.4.2 h1:SHNF/xMPyFnKWWYoFW5Y56eiuGVL/mFa1lfIw/530ls= -dappco.re/go/core/io v0.4.2/go.mod h1:w71dukyunczLb8frT9JOd5B78PjwWQD3YAXiCt3AcPA= +dappco.re/go/core/inference v0.2.1 h1:/kgZuG4tXibgwTB3VIB1zg9UMCHRm9T3EWoDLjptBpY= +dappco.re/go/core/inference v0.2.1/go.mod h1:wbRY0v6iwOoJCpTvcBFarAM08bMgpPcrF6yv3vccYoA= dappco.re/go/core/log v0.1.2 h1:pQSZxKD8VycdvjNJmatXbPSq2OxcP2xHbF20zgFIiZI= dappco.re/go/core/log v0.1.2/go.mod h1:Nkqb8gsXhZAO8VLpx7B8i1iAmohhzqA20b9Zr8VUcJs= -dappco.re/go/core/scm v0.6.1 h1:nQWr2AGreLzhp//2zZolol87TCKlzV2/I/hpBVkv0Gc= -dappco.re/go/core/scm v0.6.1/go.mod h1:fYy/xazjyv84X8sxBIpTBikSdU5nQq4qf/IR2hXnd5E= -forge.lthn.ai/core/cli v0.3.7 h1:1GrbaGg0wDGHr6+klSbbGyN/9sSbHvFbdySJznymhwg= -forge.lthn.ai/core/cli v0.3.7/go.mod h1:DBUppJkA9P45ZFGgI2B8VXw1rAZxamHoI/KG7fRvTNs= -forge.lthn.ai/core/go v0.3.3 h1:kYYZ2nRYy0/Be3cyuLJspRjLqTMxpckVyhb/7Sw2gd0= -forge.lthn.ai/core/go v0.3.3/go.mod h1:Cp4ac25pghvO2iqOu59t1GyngTKVOzKB5/VPdhRi9CQ= -forge.lthn.ai/core/go-i18n v0.1.7 h1:aHkAoc3W8fw3RPNvw/UszQbjyFWXHszzbZgty3SwyAA= -forge.lthn.ai/core/go-i18n v0.1.7/go.mod h1:0VDjwtY99NSj2iqwrI09h5GUsJeM9s48MLkr+/Dn4G8= -forge.lthn.ai/core/go-inference v0.1.7 h1:9Dy6v03jX5ZRH3n5iTzlYyGtucuBIgSe+S7GWvBzx9Q= -forge.lthn.ai/core/go-inference v0.1.7/go.mod h1:jfWz+IJX55wAH98+ic6FEqqGB6/P31CHlg7VY7pxREw= -forge.lthn.ai/core/go-log v0.0.4 h1:KTuCEPgFmuM8KJfnyQ8vPOU1Jg654W74h8IJvfQMfv0= -forge.lthn.ai/core/go-log v0.0.4/go.mod h1:r14MXKOD3LF/sI8XUJQhRk/SZHBE7jAFVuCfgkXoZPw= +github.com/99designs/gqlgen v0.17.88 h1:neMQDgehMwT1vYIOx/w5ZYPUU/iMNAJzRO44I5Intoc= +github.com/99designs/gqlgen v0.17.88/go.mod h1:qeqYFEgOeSKqWedOjogPizimp2iu4E23bdPvl4jTYic= +github.com/KyleBanks/depth v1.2.1 h1:5h8fQADFrWtarTdtDudMmGsC7GPbOAu6RVB3ffsVFHc= +github.com/KyleBanks/depth v1.2.1/go.mod h1:jzSb9d0L43HxTQfT+oSA1EEp2q+ne2uh6XgeJcm8brE= +github.com/PuerkitoBio/goquery v1.11.0 h1:jZ7pwMQXIITcUXNH83LLk+txlaEy6NVOfTuP43xxfqw= +github.com/PuerkitoBio/goquery v1.11.0/go.mod h1:wQHgxUOU3JGuj3oD/QFfxUdlzW6xPHfqyHre6VMY4DQ= +github.com/agnivade/levenshtein v1.2.1 h1:EHBY3UOn1gwdy/VbFwgo4cxecRznFk7fKWN1KOX7eoM= +github.com/agnivade/levenshtein v1.2.1/go.mod h1:QVVI16kDrtSuwcpd0p1+xMC6Z/VfhtCyDIjcwga4/DU= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ= +github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8= +github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= +github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= +github.com/andybalholm/cascadia v1.3.3 h1:AG2YHrzJIm4BZ19iwJ/DAua6Btl3IwJX+VI4kktS1LM= +github.com/andybalholm/cascadia v1.3.3/go.mod h1:xNd9bqTn98Ln4DwST8/nG+H0yuB8Hmgu1YHNnWw0GeA= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0 h1:jfIu9sQUG6Ig+0+Ap1h4unLjW6YQJpKZVmUzxsD4E/Q= +github.com/arbovm/levenshtein v0.0.0-20160628152529-48b4e1c0c4d0/go.mod h1:t2tdKJDJF9BV14lnkjHmOQgcvEKgtqs5a1N3LNdJhGE= github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k= github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/buger/jsonparser v1.1.1 h1:2PnMjfWD7wBILjqQbt530v576A/cAbQvEW9gGIpYMUs= github.com/buger/jsonparser v1.1.1/go.mod h1:6RYKKt7H4d4+iWqouImQ9R2FZql3VbhNgx27UK13J/0= +github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM= +github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4= +github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE= +github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k= +github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE= +github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo= +github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk= +github.com/casbin/casbin/v2 v2.135.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/charmbracelet/bubbletea v1.3.10 h1:otUDHWMMzQSB0Pkc87rm691KZ3SWa4KUlvF9nRvCICw= @@ -44,29 +64,146 @@ github.com/clipperhouse/displaywidth v0.11.0 h1:lBc6kY44VFw+TDx4I8opi/EtL9m20WSE github.com/clipperhouse/displaywidth v0.11.0/go.mod h1:bkrFNkf81G8HyVqmKGxsPufD3JhNl3dSqnGhOoSD/o0= github.com/clipperhouse/uax29/v2 v2.7.0 h1:+gs4oBZ2gPfVrKPthwbMzWZDaAFPGYK72F0NJv2v7Vk= github.com/clipperhouse/uax29/v2 v2.7.0/go.mod h1:EFJ2TJMRUaplDxHKj1qAEhCtQPW2tJSwu5BF98AuoVM= +github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M= +github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU= +github.com/coreos/go-oidc/v3 v3.17.0 h1:hWBGaQfbi0iVviX4ibC7bk8OKT5qNr4klBaCHVNvehc= +github.com/coreos/go-oidc/v3 v3.17.0/go.mod h1:wqPbKFrVnE90vty060SB40FCJ8fTHTxSwyXJqZH+sI8= github.com/cpuguy83/go-md2man/v2 v2.0.6/go.mod h1:oOW0eioCTA6cOiMLiUPZOpcVxMig6NIQQ7OS05n1F4g= +github.com/dappcore/api v0.8.0-alpha.1 h1:tvhqltzmy8vSlNR1CrqccB7I6F+H0kO5YGgQkNrjR4I= +github.com/dappcore/api v0.8.0-alpha.1/go.mod h1:EdQjhzoMGSS4KV34MgAeyOBZFAiOKXo/n9rTs/0i9Zw= +github.com/dappcore/go-i18n v0.8.0-alpha.1 h1:fHB8yWWp7M8UNRndo8owTgnVq+XrtNTU+n0R3HX0uPA= +github.com/dappcore/go-i18n v0.8.0-alpha.1/go.mod h1:aSfWSAW2EVh/aMbMplc27URnjl6DvRVvWfvRC2my7AY= +github.com/dappcore/go-inference v0.8.0-alpha.1 h1:pzJoaJI0FhzUakq7tZqD6VdgMoMiuFTflVXXHCQuI0I= +github.com/dappcore/go-inference v0.8.0-alpha.1/go.mod h1:rfNXLcfMilEI3nKpcdrC0PQKyUyaf6bDYseowgRwDP8= +github.com/dappcore/go-io v0.8.0-alpha.1 h1:Ssyc5Q/U0heRZSgiEm/tsLVerkN8QmgEvcVXvAwlfwI= +github.com/dappcore/go-io v0.8.0-alpha.1/go.mod h1:491Lt0LOTK4/88EGWVWhrACuXAoxPXvXYu/iIwYc9C0= +github.com/dappcore/go-log v0.8.0-alpha.1 h1:OqZ9Njhz4fr+2BCHOgWxZZcPj/T46jN2UlOCytOCr2Y= +github.com/dappcore/go-log v0.8.0-alpha.1/go.mod h1:IC04Em9SfVTcXiWc1BqZDQfa1MtOuMDEermZkQcTz9c= +github.com/dappcore/go-rag v0.8.0-alpha.1 h1:a/8VuXcH3bFaPfPK2SG3ssogGwKo0LYZozr5/airIQE= +github.com/dappcore/go-rag v0.8.0-alpha.1/go.mod h1:OJgmSJ8lctK/bVPGWWRiAtG8EhDKY6Hm+Y2tFxBTs0k= +github.com/dappcore/go-scm v0.8.0-alpha.1 h1:QTO90tnuO7s2ke3LJ+Vs2NhHSyvfuJtcZoMhllXufvM= +github.com/dappcore/go-scm v0.8.0-alpha.1/go.mod h1:11xL67SU5TJ+fTBLyqYDDwotl7Y1qy5rWY+JgEQ16UQ= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54 h1:SG7nF6SRlWhcT7cNTs5R6Hk4V2lcmLz2NsG2VnInyNo= +github.com/dgryski/trifles v0.0.0-20230903005119-f50d829f2e54/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f h1:Y/CXytFA4m6baUTXGLOoWe4PQhGxaX0KpnayAqC48p4= github.com/erikgeiser/coninput v0.0.0-20211004153227-1c3628e74d0f/go.mod h1:vw97MGsxSvLiUE2X8qFplwetxpGLQrlU1Q9AUEIzCaM= +github.com/gabriel-vasile/mimetype v1.4.13 h1:46nXokslUBsAJE/wMsp5gtO500a4F3Nkz9Ufpk2AcUM= +github.com/gabriel-vasile/mimetype v1.4.13/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gin-contrib/authz v1.0.6 h1:qAO4sSSzOPCwYRZI6YtubC+h2tZVwhwSJeyEZn2W+5k= +github.com/gin-contrib/authz v1.0.6/go.mod h1:A2B5Im1M/HIoHPjLc31j3RlENSE6j8euJY9NFdzZeYo= +github.com/gin-contrib/cors v1.7.6 h1:3gQ8GMzs1Ylpf70y8bMw4fVpycXIeX1ZemuSQIsnQQY= +github.com/gin-contrib/cors v1.7.6/go.mod h1:Ulcl+xN4jel9t1Ry8vqph23a60FwH9xVLd+3ykmTjOk= +github.com/gin-contrib/expvar v1.0.3 h1:nIbUaokxZfUEC/35h+RyWCP1SMF/suV/ARbXL3H3jrw= +github.com/gin-contrib/expvar v1.0.3/go.mod h1:bwqqmhty1Zl2JYVLzBIL6CSHDWDbQoQoicalAnBvUnY= +github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI= +github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw= +github.com/gin-contrib/httpsign v1.0.3 h1:NpeDQjmUV0qFjGCm/rkXSp3HH0hU7r84q1v+VtTiI5I= +github.com/gin-contrib/httpsign v1.0.3/go.mod h1:n4GC7StmHNBhIzWzuW2njKbZMeEWh4tDbmn3bD1ab+k= +github.com/gin-contrib/location/v2 v2.0.0 h1:iLx5RatHQHSxgC0tm2AG0sIuQKecI7FhREessVd6RWY= +github.com/gin-contrib/location/v2 v2.0.0/go.mod h1:276TDNr25NENBA/NQZUuEIlwxy/I5CYVFIr/d2TgOdU= +github.com/gin-contrib/pprof v1.5.3 h1:Bj5SxJ3kQDVez/s/+f9+meedJIqLS+xlkIVDe/lcvgM= +github.com/gin-contrib/pprof v1.5.3/go.mod h1:0+LQSZ4SLO0B6+2n6JBzaEygpTBxe/nI+YEYpfQQ6xY= +github.com/gin-contrib/secure v1.1.2 h1:6G8/NCOTSywWY7TeaH/0Yfaa6bfkE5ukkqtIm7lK11U= +github.com/gin-contrib/secure v1.1.2/go.mod h1:xI3jI5/BpOYMCBtjgmIVrMA3kI7y9LwCFxs+eLf5S3w= +github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U= +github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs= +github.com/gin-contrib/slog v1.2.0 h1:vAxZfr7knD1ZYK5+pMJLP52sZXIkJXkcRPa/0dx9hSk= +github.com/gin-contrib/slog v1.2.0/go.mod h1:vYK6YltmpsEFkO0zfRMLTKHrWS3DwUSn0TMpT+kMagI= +github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= +github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= +github.com/gin-contrib/static v1.1.5 h1:bAPqT4KTZN+4uDY1b90eSrD1t8iNzod7Jj8njwmnzz4= +github.com/gin-contrib/static v1.1.5/go.mod h1:8JSEXwZHcQ0uCrLPcsvnAJ4g+ODxeupP8Zetl9fd8wM= +github.com/gin-contrib/timeout v1.1.0 h1:WAmWseo5gfBUbMrMJu5hJxDclehfSJUmK2wGwCC/EFw= +github.com/gin-contrib/timeout v1.1.0/go.mod h1:NpRo4gd1Ad8ZQ4T6bQLVFDqiplCmPRs2nvfckxS2Fw4= +github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8= +github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc= +github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs= +github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08= +github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +github.com/go-openapi/jsonpointer v0.22.5 h1:8on/0Yp4uTb9f4XvTrM2+1CPrV05QPZXu+rvu2o9jcA= +github.com/go-openapi/jsonpointer v0.22.5/go.mod h1:gyUR3sCvGSWchA2sUBJGluYMbe1zazrYWIkWPjjMUY0= +github.com/go-openapi/jsonreference v0.21.5 h1:6uCGVXU/aNF13AQNggxfysJ+5ZcU4nEAe+pJyVWRdiE= +github.com/go-openapi/jsonreference v0.21.5/go.mod h1:u25Bw85sX4E2jzFodh1FOKMTZLcfifd1Q+iKKOUxExw= +github.com/go-openapi/spec v0.22.4 h1:4pxGjipMKu0FzFiu/DPwN3CTBRlVM2yLf/YTWorYfDQ= +github.com/go-openapi/spec v0.22.4/go.mod h1:WQ6Ai0VPWMZgMT4XySjlRIE6GP1bGQOtEThn3gcWLtQ= +github.com/go-openapi/swag v0.19.15 h1:D2NRCBzS9/pEY3gP9Nl8aDqGUcPFrwG2p+CNFrLyrCM= +github.com/go-openapi/swag/conv v0.25.5 h1:wAXBYEXJjoKwE5+vc9YHhpQOFj2JYBMF2DUi+tGu97g= +github.com/go-openapi/swag/conv v0.25.5/go.mod h1:CuJ1eWvh1c4ORKx7unQnFGyvBbNlRKbnRyAvDvzWA4k= +github.com/go-openapi/swag/jsonname v0.25.5 h1:8p150i44rv/Drip4vWI3kGi9+4W9TdI3US3uUYSFhSo= +github.com/go-openapi/swag/jsonname v0.25.5/go.mod h1:jNqqikyiAK56uS7n8sLkdaNY/uq6+D2m2LANat09pKU= +github.com/go-openapi/swag/jsonutils v0.25.5 h1:XUZF8awQr75MXeC+/iaw5usY/iM7nXPDwdG3Jbl9vYo= +github.com/go-openapi/swag/jsonutils v0.25.5/go.mod h1:48FXUaz8YsDAA9s5AnaUvAmry1UcLcNVWUjY42XkrN4= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5 h1:SX6sE4FrGb4sEnnxbFL/25yZBb5Hcg1inLeErd86Y1U= +github.com/go-openapi/swag/jsonutils/fixtures_test v0.25.5/go.mod h1:/2KvOTrKWjVA5Xli3DZWdMCZDzz3uV/T7bXwrKWPquo= +github.com/go-openapi/swag/loading v0.25.5 h1:odQ/umlIZ1ZVRteI6ckSrvP6e2w9UTF5qgNdemJHjuU= +github.com/go-openapi/swag/loading v0.25.5/go.mod h1:I8A8RaaQ4DApxhPSWLNYWh9NvmX2YKMoB9nwvv6oW6g= +github.com/go-openapi/swag/stringutils v0.25.5 h1:NVkoDOA8YBgtAR/zvCx5rhJKtZF3IzXcDdwOsYzrB6M= +github.com/go-openapi/swag/stringutils v0.25.5/go.mod h1:PKK8EZdu4QJq8iezt17HM8RXnLAzY7gW0O1KKarrZII= +github.com/go-openapi/swag/typeutils v0.25.5 h1:EFJ+PCga2HfHGdo8s8VJXEVbeXRCYwzzr9u4rJk7L7E= +github.com/go-openapi/swag/typeutils v0.25.5/go.mod h1:itmFmScAYE1bSD8C4rS0W+0InZUBrB2xSPbWt6DLGuc= +github.com/go-openapi/swag/yamlutils v0.25.5 h1:kASCIS+oIeoc55j28T4o8KwlV2S4ZLPT6G0iq2SSbVQ= +github.com/go-openapi/swag/yamlutils v0.25.5/go.mod h1:Gek1/SjjfbYvM+Iq4QGwa/2lEXde9n2j4a3wI3pNuOQ= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0 h1:7SgOMTvJkM8yWrQlU8Jm18VeDPuAvB/xWrdxFJkoFag= +github.com/go-openapi/testify/enable/yaml/v2 v2.4.0/go.mod h1:14iV8jyyQlinc9StD7w1xVPW3CO3q1Gj04Jy//Kw4VM= +github.com/go-openapi/testify/v2 v2.4.0 h1:8nsPrHVCWkQ4p8h1EsRVymA2XABB4OT40gcvAu+voFM= +github.com/go-openapi/testify/v2 v2.4.0/go.mod h1:HCPmvFFnheKK2BuwSA0TbbdxJ3I16pjwMkYkP4Ywn54= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w= +github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM= +github.com/go-viper/mapstructure/v2 v2.5.0 h1:vM5IJoUAy3d7zRSVtIwQgBj7BiWtMPfmPEgAXnvj1Ro= +github.com/go-viper/mapstructure/v2 v2.5.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= +github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU= +github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= +github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= +github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/golang/mock v1.4.4 h1:l75CXGRSwbaYNpl/Z2X1XIIAMSCquvXgpVZDhwEIJsc= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= +github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM= +github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA= +github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= +github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ= +github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik= +github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= +github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y= +github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728 h1:QwWKgMY28TAXaDl+ExRDqGQltzXqN/xypdKP86niVn8= github.com/ledongthuc/pdf v0.0.0-20250511090121-5959a4027728/go.mod h1:1fEHWurg7pvf5SG6XNE5Q8UZmOwex51Mkx3SLhrW5B4= +github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= +github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/lucasb-eyer/go-colorful v1.3.0 h1:2/yBRLdWBZKrf7gB40FoiKfAWYQ0lqNcbuQwVHXptag= github.com/lucasb-eyer/go-colorful v1.3.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0= github.com/mailru/easyjson v0.9.2 h1:dX8U45hQsZpxd80nLvDGihsQ/OxlvTkVUXH2r/8cb2M= @@ -77,6 +214,11 @@ github.com/mattn/go-localereader v0.0.1 h1:ygSAOl7ZXTx4RdPYinUpg6W99U8jWvWi9Ye2J github.com/mattn/go-localereader v0.0.1/go.mod h1:8fBrzywKY7BI3czFoHkuzRoWE9C+EiG4R1k4Cjx5p88= github.com/mattn/go-runewidth v0.0.21 h1:jJKAZiQH+2mIinzCJIaIG9Be1+0NR+5sz/lYEEjdM8w= github.com/mattn/go-runewidth v0.0.21/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6 h1:ZK8zHtRHOkbHy6Mmr5D264iyp3TiX5OmNcI5cIARiQI= github.com/muesli/ansi v0.0.0-20230316100256-276c6243b2f6/go.mod h1:CJlz5H+gyd6CUWT45Oy4q24RdLyn7Md9Vj2/ldJBSIo= github.com/muesli/cancelreader v0.2.2 h1:3I4Kt4BQjOR54NavqnDogx/MIoWBFa0StPA8ELUXHmA= @@ -85,30 +227,73 @@ github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk= github.com/ollama/ollama v0.18.1 h1:7K6anW64C2keASpToYfuOa00LuP8aCmofLKcT2c1mlY= github.com/ollama/ollama v0.18.1/go.mod h1:tCX4IMV8DHjl3zY0THxuEkpWDZSOchJpzTuLACpMwFw= +github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4= +github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/qdrant/go-client v1.17.1 h1:7QmPwDddrHL3hC4NfycwtQlraVKRLcRi++BX6TTm+3g= github.com/qdrant/go-client v1.17.1/go.mod h1:n1h6GhkdAzcohoXt/5Z19I2yxbCkMA6Jejob3S6NZT8= +github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= +github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= +github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= +github.com/quic-go/quic-go v0.59.0/go.mod h1:upnsH4Ju1YkqpLXC305eW3yDZ4NfnNbmQRCMWS58IKU= github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ= github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +github.com/sergi/go-diff v1.4.0 h1:n/SP9D5ad1fORl+llWyN+D6qoUETXNZARKjyY2/KVCw= +github.com/sergi/go-diff v1.4.0/go.mod h1:A0bzQcvG0E7Rwjx0REVgAGH58e96+X0MeOfepqsbeW4= +github.com/sosodev/duration v1.4.0 h1:35ed0KiVFriGHHzZZJaZLgmTEEICIyt8Sx0RQfj9IjE= +github.com/sosodev/duration v1.4.0/go.mod h1:RQIBBX0+fMLc/D9+Jb/fwvVmo0eZvDDEERAikUR6SDg= github.com/spf13/cobra v1.10.2 h1:DMTTonx5m65Ic0GOoRY2c16WCbHxOOw6xxezuLaBpcU= github.com/spf13/cobra v1.10.2/go.mod h1:7C1pvHqHw5A4vrJfjNwvOdzYu0Gml16OCs2GRiTUUS4= github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/pflag v1.0.10 h1:4EBh2KAYBwaONj6b2Ye1GiHfwjqyROoF4RwYO+vPwFk= github.com/spf13/pflag v1.0.10/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/swaggo/files v1.0.1 h1:J1bVJ4XHZNq0I46UU90611i9/YzdrF7x92oX1ig5IdE= +github.com/swaggo/files v1.0.1/go.mod h1:0qXmMNH6sXNf+73t65aKeB+ApmgxdnkQzVTAj2uaMUg= +github.com/swaggo/gin-swagger v1.6.1 h1:Ri06G4gc9N4t4k8hekMigJ9zKTFSlqj/9paAQCQs7cY= +github.com/swaggo/gin-swagger v1.6.1/go.mod h1:LQ+hJStHakCWRiK/YNYtJOu4mR2FP+pxLnILT/qNiTw= +github.com/swaggo/swag v1.16.6 h1:qBNcx53ZaX+M5dxVyTrgQ0PJ/ACK+NzhwcbieTt+9yI= +github.com/swaggo/swag v1.16.6/go.mod h1:ngP2etMK5a0P3QBizic5MEwpRmluJZPHjXcMoj4Xesg= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY= +github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4= +github.com/vektah/gqlparser/v2 v2.5.32 h1:k9QPJd4sEDTL+qB4ncPLflqTJ3MmjB9SrVzJrawpFSc= +github.com/vektah/gqlparser/v2 v2.5.32/go.mod h1:c1I28gSOVNzlfc4WuDlqU7voQnsqI6OG2amkBAFmgts= github.com/wk8/go-ordered-map/v2 v2.1.8 h1:5h/BUHu93oj4gIdvHHHGsScSTMijfx5PeYkE/fJgbpc= github.com/wk8/go-ordered-map/v2 v2.1.8/go.mod h1:5nJHM5DyteebpVlHnWMV0rPz6Zp7+xBAnxjb1X5vnTw= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no= github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM= +github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= +github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0 h1:E7DmskpIO7ZR6QI6zKSEKIDNUYoKw9oHXP23gzbCdU0= +go.opentelemetry.io/contrib/instrumentation/github.com/gin-gonic/gin/otelgin v0.67.0/go.mod h1:WB2cS9y+AwqqKhoo9gw6/ZxlSjFBUQGZ8BQOaD3FVXM= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0 h1:B2Pew5ufEtgkjLF+tSkXjgYZXQr9m7aCm1wLKB0URbU= +go.opentelemetry.io/contrib/propagators/b3 v1.42.0/go.mod h1:iPgUcSEF5DORW6+yNbdw/YevUy+QqJ508ncjhrRSCjc= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0 h1:s/1iRkCKDfhlh1JF26knRneorus8aOwVIDhvYx9WoDw= +go.opentelemetry.io/otel/exporters/stdout/stdouttrace v1.42.0/go.mod h1:UI3wi0FXg1Pofb8ZBiBLhtMzgoTm1TYkMvn71fAqDzs= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI= go.opentelemetry.io/otel/sdk v1.42.0 h1:LyC8+jqk6UJwdrI/8VydAq/hvkFKNHZVIWuslJXYsDo= @@ -117,21 +302,64 @@ go.opentelemetry.io/otel/sdk/metric v1.42.0 h1:D/1QR46Clz6ajyZ3G8SgNlTJKBdGp84q9 go.opentelemetry.io/otel/sdk/metric v1.42.0/go.mod h1:Ua6AAlDKdZ7tdvaQKfSmnFTdHx37+J4ba8MwVCYM5hc= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= +go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y= +go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU= +go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= go.yaml.in/yaml/v3 v3.0.4/go.mod h1:DhzuOOF2ATzADvBadXxruRBLzYTpT36CKvDb3+aBEFg= -golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4= -golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA= +golang.org/x/arch v0.25.0 h1:qnk6Ksugpi5Bz32947rkUgDt9/s5qvqDPl/gBKdMJLE= +golang.org/x/arch v0.25.0/go.mod h1:0X+GdSIP+kL5wPmpK7sdkEVTt2XoYP0cSjQSbZBwOi8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.50.0 h1:zO47/JPrL6vsNkINmLoo/PH1gcxpls50DNogFvB5ZGI= +golang.org/x/crypto v0.50.0/go.mod h1:3muZ7vA7PBCE6xgPX7nkzzjiUq87kRItoJQM1Yo8S+Q= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90 h1:jiDhWWeC7jfWqR9c/uplMOqJ0sbNlNWv0UkzE0vX1MA= golang.org/x/exp v0.0.0-20260312153236-7ab1446f8b90/go.mod h1:xE1HEv6b+1SCZ5/uscMRjUBKtIxworgEcEi+/n9NQDQ= -golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0= -golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/mod v0.34.0 h1:xIHgNUUnW6sYkcM5Jleh05DvLOtwc6RitGHbDk4akRI= +golang.org/x/mod v0.34.0/go.mod h1:ykgH52iCZe79kzLLMhyCUzhMci+nQj+0XkbXpNYtVjY= +golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= +golang.org/x/net v0.53.0 h1:d+qAbo5L0orcWAr0a9JweQpjXF19LMXJE8Ey7hwOdUA= +golang.org/x/net v0.53.0/go.mod h1:JvMuJH7rrdiCfbeHoo3fCQU24Lf5JJwT9W3sJFulfgs= +golang.org/x/oauth2 v0.36.0 h1:peZ/1z27fi9hUOFCAZaHyrpWG5lwe0RJEEEeH0ThlIs= +golang.org/x/oauth2 v0.36.0/go.mod h1:YDBUJMTkDnJS+A4BP4eZBjCqtokkg1hODuPjwiGPO7Q= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU= -golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A= -golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8= -golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA= +golang.org/x/sys v0.43.0 h1:Rlag2XtaFTxp19wS8MXlJwTvoh8ArU6ezoyFsMyCTNI= +golang.org/x/sys v0.43.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/term v0.42.0 h1:UiKe+zDFmJobeJ5ggPwOshJIVt6/Ft0rcfrXZDLWAWY= +golang.org/x/term v0.42.0/go.mod h1:Dq/D+snpsbazcBG5+F9Q1n2rXV8Ma+71xEjTRufARgY= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= +golang.org/x/text v0.36.0 h1:JfKh3XmcRPqZPKevfXVpI1wXPTqbkE5f7JA92a55Yxg= +golang.org/x/text v0.36.0/go.mod h1:NIdBknypM8iqVmPiuco0Dh6P5Jcdk8lJL0CUebqK164= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/tools v0.43.0 h1:12BdW9CeB3Z+J/I/wj34VMl8X+fEXBxVR90JeMX5E7s= +golang.org/x/tools v0.43.0/go.mod h1:uHkMso649BX2cZK6+RpuIPXS3ho2hZo4FVwfoy1vIk0= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4= gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E= google.golang.org/genproto/googleapis/rpc v0.0.0-20260316180232-0b37fe3546d5 h1:aJmi6DVGGIStN9Mobk/tZOOQUBbj0BPjZjjnOdoZKts= @@ -143,5 +371,6 @@ google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mcp/ax7_test.go b/mcp/ax7_test.go new file mode 100644 index 0000000..f947deb --- /dev/null +++ b/mcp/ax7_test.go @@ -0,0 +1,554 @@ +package mcp + +import ( + "context" + "os" + + core "dappco.re/go" +) + +type ax7Subsystem struct { + called *bool + err error +} + +func (s ax7Subsystem) Name() string { return "ax7" } + +func (s ax7Subsystem) RegisterTools(*Service) { + if s.called != nil { + *s.called = true + } +} + +func (s ax7Subsystem) Shutdown(context.Context) error { + if s.called != nil { + *s.called = true + } + return s.err +} + +func TestMCP_New_Good(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + names := service.ToolNames() + + core.AssertNoError(t, err) + core.AssertTrue(t, len(names) > 0) +} + +func TestMCP_New_Bad(t *core.T) { + service, err := New(42) + got := core.ErrorMessage(err) + + core.AssertNil(t, service) + core.AssertError(t, err) + core.AssertContains(t, got, "unsupported") +} + +func TestMCP_New_Ugly(t *core.T) { + service, err := New(Options{Unrestricted: true}) + root := service.WorkspaceRoot() + + core.AssertNoError(t, err) + core.AssertEqual(t, "", root) +} + +func TestMCP_WithWorkspaceRoot_Good(t *core.T) { + service := &Service{} + option := WithWorkspaceRoot(t.TempDir()) + err := option(service) + + core.AssertNoError(t, err) + core.AssertNotEqual(t, "", service.WorkspaceRoot()) +} + +func TestMCP_WithWorkspaceRoot_Bad(t *core.T) { + service := &Service{workspaceRoot: "before"} + option := WithWorkspaceRoot("") + err := option(service) + + core.AssertNoError(t, err) + core.AssertEqual(t, "", service.WorkspaceRoot()) +} + +func TestMCP_WithWorkspaceRoot_Ugly(t *core.T) { + service := &Service{} + option := WithWorkspaceRoot(".") + err := option(service) + + core.AssertNoError(t, err) + core.AssertTrue(t, service.WorkspaceRoot() != ".") +} + +func TestMCP_WithProcessService_Good(t *core.T) { + service := &Service{} + option := WithProcessService("process") + err := option(service) + + core.AssertNoError(t, err) + core.AssertEqual(t, "process", service.processService) +} + +func TestMCP_WithProcessService_Bad(t *core.T) { + service := &Service{processService: "before"} + option := WithProcessService(nil) + err := option(service) + + core.AssertNoError(t, err) + core.AssertNil(t, service.processService) +} + +func TestMCP_WithProcessService_Ugly(t *core.T) { + service := &Service{} + payload := map[string]bool{"ok": true} + err := WithProcessService(payload)(service) + + core.AssertNoError(t, err) + core.AssertEqual(t, payload, service.processService) +} + +func TestMCP_WithWSHub_Good(t *core.T) { + service := &Service{} + option := WithWSHub("hub") + err := option(service) + + core.AssertNoError(t, err) + core.AssertEqual(t, "hub", service.wsHub) +} + +func TestMCP_WithWSHub_Bad(t *core.T) { + service := &Service{wsHub: "before"} + option := WithWSHub(nil) + err := option(service) + + core.AssertNoError(t, err) + core.AssertNil(t, service.wsHub) +} + +func TestMCP_WithWSHub_Ugly(t *core.T) { + service := &Service{} + payload := map[string]bool{"connected": true} + err := WithWSHub(payload)(service) + + core.AssertNoError(t, err) + core.AssertEqual(t, payload, service.wsHub) +} + +func TestMCP_WithSubsystem_Good(t *core.T) { + service := &Service{} + sub := ax7Subsystem{} + err := WithSubsystem(sub)(service) + + core.AssertNoError(t, err) + core.AssertLen(t, service.subsystems, 1) +} + +func TestMCP_WithSubsystem_Bad(t *core.T) { + service := &Service{} + err := WithSubsystem(nil)(service) + got := len(service.subsystems) + + core.AssertNoError(t, err) + core.AssertEqual(t, 0, got) +} + +func TestMCP_WithSubsystem_Ugly(t *core.T) { + service := &Service{} + first := ax7Subsystem{} + second := ax7Subsystem{} + + core.AssertNoError(t, WithSubsystem(first)(service)) + core.AssertNoError(t, WithSubsystem(second)(service)) + core.AssertLen(t, service.subsystems, 2) +} + +func TestMCP_Service_WorkspaceRoot_Good(t *core.T) { + service := &Service{workspaceRoot: "/repo"} + got := service.WorkspaceRoot() + want := "/repo" + + core.AssertEqual(t, want, got) + core.AssertNotEqual(t, "", got) +} + +func TestMCP_Service_WorkspaceRoot_Bad(t *core.T) { + service := &Service{} + got := service.WorkspaceRoot() + want := "" + + core.AssertEqual(t, want, got) + core.AssertEmpty(t, got) +} + +func TestMCP_Service_WorkspaceRoot_Ugly(t *core.T) { + service := &Service{workspaceRoot: ""} + got := service.WorkspaceRoot() + unrestricted := got == "" + + core.AssertTrue(t, unrestricted) + core.AssertEqual(t, "", got) +} + +func TestMCP_Service_Tools_Good(t *core.T) { + handler := typedHandler(func(context.Context, struct{}) (map[string]bool, error) { return map[string]bool{"ok": true}, nil }) + service := &Service{tools: map[string]Tool{"x": {Name: "x", InputSchema: objectSchema(), Handler: handler}}, toolOrder: []string{"x"}} + records := service.Tools() + + core.AssertLen(t, records, 1) + core.AssertEqual(t, "x", records[0].Name) +} + +func TestMCP_Service_Tools_Bad(t *core.T) { + service := &Service{tools: map[string]Tool{}, toolOrder: nil} + records := service.Tools() + got := len(records) + + core.AssertEqual(t, 0, got) + core.AssertEmpty(t, records) +} + +func TestMCP_Service_Tools_Ugly(t *core.T) { + handler := typedHandler(func(context.Context, struct{}) (map[string]bool, error) { return map[string]bool{"ok": true}, nil }) + service := &Service{tools: map[string]Tool{"x": {Name: "x", InputSchema: objectSchema(), Handler: handler}}, toolOrder: []string{"x"}} + records := service.Tools() + + records[0].InputSchema["mutated"] = true + core.AssertNil(t, service.tools["x"].InputSchema["mutated"]) +} + +func TestMCP_Service_ToolNames_Good(t *core.T) { + service := &Service{toolOrder: []string{"a", "b"}} + names := service.ToolNames() + got := core.Join(",", names...) + + core.AssertEqual(t, "a,b", got) + core.AssertLen(t, names, 2) +} + +func TestMCP_Service_ToolNames_Bad(t *core.T) { + service := &Service{} + names := service.ToolNames() + got := len(names) + + core.AssertEqual(t, 0, got) + core.AssertEmpty(t, names) +} + +func TestMCP_Service_ToolNames_Ugly(t *core.T) { + service := &Service{toolOrder: []string{"a"}} + names := service.ToolNames() + names[0] = "mutated" + + core.AssertEqual(t, []string{"a"}, service.ToolNames()) + core.AssertEqual(t, []string{"mutated"}, names) +} + +func TestMCP_Service_RegisterTool_Good(t *core.T) { + handler := typedHandler(func(context.Context, struct{}) (map[string]bool, error) { return map[string]bool{"ok": true}, nil }) + service := &Service{tools: map[string]Tool{}} + err := service.RegisterTool(Tool{Name: "custom", Handler: handler}) + + core.AssertNoError(t, err) + core.AssertEqual(t, []string{"custom"}, service.ToolNames()) +} + +func TestMCP_Service_RegisterTool_Bad(t *core.T) { + service := &Service{tools: map[string]Tool{}} + err := service.RegisterTool(Tool{Name: "", Handler: typedHandler(func(context.Context, struct{}) (map[string]bool, error) { return nil, nil })}) + got := core.ErrorMessage(err) + + core.AssertError(t, err) + core.AssertContains(t, got, "name is required") +} + +func TestMCP_Service_RegisterTool_Ugly(t *core.T) { + handler := typedHandler(func(context.Context, struct{}) (map[string]bool, error) { return map[string]bool{"ok": true}, nil }) + service := &Service{tools: map[string]Tool{}} + err := service.RegisterTool(Tool{Name: "custom", Handler: handler}) + + core.AssertNoError(t, err) + core.AssertEqual(t, "object", service.tools["custom"].InputSchema["type"]) +} + +func TestMCP_Service_RegisterToolFunc_Good(t *core.T) { + handler := typedHandler(func(context.Context, struct{}) (map[string]bool, error) { return map[string]bool{"ok": true}, nil }) + service := &Service{tools: map[string]Tool{}} + err := service.RegisterToolFunc("group", "custom", "Custom tool", handler) + + core.AssertNoError(t, err) + core.AssertEqual(t, "group", service.tools["custom"].Group) +} + +func TestMCP_Service_RegisterToolFunc_Bad(t *core.T) { + service := &Service{tools: map[string]Tool{}} + err := service.RegisterToolFunc("group", "", "Custom tool", nil) + got := core.ErrorMessage(err) + + core.AssertError(t, err) + core.AssertContains(t, got, "name is required") +} + +func TestMCP_Service_RegisterToolFunc_Ugly(t *core.T) { + handler := typedHandler(func(context.Context, struct{}) (map[string]bool, error) { return map[string]bool{"ok": true}, nil }) + service := &Service{tools: map[string]Tool{}} + err := service.RegisterToolFunc("", "custom", "", handler) + + core.AssertNoError(t, err) + core.AssertEqual(t, "", service.tools["custom"].Group) +} + +func TestMCP_Service_Shutdown_Good(t *core.T) { + called := false + service := &Service{subsystems: []Subsystem{ax7Subsystem{called: &called}}} + err := service.Shutdown(core.Background()) + + core.AssertNoError(t, err) + core.AssertTrue(t, called) +} + +func TestMCP_Service_Shutdown_Bad(t *core.T) { + service := &Service{subsystems: []Subsystem{ax7Subsystem{err: core.AnError}}} + err := service.Shutdown(core.Background()) + got := core.ErrorMessage(err) + + core.AssertError(t, err) + core.AssertContains(t, got, core.AnError.Error()) +} + +func TestMCP_Service_Shutdown_Ugly(t *core.T) { + service := &Service{processes: map[string]*managedProcess{}} + err := service.Shutdown(core.Background()) + got := len(service.processes) + + core.AssertNoError(t, err) + core.AssertEqual(t, 0, got) +} + +func TestMCP_Service_HandleFrame_Good(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + response, err := service.HandleFrame(core.Background(), []byte(`{"jsonrpc":"2.0","id":1,"method":"ping"}`)) + + core.AssertNoError(t, err) + core.AssertContains(t, string(response), `"result"`) +} + +func TestMCP_Service_HandleFrame_Bad(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + response, err := service.HandleFrame(core.Background(), []byte(`{bad json`)) + + core.AssertError(t, err) + core.AssertContains(t, string(response), "parse error") +} + +func TestMCP_Service_HandleFrame_Ugly(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + response, err := service.HandleFrame(core.Background(), []byte(`{"jsonrpc":"2.0","method":"notifications/initialized"}`)) + + core.AssertNoError(t, err) + core.AssertNil(t, response) +} + +func TestMCP_Service_ServeStdio_Good(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + oldReader, oldWriter := stdioReader, stdioWriter + defer func() { stdioReader, stdioWriter = oldReader, oldWriter }() + + var output safeBuffer + stdioReader = core.NewReader(`{"jsonrpc":"2.0","id":1,"method":"tools/list"}` + "\n") + stdioWriter = &output + err = service.ServeStdio(core.Background()) + + core.AssertNoError(t, err) + core.AssertContains(t, output.String(), `"tools"`) +} + +func TestMCP_Service_ServeStdio_Bad(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + oldReader, oldWriter := stdioReader, stdioWriter + defer func() { stdioReader, stdioWriter = oldReader, oldWriter }() + + var output safeBuffer + stdioReader = core.NewReader("{bad json\n") + stdioWriter = &output + err = service.ServeStdio(core.Background()) + + core.AssertNoError(t, err) + core.AssertContains(t, output.String(), "parse error") +} + +func TestMCP_Service_ServeStdio_Ugly(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + oldReader, oldWriter := stdioReader, stdioWriter + defer func() { stdioReader, stdioWriter = oldReader, oldWriter }() + + stdioReader = core.NewReader("") + stdioWriter = &safeBuffer{} + err = service.ServeStdio(core.Background()) + + core.AssertNoError(t, err) + core.AssertEqual(t, []string{}, []string{}) +} + +func TestMCP_Service_ServeTCP_Good(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + addr := reserveTCPAddr(t) + ctx, cancel := core.WithCancel(core.Background()) + + errCh := make(chan error, 1) + go func() { errCh <- service.ServeTCP(ctx, addr) }() + waitForTCP(t, addr) + cancel() + core.AssertNoError(t, <-errCh) +} + +func TestMCP_Service_ServeTCP_Bad(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + err = service.ServeTCP(core.Background(), "127.0.0.1:bad") + + core.AssertError(t, err) + core.AssertContains(t, core.ErrorMessage(err), "listen") +} + +func TestMCP_Service_ServeTCP_Ugly(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + err = service.ServeTCP(core.Background(), "256.256.256.256:1") + + core.AssertError(t, err) + core.AssertContains(t, core.ErrorMessage(err), "listen") +} + +func TestMCP_Service_ServeUnix_Good(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + socketPath := core.Path(t.TempDir(), "mcp.sock") + ctx, cancel := core.WithCancel(core.Background()) + + errCh := make(chan error, 1) + go func() { errCh <- service.ServeUnix(ctx, socketPath) }() + waitForUnix(t, socketPath) + cancel() + core.AssertNoError(t, <-errCh) +} + +func TestMCP_Service_ServeUnix_Bad(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + err = service.ServeUnix(core.Background(), "\x00") + + core.AssertError(t, err) + core.AssertContains(t, core.ErrorMessage(err), "invalid") +} + +func TestMCP_Service_ServeUnix_Ugly(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + socketPath := core.Path(t.TempDir(), "mcp.sock") + core.AssertNoError(t, os.WriteFile(socketPath, []byte("stale socket"), 0o600)) + ctx, cancel := core.WithCancel(core.Background()) + + errCh := make(chan error, 1) + go func() { errCh <- service.ServeUnix(ctx, socketPath) }() + waitForUnix(t, socketPath) + cancel() + core.AssertNoError(t, <-errCh) +} + +func TestMCP_Service_Run_Good(t *core.T) { + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + oldReader, oldWriter := stdioReader, stdioWriter + defer func() { stdioReader, stdioWriter = oldReader, oldWriter }() + + var output safeBuffer + stdioReader = core.NewReader(`{"jsonrpc":"2.0","id":1,"method":"ping"}` + "\n") + stdioWriter = &output + err = service.Run(core.Background()) + + core.AssertNoError(t, err) + core.AssertContains(t, output.String(), `"result"`) +} + +func TestMCP_Service_Run_Bad(t *core.T) { + t.Setenv("MCP_ADDR", "127.0.0.1:bad") + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + + err = service.Run(core.Background()) + core.AssertError(t, err) + core.AssertContains(t, core.ErrorMessage(err), "listen") +} + +func TestMCP_Service_Run_Ugly(t *core.T) { + t.Setenv("MCP_UNIX_SOCKET", core.Path(t.TempDir(), "socket-name-that-is-intentionally-too-long-for-a-unix-domain-socket-path-because-the-kernel-limit-is-small")) + service, err := New(WithWorkspaceRoot(t.TempDir())) + core.RequireNoError(t, err) + + err = service.Run(core.Background()) + core.AssertError(t, err) + core.AssertContains(t, core.ErrorMessage(err), "invalid") +} + +func TestMCP_Buffer_Write_Good(t *core.T) { + var buffer safeBuffer + n, err := buffer.Write([]byte("agent")) + got := buffer.String() + + core.AssertNoError(t, err) + core.AssertEqual(t, 5, n) + core.AssertEqual(t, "agent", got) +} + +func TestMCP_Buffer_Write_Bad(t *core.T) { + var buffer safeBuffer + n, err := buffer.Write(nil) + got := buffer.String() + + core.AssertNoError(t, err) + core.AssertEqual(t, 0, n) + core.AssertEqual(t, "", got) +} + +func TestMCP_Buffer_Write_Ugly(t *core.T) { + var buffer safeBuffer + first, firstErr := buffer.Write([]byte("agent")) + second, secondErr := buffer.Write([]byte("-ready")) + + core.AssertNoError(t, firstErr) + core.AssertNoError(t, secondErr) + core.AssertEqual(t, 11, first+second) +} + +func TestMCP_Buffer_String_Good(t *core.T) { + var buffer safeBuffer + _, err := buffer.Write([]byte("agent")) + got := buffer.String() + + core.AssertNoError(t, err) + core.AssertEqual(t, "agent", got) +} + +func TestMCP_Buffer_String_Bad(t *core.T) { + var buffer safeBuffer + got := buffer.String() + want := "" + + core.AssertEqual(t, want, got) + core.AssertEmpty(t, got) +} + +func TestMCP_Buffer_String_Ugly(t *core.T) { + var buffer safeBuffer + _, err := buffer.Write([]byte("agent")) + first := buffer.String() + + core.AssertNoError(t, err) + core.AssertEqual(t, first, buffer.String()) +} diff --git a/mcp/tools_external.go b/mcp/tools_external.go index 9b46809..dbe292c 100644 --- a/mcp/tools_external.go +++ b/mcp/tools_external.go @@ -619,7 +619,9 @@ func (p *managedProcess) wait() { } } if p.stdin != nil { - _ = p.stdin.Close() + if closeErr := p.stdin.Close(); closeErr != nil && p.errText == "" { + p.errText = closeErr.Error() + } } } diff --git a/mcp/transport_tcp.go b/mcp/transport_tcp.go index 4362751..56090b6 100644 --- a/mcp/transport_tcp.go +++ b/mcp/transport_tcp.go @@ -2,6 +2,7 @@ package mcp import ( "context" + "errors" "fmt" "net" "os" @@ -29,7 +30,9 @@ func (s *Service) ServeTCP(ctx context.Context, addr string) error { go func() { <-ctx.Done() - _ = listener.Close() + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + fmt.Fprintln(os.Stderr, "MCP TCP listener close error:", err) + } }() for { @@ -62,7 +65,11 @@ func (s *Service) serveConn(ctx context.Context, conn net.Conn) { defer conn.Close() go func() { <-ctx.Done() - _ = conn.Close() + if err := conn.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + fmt.Fprintln(os.Stderr, "MCP TCP connection close error:", err) + } }() - _ = serveReaderWriter(ctx, conn, conn, s.HandleFrame) + if err := serveReaderWriter(ctx, conn, conn, s.HandleFrame); err != nil && !errors.Is(err, net.ErrClosed) { + fmt.Fprintln(os.Stderr, "MCP TCP connection error:", err) + } } diff --git a/mcp/transport_unix.go b/mcp/transport_unix.go index 3c8332a..9c2a042 100644 --- a/mcp/transport_unix.go +++ b/mcp/transport_unix.go @@ -2,6 +2,7 @@ package mcp import ( "context" + "errors" "fmt" "net" "os" @@ -19,20 +20,28 @@ func (s *Service) ServeUnix(ctx context.Context, socketPath string) error { if err := os.MkdirAll(filepath.Dir(socketPath), 0o755); err != nil { return err } - _ = os.Remove(socketPath) + if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) { + return err + } listener, err := net.Listen("unix", socketPath) if err != nil { return err } defer func() { - _ = listener.Close() - _ = os.Remove(socketPath) + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + fmt.Fprintln(os.Stderr, "MCP Unix listener close error:", err) + } + if err := os.Remove(socketPath); err != nil && !errors.Is(err, os.ErrNotExist) { + fmt.Fprintln(os.Stderr, "MCP Unix socket cleanup error:", err) + } }() go func() { <-ctx.Done() - _ = listener.Close() + if err := listener.Close(); err != nil && !errors.Is(err, net.ErrClosed) { + fmt.Fprintln(os.Stderr, "MCP Unix listener close error:", err) + } }() for { diff --git a/pkg/api/ax7_test.go b/pkg/api/ax7_test.go new file mode 100644 index 0000000..2b1a6e7 --- /dev/null +++ b/pkg/api/ax7_test.go @@ -0,0 +1,179 @@ +package api + +import ( + "net/http" + "net/http/httptest" + + core "dappco.re/go" + "github.com/gin-gonic/gin" +) + +func TestAPI_New_Good(t *core.T) { + provider := New() + name := provider.Name() + basePath := provider.BasePath() + + core.AssertNotNil(t, provider) + core.AssertEqual(t, "ai", name) + core.AssertEqual(t, "/v1", basePath) +} + +func TestAPI_New_Bad(t *core.T) { + first := New() + second := New() + same := first == second + + core.AssertNotNil(t, first) + core.AssertNotNil(t, second) + core.AssertFalse(t, same) +} + +func TestAPI_New_Ugly(t *core.T) { + provider := New() + descriptions := provider.Describe() + got := len(descriptions) + + core.AssertTrue(t, got > 0) + core.AssertEqual(t, "ai", provider.Name()) +} + +func TestAPI_NewProvider_Good(t *core.T) { + provider := NewProvider() + name := provider.Name() + basePath := provider.BasePath() + + core.AssertNotNil(t, provider) + core.AssertEqual(t, "ai", name) + core.AssertEqual(t, "/v1", basePath) +} + +func TestAPI_NewProvider_Bad(t *core.T) { + first := NewProvider() + second := NewProvider() + same := first == second + + core.AssertNotNil(t, first) + core.AssertNotNil(t, second) + core.AssertFalse(t, same) +} + +func TestAPI_NewProvider_Ugly(t *core.T) { + provider := NewProvider() + descriptions := provider.Describe() + got := len(descriptions) + + core.AssertEqual(t, 6, got) + core.AssertEqual(t, "ai", provider.Name()) +} + +func TestAPI_AIProvider_Name_Good(t *core.T) { + provider := &AIProvider{} + got := provider.Name() + want := "ai" + + core.AssertEqual(t, want, got) + core.AssertNotEqual(t, "", got) +} + +func TestAPI_AIProvider_Name_Bad(t *core.T) { + var provider *AIProvider + got := provider.Name() + want := "ai" + + core.AssertEqual(t, want, got) + core.AssertNotEqual(t, "", got) +} + +func TestAPI_AIProvider_Name_Ugly(t *core.T) { + provider := NewProvider() + got := provider.Name() + again := provider.Name() + + core.AssertEqual(t, got, again) + core.AssertEqual(t, "ai", got) +} + +func TestAPI_AIProvider_BasePath_Good(t *core.T) { + provider := &AIProvider{} + got := provider.BasePath() + want := "/v1" + + core.AssertEqual(t, want, got) + core.AssertTrue(t, core.HasPrefix(got, "/")) +} + +func TestAPI_AIProvider_BasePath_Bad(t *core.T) { + var provider *AIProvider + got := provider.BasePath() + want := "/v1" + + core.AssertEqual(t, want, got) + core.AssertNotEqual(t, "", got) +} + +func TestAPI_AIProvider_BasePath_Ugly(t *core.T) { + provider := NewProvider() + got := provider.BasePath() + again := provider.BasePath() + + core.AssertEqual(t, got, again) + core.AssertEqual(t, "/v1", got) +} + +func TestAPI_AIProvider_RegisterRoutes_Good(t *core.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + NewProvider().RegisterRoutes(router.Group("/v1")) + + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/v1/health", nil) + router.ServeHTTP(rec, req) + core.AssertEqual(t, http.StatusOK, rec.Code) +} + +func TestAPI_AIProvider_RegisterRoutes_Bad(t *core.T) { + gin.SetMode(gin.TestMode) + router := gin.New() + var provider *AIProvider + + provider.RegisterRoutes(router.Group("/v1")) + rec := httptest.NewRecorder() + req := httptest.NewRequest(http.MethodGet, "/v1/health", nil) + router.ServeHTTP(rec, req) + core.AssertEqual(t, http.StatusNotFound, rec.Code) +} + +func TestAPI_AIProvider_RegisterRoutes_Ugly(t *core.T) { + provider := NewProvider() + core.AssertNotPanics(t, func() { + provider.RegisterRoutes(nil) + }) + core.AssertEqual(t, "ai", provider.Name()) +} + +func TestAPI_AIProvider_Describe_Good(t *core.T) { + provider := NewProvider() + descriptions := provider.Describe() + first := descriptions[0] + + core.AssertLen(t, descriptions, 6) + core.AssertEqual(t, http.MethodPost, first.Method) +} + +func TestAPI_AIProvider_Describe_Bad(t *core.T) { + var provider *AIProvider + descriptions := provider.Describe() + got := len(descriptions) + + core.AssertEqual(t, 6, got) + core.AssertEqual(t, "/health", descriptions[5].Path) +} + +func TestAPI_AIProvider_Describe_Ugly(t *core.T) { + provider := NewProvider() + descriptions := provider.Describe() + health := descriptions[5] + + core.AssertEqual(t, http.MethodGet, health.Method) + core.AssertEqual(t, "/health", health.Path) +} diff --git a/pkg/lab/ax7_test.go b/pkg/lab/ax7_test.go new file mode 100644 index 0000000..1d156fb --- /dev/null +++ b/pkg/lab/ax7_test.go @@ -0,0 +1,145 @@ +package lab + +import ( + . "dappco.re/go" + "dappco.re/go/cli/pkg/cli" +) + +func TestLab_AddLabCommands_Good(t *T) { + root := &cli.Command{Use: "core"} + AddLabCommands(root) + cmd, _, err := root.Find([]string{"lab"}) + + AssertNoError(t, err) + AssertEqual(t, "lab", cmd.Name()) +} + +func TestLab_AddLabCommands_Bad(t *T) { + root := &cli.Command{Use: "core"} + AddLabCommands(root) + AddLabCommands(root) + + AssertLen(t, root.Commands(), 1) + AssertEqual(t, "lab", root.Commands()[0].Name()) +} + +func TestLab_AddLabCommands_Ugly(t *T) { + root := &cli.Command{Use: "core"} + root.AddCommand(&cli.Command{Use: "lab"}) + AddLabCommands(root) + + AssertLen(t, root.Commands(), 1) + AssertEqual(t, "lab", root.Commands()[0].Name()) +} + +func TestLab_RunServe_Good(t *T) { + t.Setenv("CORE_LAB_API_TOKEN", "") + err := RunServe(CommandOptions{Bind: "0.0.0.0:8080"}) + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "non-loopback") +} + +func TestLab_RunServe_Bad(t *T) { + t.Setenv("CORE_LAB_API_TOKEN", "") + err := RunServe(CommandOptions{Bind: "127.0.0.1:8080", AllowRemote: true}) + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "CORE_LAB_API_TOKEN") +} + +func TestLab_RunServe_Ugly(t *T) { + t.Setenv("CORE_LAB_API_TOKEN", "") + err := RunServe(CommandOptions{Bind: "not-a-host", AllowRemote: false}) + got := ErrorMessage(err) + + AssertError(t, err) + AssertContains(t, got, "non-loopback") +} + +func TestLab_ValidateBindAddress_Good(t *T) { + err := ValidateBindAddress("127.0.0.1:8080", false) + got := IsLoopbackBindAddress("127.0.0.1:8080") + want := true + + AssertNoError(t, err) + AssertEqual(t, want, got) +} + +func TestLab_ValidateBindAddress_Bad(t *T) { + err := ValidateBindAddress("0.0.0.0:8080", false) + got := ErrorMessage(err) + want := "non-loopback" + + AssertError(t, err) + AssertContains(t, got, want) +} + +func TestLab_ValidateBindAddress_Ugly(t *T) { + err := ValidateBindAddress(":8080", true) + got := IsLoopbackBindAddress(":8080") + want := false + + AssertNoError(t, err) + AssertEqual(t, want, got) +} + +func TestLab_IsLoopbackBindAddress_Good(t *T) { + got := IsLoopbackBindAddress("localhost:8080") + ipv4 := IsLoopbackBindAddress("127.0.0.1:8080") + ipv6 := IsLoopbackBindAddress("[::1]:8080") + + AssertTrue(t, got) + AssertTrue(t, ipv4) + AssertTrue(t, ipv6) +} + +func TestLab_IsLoopbackBindAddress_Bad(t *T) { + got := IsLoopbackBindAddress("0.0.0.0:8080") + wildcard := IsLoopbackBindAddress(":8080") + remote := IsLoopbackBindAddress("example.com:8080") + + AssertFalse(t, got) + AssertFalse(t, wildcard) + AssertFalse(t, remote) +} + +func TestLab_IsLoopbackBindAddress_Ugly(t *T) { + empty := IsLoopbackBindAddress("") + malformed := IsLoopbackBindAddress("::notanaddr:8080") + missingPort := IsLoopbackBindAddress("localhost") + + AssertFalse(t, empty) + AssertFalse(t, malformed) + AssertFalse(t, missingPort) +} + +func TestLab_ValidateRemoteAuth_Good(t *T) { + err := ValidateRemoteAuth(false, "") + remoteErr := ValidateRemoteAuth(true, "token") + want := true + + AssertNoError(t, err) + AssertNoError(t, remoteErr) + AssertTrue(t, want) +} + +func TestLab_ValidateRemoteAuth_Bad(t *T) { + err := ValidateRemoteAuth(true, "") + got := ErrorMessage(err) + want := "CORE_LAB_API_TOKEN" + + AssertError(t, err) + AssertContains(t, got, want) +} + +func TestLab_ValidateRemoteAuth_Ugly(t *T) { + err := ValidateRemoteAuth(true, " ") + got := ErrorMessage(err) + want := "--allow-remote" + + AssertError(t, err) + AssertContains(t, got, want) +} diff --git a/pkg/lab/cmd.go b/pkg/lab/cmd.go index a18a176..c11c424 100644 --- a/pkg/lab/cmd.go +++ b/pkg/lab/cmd.go @@ -12,8 +12,8 @@ import ( "syscall" "time" + "dappco.re/go" "dappco.re/go/cli/pkg/cli" - "dappco.re/go/core" ) const defaultBindAddr = "127.0.0.1:8080" diff --git a/third_party/ollama/api/ax7_test.go b/third_party/ollama/api/ax7_test.go new file mode 100644 index 0000000..3f0966e --- /dev/null +++ b/third_party/ollama/api/ax7_test.go @@ -0,0 +1,82 @@ +package api + +import ( + "net/http" + "net/http/httptest" + "net/url" + + . "dappco.re/go" +) + +func TestOllama_NewClient_Good(t *T) { + base, err := url.Parse("http://127.0.0.1:11434") + RequireNoError(t, err) + client := NewClient(base, nil) + + AssertNotNil(t, client) + AssertEqual(t, base, client.baseURL) +} + +func TestOllama_NewClient_Bad(t *T) { + client := NewClient(nil, nil) + got := client.baseURL + httpClient := client.httpClient + + AssertNil(t, got) + AssertNotNil(t, httpClient) +} + +func TestOllama_NewClient_Ugly(t *T) { + base, err := url.Parse("http://127.0.0.1:11434/base/") + RequireNoError(t, err) + client := NewClient(base, http.DefaultClient) + + AssertNotNil(t, client) + AssertEqual(t, http.DefaultClient, client.httpClient) +} + +func TestOllama_Client_Embed_Good(t *T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + AssertEqual(t, "/api/embed", r.URL.Path) + w.WriteHeader(http.StatusOK) + _, err := w.Write([]byte(`{"embedding":[1,2,3]}`)) + AssertNoError(t, err) + })) + defer server.Close() + + base, err := url.Parse(server.URL) + RequireNoError(t, err) + client := NewClient(base, server.Client()) + response, err := client.Embed(Background(), &EmbedRequest{Model: "m", Input: "hello"}) + + AssertNoError(t, err) + AssertEqual(t, [][]float64{{1, 2, 3}}, response.Embeddings) +} + +func TestOllama_Client_Embed_Bad(t *T) { + client := NewClient(nil, nil) + response, err := client.Embed(Background(), &EmbedRequest{Model: "m", Input: "hello"}) + got := ErrorMessage(err) + + AssertNil(t, response) + AssertError(t, err) + AssertContains(t, got, "base url") +} + +func TestOllama_Client_Embed_Ugly(t *T) { + server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(http.StatusServiceUnavailable) + _, err := w.Write([]byte("model unavailable")) + AssertNoError(t, err) + })) + defer server.Close() + + base, err := url.Parse(server.URL) + RequireNoError(t, err) + client := NewClient(base, server.Client()) + response, err := client.Embed(Background(), &EmbedRequest{Model: "m", Input: "hello"}) + + AssertNil(t, response) + AssertError(t, err) + AssertContains(t, ErrorMessage(err), "model unavailable") +}