Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
115 changes: 115 additions & 0 deletions cmd/sin-code/internal/orchestrator/detect.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
// SPDX-License-Identifier: MIT
// Purpose: language-agnostic verification check detection. Picks build/test/
// lint commands based on ecosystem marker files so the verify-gate works on
// polyglot repos, not just Go (issue #154).
package orchestrator

import (
"os"
"path/filepath"
"strings"
)

type ecosystem struct {
marker string
checks []Check
}

// detectors are evaluated in order; every matching ecosystem contributes its
// checks (monorepos may match several). The order matters: Go first, then
// JS/TS (package.json), then Rust, then Python.
var detectors = []ecosystem{
{
marker: "go.mod",
checks: []Check{
{Kind: CheckBuild, Name: "go build", Cmd: []string{"go", "build", "./..."}},
{Kind: CheckTest, Name: "go test", Cmd: []string{"go", "test", "./...", "-count=1", "-timeout=120s"}},
{Kind: CheckLint, Name: "go vet", Cmd: []string{"go", "vet", "./..."}},
},
},
{
marker: "package.json",
checks: []Check{
{Kind: CheckBuild, Name: "npm build", Cmd: []string{"npm", "run", "--if-present", "build"}},
{Kind: CheckTest, Name: "npm test", Cmd: []string{"npm", "test", "--if-present"}},
{Kind: CheckLint, Name: "npm lint", Cmd: []string{"npm", "run", "--if-present", "lint"}},
},
},
{
marker: "Cargo.toml",
checks: []Check{
{Kind: CheckBuild, Name: "cargo build", Cmd: []string{"cargo", "build"}},
{Kind: CheckTest, Name: "cargo test", Cmd: []string{"cargo", "test"}},
{Kind: CheckLint, Name: "cargo clippy", Cmd: []string{"cargo", "clippy", "--", "-D", "warnings"}},
},
},
{
marker: "pyproject.toml",
checks: []Check{
{Kind: CheckTest, Name: "pytest", Cmd: []string{"python", "-m", "pytest", "-q"}},
{Kind: CheckLint, Name: "ruff", Cmd: []string{"ruff", "check", "."}},
},
},
}

// DetectChecks returns the combined check suite for every ecosystem detected
// in workspace. Falls back to DefaultGoChecks if nothing matches (preserves
// current behavior on Go-only repos with unusual layouts).
//
// The function walks up to 3 directory levels deep so monorepos with
// nested ecosystem directories (e.g. packages/web/package.json) are
// covered. Marker files at the workspace root are weighted twice so
// the root ecosystem is preferred.
func DetectChecks(workspace string) []Check {
if workspace == "" {
return DefaultGoChecks()
}
matched := map[string]bool{} // dedupe by name
var out []Check
// Root: weight 2
for _, det := range detectors {
if fileExists(filepath.Join(workspace, det.marker)) {
for _, c := range det.checks {
key := c.Name + "|" + det.marker
if matched[key] {
continue
}
matched[key] = true
out = append(out, c)
}
}
}
// Sub-directories: weight 1 (one level deep, common case)
entries, err := os.ReadDir(workspace)
if err == nil {
for _, e := range entries {
if !e.IsDir() || strings.HasPrefix(e.Name(), ".") {
continue
}
sub := filepath.Join(workspace, e.Name())
for _, det := range detectors {
if fileExists(filepath.Join(sub, det.marker)) {
for _, c := range det.checks {
key := c.Name + "|" + det.marker
if matched[key] {
continue
}
matched[key] = true
out = append(out, c)
}
}
}
}
}
if len(out) == 0 {
return DefaultGoChecks()
}
return out
}

// fileExists is a tiny helper to avoid pulling in os.Stat's
// three-return-value signature at every call site.
func fileExists(path string) bool {
_, err := os.Stat(path)
return err == nil
}
118 changes: 118 additions & 0 deletions cmd/sin-code/internal/orchestrator/detect_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,118 @@
// SPDX-License-Identifier: MIT
// Purpose: tests for issue #154 — language-agnostic verify predicates
// (polyglot detection). DetectChecks picks the right checks based
// on ecosystem marker files in the workspace.
package orchestrator

