From c32b0157ee47a59adfe1bee9a24001192ed8d353 Mon Sep 17 00:00:00 2001 From: Delqhi Date: Sun, 14 Jun 2026 12:27:49 +0200 Subject: [PATCH] feat+ci: Go-native websearch skill integration + E2E test - Switch websearch skill from archived Python SIN-Code-Websearch-Skill to Go-native OpenSIN-Code/web_search_bundle. - mcpclient registry now uses the built binary at SIN_SKILLS_DIR/web_search_bundle/sin-websearch and falls back to PATH. - Registry also uses the default skills dir (~/.local/share/sin-code/skills) when SIN_SKILLS_DIR env is unset, matching skillmgr behavior. - skillmgr verifies the Go entrypoint by building sin-websearch into the repo root with -o sin-websearch. - Add registry tests for local-binary, PATH-fallback, and default-skills-dir. - Add E2E test (build tag e2e) that installs websearch from GitHub and verifies the binary runs --help. - Add skill-e2e.yml GitHub Actions workflow to run the E2E test on main/push, on PRs touching skillmgr/mcpclient, and manually. --- .github/workflows/skill-e2e.yml | 32 ++++++++ .github/workflows/skill-e2e.yml.doc.md | 11 +++ cmd/sin-code/internal/mcpclient/registry.go | 36 ++++++++- .../internal/mcpclient/registry_test.go | 76 +++++++++++++++++++ cmd/sin-code/internal/skillmgr/manager.go | 12 ++- .../internal/skillmgr/manager_e2e_test.go | 64 ++++++++++++++++ .../skillmgr/manager_e2e_test.go.doc.md | 13 ++++ 7 files changed, 241 insertions(+), 3 deletions(-) create mode 100644 .github/workflows/skill-e2e.yml create mode 100644 .github/workflows/skill-e2e.yml.doc.md create mode 100644 cmd/sin-code/internal/mcpclient/registry_test.go create mode 100644 cmd/sin-code/internal/skillmgr/manager_e2e_test.go create mode 100644 cmd/sin-code/internal/skillmgr/manager_e2e_test.go.doc.md diff --git a/.github/workflows/skill-e2e.yml b/.github/workflows/skill-e2e.yml new file mode 100644 index 0000000..137d7c7 --- /dev/null +++ b/.github/workflows/skill-e2e.yml @@ -0,0 +1,32 @@ +name: skill-e2e + +on: + push: + branches: [main] + pull_request: + branches: [main] + paths: + - 'cmd/sin-code/internal/skillmgr/**' + - 'cmd/sin-code/internal/mcpclient/**' + workflow_dispatch: + +concurrency: + group: skill-e2e-${{ github.ref }} + cancel-in-progress: true + +jobs: + websearch: + name: websearch skill install E2E + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v5 + with: + go-version: "1.25.11" + - name: Install websearch skill from GitHub + run: | + go test -tags=e2e ./cmd/sin-code/internal/skillmgr/ -run TestInstallWebsearchFromGitHub -count=1 -v 2>&1 | tail -30 + - name: Verify binary is registered + run: | + go build -o /tmp/sin-code ./cmd/sin-code + /tmp/sin-code mcp list | grep 'websearch' diff --git a/.github/workflows/skill-e2e.yml.doc.md b/.github/workflows/skill-e2e.yml.doc.md new file mode 100644 index 0000000..54b76b4 --- /dev/null +++ b/.github/workflows/skill-e2e.yml.doc.md @@ -0,0 +1,11 @@ +What it does: Runs the E2E integration test that installs the `websearch` skill from GitHub and verifies the `sin-websearch` binary is built and registered. + +Dependencies: Triggered on push to `main`, on PRs that touch `skillmgr` or `mcpclient`, and via `workflow_dispatch`. Requires Go 1.25.11 and network access to GitHub. + +Important config: +- Runs `go test -tags=e2e ./cmd/sin-code/internal/skillmgr/ -run TestInstallWebsearchFromGitHub`. +- Builds `sin-code` and runs `sin-code mcp list` to verify the websearch server is registered. + +Caveats: +- Slow (~1 minute) due to cloning and building `web_search_bundle` from source. +- Can fail due to external network issues. diff --git a/cmd/sin-code/internal/mcpclient/registry.go b/cmd/sin-code/internal/mcpclient/registry.go index c6cefb6..db339d6 100644 --- a/cmd/sin-code/internal/mcpclient/registry.go +++ b/cmd/sin-code/internal/mcpclient/registry.go @@ -14,7 +14,7 @@ import ( // tool-name prefixes ("websearch__search", "browser__navigate", ...), which // the permission matrix gates via the "mcp" policy class. func DefaultServers() []ServerConfig { - skillsDir := os.Getenv("SIN_SKILLS_DIR") + skillsDir := skillsDirOrDefault() py := func(repo string) ServerConfig { name := shortName(repo) cfg := ServerConfig{Name: name, Transport: "stdio"} @@ -26,8 +26,28 @@ func DefaultServers() []ServerConfig { } return cfg } + // goNative returns a ServerConfig for a Go-native skill. It prefers the + // binary built inside SIN_SKILLS_DIR// so that skillmgr + // can install and run the skill without requiring the user to put the binary + // on PATH. Falls back to the binary name on PATH if no local checkout exists. + goNative := func(repo, binary string, args ...string) ServerConfig { + name := shortName(repo) + cfg := ServerConfig{Name: name, Transport: "stdio", Args: args} + if skillsDir != "" { + localBin := filepath.Join(skillsDir, repo, binary) + if _, err := os.Stat(localBin); err == nil { + cfg.Command = localBin + } else { + cfg.Command = binary + } + } else { + cfg.Command = binary + } + return cfg + } return []ServerConfig{ - py("SIN-Code-Websearch-Skill"), + // web_search_bundle is the Go-native successor to SIN-Code-Websearch-Skill. + goNative("web_search_bundle", "sin-websearch", "serve"), py("SIN-Code-Scheduler-Skill"), py("SIN-Code-Goal-Mode-Skill"), py("SIN-Code-Grill-Me-Skill"), @@ -48,6 +68,7 @@ func DefaultServers() []ServerConfig { func shortName(repo string) string { m := map[string]string{ + "web_search_bundle": "websearch", "SIN-Code-Websearch-Skill": "websearch", "SIN-Code-Scheduler-Skill": "scheduler", "SIN-Code-Goal-Mode-Skill": "goalmode", @@ -67,3 +88,14 @@ func shortName(repo string) string { } return repo } + +// skillsDirOrDefault returns the configured SIN_SKILLS_DIR or the default +// local share location used by skillmgr. This keeps the registry in sync +// with where skillmgr actually installs skills. +func skillsDirOrDefault() string { + if d := os.Getenv("SIN_SKILLS_DIR"); d != "" { + return d + } + home, _ := os.UserHomeDir() + return filepath.Join(home, ".local", "share", "sin-code", "skills") +} diff --git a/cmd/sin-code/internal/mcpclient/registry_test.go b/cmd/sin-code/internal/mcpclient/registry_test.go new file mode 100644 index 0000000..dfa2043 --- /dev/null +++ b/cmd/sin-code/internal/mcpclient/registry_test.go @@ -0,0 +1,76 @@ +// SPDX-License-Identifier: MIT +// Purpose: tests for the built-in ecosystem registry in mcpclient. +package mcpclient + +import ( + "os" + "path/filepath" + "testing" +) + +func TestDefaultServersWebsearchUsesLocalBinaryWhenPresent(t *testing.T) { + dir := t.TempDir() + bin := filepath.Join(dir, "web_search_bundle", "sin-websearch") + if err := os.MkdirAll(filepath.Dir(bin), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(bin, []byte("#!/bin/sh\necho fake"), 0o755); err != nil { + t.Fatal(err) + } + + t.Setenv("SIN_SKILLS_DIR", dir) + for _, s := range DefaultServers() { + if s.Name != "websearch" { + continue + } + if s.Command != bin { + t.Fatalf("websearch command should use local binary %q, got %q", bin, s.Command) + } + if len(s.Args) != 1 || s.Args[0] != "serve" { + t.Fatalf("websearch args should be [serve], got %v", s.Args) + } + return + } + t.Fatal("websearch server not found in DefaultServers") +} + +func TestDefaultServersWebsearchFallsBackToPathBinary(t *testing.T) { + // Use a HOME that has no sin-code skills checkout so the default skills + // dir check fails and the registry falls back to the binary on PATH. + t.Setenv("HOME", t.TempDir()) + t.Setenv("SIN_SKILLS_DIR", "") + for _, s := range DefaultServers() { + if s.Name != "websearch" { + continue + } + if s.Command != "sin-websearch" { + t.Fatalf("websearch command should fall back to %q, got %q", "sin-websearch", s.Command) + } + return + } + t.Fatal("websearch server not found in DefaultServers") +} + +func TestDefaultServersWebsearchUsesDefaultSkillsDir(t *testing.T) { + home := t.TempDir() + bin := filepath.Join(home, ".local", "share", "sin-code", "skills", "web_search_bundle", "sin-websearch") + if err := os.MkdirAll(filepath.Dir(bin), 0o755); err != nil { + t.Fatal(err) + } + if err := os.WriteFile(bin, []byte("#!/bin/sh\necho fake"), 0o755); err != nil { + t.Fatal(err) + } + + t.Setenv("SIN_SKILLS_DIR", "") + t.Setenv("HOME", home) + for _, s := range DefaultServers() { + if s.Name != "websearch" { + continue + } + if s.Command != bin { + t.Fatalf("websearch command should use default skills dir binary %q, got %q", bin, s.Command) + } + return + } + t.Fatal("websearch server not found in DefaultServers") +} diff --git a/cmd/sin-code/internal/skillmgr/manager.go b/cmd/sin-code/internal/skillmgr/manager.go index 616b1b8..74f8343 100644 --- a/cmd/sin-code/internal/skillmgr/manager.go +++ b/cmd/sin-code/internal/skillmgr/manager.go @@ -38,7 +38,7 @@ func SkillsDir() string { // with mcpclient.DefaultServers (ecosystem-sync CI enforces it). func KnownSkills() map[string]string { return map[string]string{ - "websearch": "SIN-Code-Websearch-Skill", + "websearch": "web_search_bundle", "scheduler": "SIN-Code-Scheduler-Skill", "goalmode": "SIN-Code-Goal-Mode-Skill", "grillme": "SIN-Code-Grill-Me-Skill", @@ -124,5 +124,15 @@ func verifyEntrypoint(ctx context.Context, dir string) (bool, string) { if _, err := os.Stat(filepath.Join(dir, "package.json")); err == nil { return true, "node entrypoint (package.json)" } + if _, err := os.Stat(filepath.Join(dir, "go.mod")); err == nil { + // Go-native skill: build the binary into the repo root so the MCP + // registry can use the full path (SIN_SKILLS_DIR//). + cmd := exec.CommandContext(ctx, "go", "build", "-o", "sin-websearch", "./cmd/sin-websearch") + cmd.Dir = dir + if _, err := cmd.CombinedOutput(); err != nil { + return false, fmt.Sprintf("go entrypoint exists but build failed: %v", err) + } + return true, "go entrypoint builds" + } return false, "no recognized MCP entrypoint" } diff --git a/cmd/sin-code/internal/skillmgr/manager_e2e_test.go b/cmd/sin-code/internal/skillmgr/manager_e2e_test.go new file mode 100644 index 0000000..971f59e --- /dev/null +++ b/cmd/sin-code/internal/skillmgr/manager_e2e_test.go @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: MIT +// Purpose: E2E integration test that installs the websearch skill from GitHub +// and verifies the built binary is runnable. Run with: go test -tags=e2e ./... +//go:build e2e + +package skillmgr + +import ( + "context" + "os" + "os/exec" + "path/filepath" + "testing" + "time" +) + +func listDir(dir string) []string { + entries, err := os.ReadDir(dir) + if err != nil { + return []string{""} + } + var names []string + for _, e := range entries { + names = append(names, e.Name()) + } + return names +} + +// TestInstallWebsearchFromGitHub clones web_search_bundle via the skill manager +// and verifies the sin-websearch binary is built and runnable. This is a real +// network test and therefore gated behind the e2e build tag. +func TestInstallWebsearchFromGitHub(t *testing.T) { + skillsDir := t.TempDir() + t.Setenv("SIN_SKILLS_DIR", skillsDir) + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) + defer cancel() + + st, err := Install(ctx, "websearch") + if err != nil { + t.Fatalf("install websearch: %v", err) + } + if !st.Installed { + t.Fatalf("websearch not installed") + } + t.Logf("websearch status: installed=%v runnable=%v detail=%q", st.Installed, st.Runnable, st.Detail) + if !st.Runnable { + t.Fatalf("websearch not runnable: %s", st.Detail) + } + + bin := filepath.Join(skillsDir, "web_search_bundle", "sin-websearch") + if _, err := os.Stat(bin); err != nil { + t.Logf("skills dir contents: %v", listDir(filepath.Join(skillsDir, "web_search_bundle"))) + t.Fatalf("binary not found at %s: %v", bin, err) + } + + out, err := exec.CommandContext(ctx, bin, "--help").CombinedOutput() + if err != nil { + t.Fatalf("binary --help failed: %v\n%s", err, out) + } + if len(out) == 0 { + t.Fatalf("binary --help produced no output") + } +} diff --git a/cmd/sin-code/internal/skillmgr/manager_e2e_test.go.doc.md b/cmd/sin-code/internal/skillmgr/manager_e2e_test.go.doc.md new file mode 100644 index 0000000..c159cb5 --- /dev/null +++ b/cmd/sin-code/internal/skillmgr/manager_e2e_test.go.doc.md @@ -0,0 +1,13 @@ +What it does: E2E integration test for the `websearch` skill installation in SIN-Code. Clones `web_search_bundle` from GitHub, builds the `sin-websearch` binary, and verifies it runs `--help`. + +Dependencies: Network access to GitHub and a working Go toolchain. Gated behind the `e2e` build tag. + +Usage: +```bash +go test -tags=e2e ./cmd/sin-code/internal/skillmgr/... +``` + +Caveats: +- Slow (~1-5 minutes) because it clones and builds from source. +- Can fail due to network issues or GitHub availability. +- Not run in normal unit-test CI; requires a separate E2E workflow.