diff --git a/internal/adapter/dependency_test.go b/internal/adapter/dependency_test.go index 5dbc7dc..544a714 100644 --- a/internal/adapter/dependency_test.go +++ b/internal/adapter/dependency_test.go @@ -1,8 +1,10 @@ package adapter_test import ( + "fmt" "go/parser" "go/token" + "io/fs" "os" "path/filepath" "runtime" @@ -11,6 +13,24 @@ import ( "testing" ) +const scriptoriumModulePath = "gitea.maximumdirect.net/eric/scriptorium" + +var forbiddenFrameworkPackageRoots = []string{ + scriptoriumModulePath + "/internal/domain", + scriptoriumModulePath + "/internal/usecase", + scriptoriumModulePath + "/internal/promptdef", + scriptoriumModulePath + "/internal/prompt", + scriptoriumModulePath + "/internal/profile", + scriptoriumModulePath + "/internal/validate", + scriptoriumModulePath + "/internal/llm", + scriptoriumModulePath + "/internal/artifact", +} + +type forbiddenFrameworkImport struct { + filePath string + importPath string +} + func TestScriptoriumAdaptersUseOnlyPublicFrameworkBoundary(t *testing.T) { _, testFile, _, ok := runtime.Caller(0) if !ok { @@ -18,47 +38,95 @@ func TestScriptoriumAdaptersUseOnlyPublicFrameworkBoundary(t *testing.T) { } adapterDir := filepath.Dir(testFile) - directories := []string{ + violations, err := findForbiddenFrameworkImports([]string{ filepath.Join(adapterDir, "cli"), filepath.Join(adapterDir, "http"), filepath.Join(adapterDir, "..", "format"), + }) + if err != nil { + t.Fatalf("scan framework imports: %v", err) } - forbidden := map[string]struct{}{ - "gitea.maximumdirect.net/eric/scriptorium/internal/domain": {}, - "gitea.maximumdirect.net/eric/scriptorium/internal/usecase": {}, - "gitea.maximumdirect.net/eric/scriptorium/internal/promptdef": {}, - "gitea.maximumdirect.net/eric/scriptorium/internal/prompt": {}, - "gitea.maximumdirect.net/eric/scriptorium/internal/profile": {}, - "gitea.maximumdirect.net/eric/scriptorium/internal/profile/builtin": {}, - "gitea.maximumdirect.net/eric/scriptorium/internal/validate": {}, - "gitea.maximumdirect.net/eric/scriptorium/internal/llm": {}, - "gitea.maximumdirect.net/eric/scriptorium/internal/artifact": {}, + for _, violation := range violations { + t.Errorf("%s imports forbidden framework package %s", violation.filePath, violation.importPath) + } +} + +func TestForbiddenFrameworkImportScannerDetectsNestedPackageFamilies(t *testing.T) { + root := t.TempDir() + nestedDir := filepath.Join(root, "nested", "adapter") + if err := os.MkdirAll(nestedDir, 0o755); err != nil { + t.Fatalf("create nested source directory: %v", err) } - for _, directory := range directories { - entries, err := os.ReadDir(directory) - if err != nil { - t.Fatalf("read source directory %s: %v", directory, err) - } - for _, entry := range entries { + sourcePath := filepath.Join(nestedDir, "imports.go") + source := `package nested + +import ( + _ "gitea.maximumdirect.net/eric/scriptorium" + _ "gitea.maximumdirect.net/eric/scriptorium/internal/profile/builtin" +) +` + if err := os.WriteFile(sourcePath, []byte(source), 0o644); err != nil { + t.Fatalf("write nested source fixture: %v", err) + } + + violations, err := findForbiddenFrameworkImports([]string{root}) + if err != nil { + t.Fatalf("scan nested source fixture: %v", err) + } + if len(violations) != 1 { + t.Fatalf("expected one forbidden import, got %#v", violations) + } + if violations[0].filePath != sourcePath { + t.Fatalf("unexpected importing file: %q", violations[0].filePath) + } + wantImport := scriptoriumModulePath + "/internal/profile/builtin" + if violations[0].importPath != wantImport { + t.Fatalf("unexpected forbidden import: %q", violations[0].importPath) + } +} + +func findForbiddenFrameworkImports(roots []string) ([]forbiddenFrameworkImport, error) { + var violations []forbiddenFrameworkImport + for _, root := range roots { + err := filepath.WalkDir(root, func(path string, entry fs.DirEntry, err error) error { + if err != nil { + return err + } if entry.IsDir() || !strings.HasSuffix(entry.Name(), ".go") || strings.HasSuffix(entry.Name(), "_test.go") { - continue + return nil } - path := filepath.Join(directory, entry.Name()) file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly) if err != nil { - t.Fatalf("parse imports in %s: %v", path, err) + return fmt.Errorf("parse imports in %s: %w", path, err) } for _, imported := range file.Imports { importPath, err := strconv.Unquote(imported.Path.Value) if err != nil { - t.Fatalf("parse import path in %s: %v", path, err) + return fmt.Errorf("parse import path in %s: %w", path, err) } - if _, found := forbidden[importPath]; found { - t.Errorf("%s directly imports forbidden framework package %s", path, importPath) + if isForbiddenFrameworkImport(importPath) { + violations = append(violations, forbiddenFrameworkImport{ + filePath: path, + importPath: importPath, + }) } } + return nil + }) + if err != nil { + return nil, fmt.Errorf("walk source root %s: %w", root, err) } } + return violations, nil +} + +func isForbiddenFrameworkImport(importPath string) bool { + for _, root := range forbiddenFrameworkPackageRoots { + if importPath == root || strings.HasPrefix(importPath, root+"/") { + return true + } + } + return false }