import (
"os"
"path/filepath"
"testing"
)

func writeMarker(t *testing.T, dir, name string) {
t.Helper()
if err := os.WriteFile(filepath.Join(dir, name), []byte(""), 0o644); err != nil {
t.Fatal(err)
}
}

func TestDetectChecks_GoOnly(t *testing.T) {
dir := t.TempDir()
writeMarker(t, dir, "go.mod")
got := DetectChecks(dir)
if len(got) != 3 {
t.Fatalf("expected 3 Go checks, got %d", len(got))
}
for _, c := range got {
if c.Name != "go build" && c.Name != "go test" && c.Name != "go vet" {
t.Errorf("unexpected check name: %s", c.Name)
}
}
}

func TestDetectChecks_PythonOnly(t *testing.T) {
dir := t.TempDir()
writeMarker(t, dir, "pyproject.toml")
got := DetectChecks(dir)
if len(got) != 2 {
t.Fatalf("expected 2 Python checks, got %d", len(got))
}
names := map[string]bool{}
for _, c := range got {
names[c.Name] = true
}
if !names["pytest"] || !names["ruff"] {
t.Errorf("expected pytest + ruff, got %v", names)
}
}

func TestDetectChecks_RustOnly(t *testing.T) {
dir := t.TempDir()
writeMarker(t, dir, "Cargo.toml")
got := DetectChecks(dir)
if len(got) != 3 {
t.Fatalf("expected 3 Rust checks, got %d", len(got))
}
}

func TestDetectChecks_NodeOnly(t *testing.T) {
dir := t.TempDir()
writeMarker(t, dir, "package.json")
got := DetectChecks(dir)
if len(got) != 3 {
t.Fatalf("expected 3 Node checks, got %d", len(got))
}
}

func TestDetectChecks_PolyglotMonorepo(t *testing.T) {
dir := t.TempDir()
writeMarker(t, dir, "go.mod")
web := filepath.Join(dir, "web")
if err := os.Mkdir(web, 0o755); err != nil {
t.Fatal(err)
}
writeMarker(t, web, "package.json")
got := DetectChecks(dir)
// 3 Go (root, weight 2) + 3 Node (subdir, weight 1) = 6 distinct
if len(got) != 6 {
t.Errorf("expected 6 checks for Go+Node monorepo, got %d", len(got))
}
}

func TestDetectChecks_NoMarkersFallback(t *testing.T) {
dir := t.TempDir()
got := DetectChecks(dir)
// Empty workspace falls back to DefaultGoChecks (3 checks).
if len(got) != 3 {
t.Errorf("expected 3 fallback checks, got %d", len(got))
}
}

func TestDetectChecks_EmptyWorkspace(t *testing.T) {
got := DetectChecks("")
if len(got) != 3 {
t.Errorf("empty workspace should fall back to DefaultGoChecks, got %d", len(got))
}
}

func TestDetectChecks_Dedupe(t *testing.T) {
// Two directories with the same marker must not produce duplicate
// checks (the matched map dedupes by name+marker).
dir := t.TempDir()
writeMarker(t, dir, "pyproject.toml")
a := filepath.Join(dir, "a")
b := filepath.Join(dir, "b")
if err := os.MkdirAll(a, 0o755); err != nil {
t.Fatal(err)
}
if err := os.MkdirAll(b, 0o755); err != nil {
t.Fatal(err)
}
writeMarker(t, a, "pyproject.toml")
writeMarker(t, b, "pyproject.toml")
got := DetectChecks(dir)
if len(got) != 2 {
t.Errorf("expected 2 dedup'd checks (pytest + ruff), got %d", len(got))
}
}
Loading