package utils
import (
"go/parser"
"go/token"
"os/exec"
"strings"
"testing"
)
// TestPackageDependencies enforces the project's package dependency rules
// defined in CLAUDE.md. Packages may only import packages below them:
//
// cmd → tools, tui, utils, db
// tools → utils, db
// tui → tools, utils
// utils → (nothing — leaf package)
// db → utils
//
// Note: This test lives in utils/ only because it needs to be in some
// package. It validates rules for ALL project packages.
func TestPackageDependencies(t *testing.T) {
rules := map[string]map[string]bool{
// pkg → set of allowed skraak-internal imports
"skraak/cmd": {"skraak/tools": true, "skraak/tui": true, "skraak/utils": true, "skraak/db": true},
"skraak/tools": {"skraak/utils": true, "skraak/db": true},
"skraak/tui": {"skraak/tools": true, "skraak/utils": true},
"skraak/utils": {}, // leaf package — no skraak imports allowed
"skraak/db": {"skraak/utils": true},
}
for pkg, allowed := range rules {
imports := getInternalImports(t, pkg)
for _, imp := range imports {
if !allowed[imp] {
t.Errorf("%s imports %s — not allowed by dependency rules", pkg, imp)
}
}
}
}
// TestNoDirectDBImportInUtils checks that no file in utils/ imports skraak/db.
// This is a fast source-level check that doesn't require `go list`.
func TestNoDirectDBImportInUtils(t *testing.T) {
fset := token.NewFileSet()
pkgs, err := parser.ParseDir(fset, ".", nil, parser.ImportsOnly)
if err != nil {
t.Fatalf("parse utils/: %v", err)
}
for _, pkg := range pkgs {
for filename, file := range pkg.Files {
for _, imp := range file.Imports {
path := strings.Trim(imp.Path.Value, `"`)
if path == "skraak/db" {
t.Errorf("%s: forbidden import of skraak/db (utils is the leaf package)", filename)
}
}
}
}
}
// getInternalImports returns the skraak-internal imports for a package using `go list`.
func getInternalImports(t *testing.T, pkg string) []string {
t.Helper()
out, err := exec.Command("go", "list", "-f", "{{range .Imports}}{{.}}\n{{end}}", pkg).Output()
if err != nil {
t.Fatalf("go list %s: %v", pkg, err)
}
var internal []string
for line := range strings.SplitSeq(string(out), "\n") {
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "skraak/") || line == "skraak" {
internal = append(internal, line)
}
}
return internal
}