Files
Scriberr/internal/api/architecture_test.go
2026-05-04 01:00:51 -07:00

494 lines
14 KiB
Go

package api
import (
"fmt"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"sort"
"strings"
"testing"
)
const forbiddenDatabaseImport = "scriberr/internal/database"
func TestProductionAPIDoesNotImportDatabase(t *testing.T) {
actual, err := productionFilesImportingDatabase(".")
if err != nil {
t.Fatalf("scan API imports: %v", err)
}
if len(actual) > 0 {
t.Fatalf("production API imports internal/database:\n%s\nAPI handlers must use service boundaries instead of direct database access.",
strings.Join(actual, "\n"))
}
}
func TestOnlyAppCompositionImportsDatabase(t *testing.T) {
expected := []string{
"app/app.go",
}
actual, err := productionInternalFilesImportingDatabase("..")
if err != nil {
t.Fatalf("scan internal imports: %v", err)
}
if strings.Join(actual, "\n") != strings.Join(expected, "\n") {
t.Fatalf("internal/database import boundary violation.\nexpected only:\n%s\nactual:\n%s\nOnly composition/bootstrap code should import internal/database outside the database package itself.",
strings.Join(expected, "\n"),
strings.Join(actual, "\n"))
}
}
func TestOnlyAppCompositionImportsAPI(t *testing.T) {
expected := []string{
"app/app.go",
}
actual, err := productionInternalFilesImporting("..", "scriberr/internal/api", "api")
if err != nil {
t.Fatalf("scan internal imports: %v", err)
}
if strings.Join(actual, "\n") != strings.Join(expected, "\n") {
t.Fatalf("internal/api import boundary violation.\nexpected only:\n%s\nactual:\n%s\nOnly composition/bootstrap code should import internal/api.",
strings.Join(expected, "\n"),
strings.Join(actual, "\n"))
}
}
func TestBackendDependencyDirection(t *testing.T) {
tests := []struct {
name string
root string
forbidden []string
allowed []string
}{
{
name: "models stay persistence-only",
root: "../models",
forbidden: []string{"scriberr/internal/"},
},
{
name: "repositories depend only on models inside internal",
root: "../repository",
forbidden: []string{"scriberr/internal/"},
allowed: []string{"scriberr/internal/models"},
},
{
name: "engine providers do not depend on api or repositories",
root: "../transcription/engineprovider",
forbidden: []string{"scriberr/internal/api", "scriberr/internal/repository"},
},
{
name: "workers do not depend on api",
root: "../transcription/worker",
forbidden: []string{"scriberr/internal/api"},
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
violations, err := productionImportViolations(tt.root, tt.forbidden, tt.allowed)
if err != nil {
t.Fatalf("scan imports: %v", err)
}
if len(violations) > 0 {
t.Fatalf("backend dependency direction violations:\n%s", strings.Join(violations, "\n"))
}
})
}
}
func TestASREngineImportInventory(t *testing.T) {
expected := []string{
"internal/transcription/engineprovider/local_provider.go",
}
actual, err := productionInternalFilesImportingPrefix("../..", "scriberr-engine", "engine")
if err != nil {
t.Fatalf("scan scriberr-engine imports: %v", err)
}
if strings.Join(actual, "\n") != strings.Join(expected, "\n") {
t.Fatalf("scriberr-engine import inventory changed.\nexpected only:\n%s\nactual:\n%s\nOnly the local ASR provider adapter should import scriberr-engine after ASRP-Sprint 3; do not add new dependencies on engine internals.",
strings.Join(expected, "\n"),
strings.Join(actual, "\n"))
}
}
func TestProfileServiceDoesNotImportSherpaEngine(t *testing.T) {
violations, err := productionImportViolations("../profile", []string{"scriberr-engine"}, nil)
if err != nil {
t.Fatalf("scan profile service imports: %v", err)
}
if len(violations) > 0 {
t.Fatalf("profile service imports scriberr-engine:\n%s\nProfile validation must use the ASR provider model catalog, not sherpa engine metadata.",
strings.Join(violations, "\n"))
}
}
func TestASRProvidersDoNotDependOnAPIOrRepositories(t *testing.T) {
violations, err := productionImportViolations("../transcription/engineprovider", []string{
"scriberr/internal/api",
"scriberr/internal/repository",
}, nil)
if err != nil {
t.Fatalf("scan ASR provider imports: %v", err)
}
if len(violations) > 0 {
t.Fatalf("ASR provider boundary violations:\n%s\nProviders must remain adapter boundaries and must not depend on API or repository packages.",
strings.Join(violations, "\n"))
}
}
func TestASRContractPackageDoesNotDependOnBackendRuntime(t *testing.T) {
violations, err := productionImportViolations("../transcription/asrcontract", []string{
"scriberr/internal/api",
"scriberr/internal/models",
"scriberr/internal/repository",
"scriberr/internal/transcription/engineprovider",
}, nil)
if err != nil {
t.Fatalf("scan ASR contract imports: %v", err)
}
if len(violations) > 0 {
t.Fatalf("ASR contract package imports backend runtime packages:\n%s\nThe provider contract must stay serializable and independent of Scriberr runtime internals.",
strings.Join(violations, "\n"))
}
}
func TestASRProviderAuthorGuideDocumentsRequiredContract(t *testing.T) {
data, err := os.ReadFile("../../devnotes/v2.0.0/specs/asr-provider-author-guide.md")
if err != nil {
t.Fatalf("read ASR provider author guide: %v", err)
}
text := string(data)
for _, required := range []string{
"GET /v1/provider",
"GET /v1/models",
"GET /v1/status",
"POST /v1/jobs",
"GET /v1/jobs/{job_id}",
"GET /v1/jobs/{job_id}/events",
"POST /v1/models/{model}:load",
"POST /v1/models/{model}:unload",
"RunProviderContract",
} {
if !strings.Contains(text, required) {
t.Fatalf("ASR provider author guide does not mention required contract item %q", required)
}
}
}
func TestProductionCodeDoesNotUseOldASRParameterIdentifiers(t *testing.T) {
for _, symbol := range []string{"WhisperXParams", "WhisperX", "DiarizeModel", "diarize_model"} {
locations, err := productionFilesContainingSymbol("../..", symbol)
if err != nil {
t.Fatalf("scan old ASR parameter identifiers: %v", err)
}
if len(locations) > 0 {
t.Fatalf("production code still references old ASR parameter identifier %q in:\n%s\nUse provider-neutral ASR contracts and ASRParams instead.",
symbol, strings.Join(locations, "\n"))
}
}
}
func TestProductionAPIDoesNotOwnLLMProviderConnectionTester(t *testing.T) {
for _, symbol := range []string{
"LLMProviderConnectionTester",
"testLLMProviderConnection",
"fetchOpenAICompatibleModels",
"fetchOllamaNativeModels",
} {
locations, err := productionFilesContainingSymbol(".", symbol)
if err != nil {
t.Fatalf("scan API symbols: %v", err)
}
if len(locations) > 0 {
t.Fatalf("production API owns LLM provider probing symbol %q in:\n%s\nMove concrete provider probing to internal/llmprovider and keep API handlers request/response-only.",
symbol, strings.Join(locations, "\n"))
}
}
}
func TestProductionAPIDoesNotImportLLM(t *testing.T) {
violations, err := productionImportViolations(".", []string{"scriberr/internal/llm"}, nil)
if err != nil {
t.Fatalf("scan API imports: %v", err)
}
if len(violations) > 0 {
t.Fatalf("production API imports internal/llm:\n%s\nMove chat generation workflow and concrete LLM client usage into internal/chat.",
strings.Join(violations, "\n"))
}
}
func TestProductionDoesNotImportLegacyQueue(t *testing.T) {
actual, err := productionInternalFilesImporting("../..", "scriberr/internal/queue", "queue")
if err != nil {
t.Fatalf("scan legacy queue imports: %v", err)
}
if len(actual) > 0 {
t.Fatalf("production code imports legacy internal/queue:\n%s\nUse internal/transcription/worker for queue execution paths.",
strings.Join(actual, "\n"))
}
}
func TestAPIResponseModelsDoNotAccessFileSystemForMetadata(t *testing.T) {
for _, importPath := range []string{"os", "path/filepath"} {
importsPath, err := fileImports("response_models.go", importPath)
if err != nil {
t.Fatalf("scan response model imports: %v", err)
}
if importsPath {
t.Fatalf("response_models.go imports %s; file metadata must come from internal/files, not API filesystem probing", importPath)
}
}
for _, symbol := range []string{"os.Stat(", "AudioPath", "filepath."} {
locations, err := productionFilesContainingSymbol(".", symbol)
if err != nil {
t.Fatalf("scan API symbols: %v", err)
}
locations = excludePaths(locations, "internal/api/transcription_handlers.go")
if len(locations) > 0 {
t.Fatalf("production API file response metadata touches filesystem/path symbol %q in:\n%s", symbol, strings.Join(locations, "\n"))
}
}
}
func TestProductServicesDoNotUseUnscopedJobFindByID(t *testing.T) {
for _, symbol := range []string{
"s.jobs.FindByID(",
"s.repo.FindByID(",
} {
locations, err := productionFilesContainingSymbol("../..", symbol)
if err != nil {
t.Fatalf("scan repository symbols: %v", err)
}
locations = excludePaths(locations,
"internal/repository/",
"internal/transcription/worker/",
"internal/llmprovider/protected_repository.go",
"internal/account/service.go",
)
if len(locations) > 0 {
t.Fatalf("production product service uses unscoped job lookup %q in:\n%s\nUse user-scoped repository methods for user-owned transcription data.",
symbol, strings.Join(locations, "\n"))
}
}
}
func productionFilesImportingDatabase(root string) ([]string, error) {
var files []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
importsDatabase, err := fileImports(path, forbiddenDatabaseImport)
if err != nil {
return err
}
if importsDatabase {
files = append(files, filepath.Base(path))
}
return nil
})
sort.Strings(files)
return files, err
}
func productionInternalFilesImportingDatabase(root string) ([]string, error) {
return productionInternalFilesImporting(root, forbiddenDatabaseImport, "database")
}
func productionInternalFilesImporting(root, importPath, skipPackageDir string) ([]string, error) {
var files []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if filepath.Base(path) == skipPackageDir {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
importsTarget, err := fileImports(path, importPath)
if err != nil {
return err
}
if importsTarget {
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
files = append(files, filepath.ToSlash(rel))
}
return nil
})
sort.Strings(files)
return files, err
}
func productionInternalFilesImportingPrefix(root, importPathPrefix, skipPackageDir string) ([]string, error) {
var files []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if filepath.Base(path) == skipPackageDir {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
imports, err := fileImportPaths(path)
if err != nil {
return err
}
if matchesAnyImport(imports, importPathPrefix) {
rel, err := filepath.Rel(root, path)
if err != nil {
return err
}
files = append(files, filepath.ToSlash(rel))
}
return nil
})
sort.Strings(files)
return files, err
}
func productionImportViolations(root string, forbidden, allowed []string) ([]string, error) {
var violations []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
imports, err := fileImportPaths(path)
if err != nil {
return err
}
for _, importPath := range imports {
if !matchesAny(importPath, forbidden) || matchesAny(importPath, allowed) {
continue
}
violations = append(violations, fmt.Sprintf("%s imports %s", filepath.ToSlash(path), importPath))
}
return nil
})
sort.Strings(violations)
return violations, err
}
func productionFilesContainingSymbol(root, symbol string) ([]string, error) {
var files []string
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
return nil
}
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
return nil
}
data, err := os.ReadFile(path)
if err != nil {
return err
}
if strings.Contains(string(data), symbol) {
files = append(files, filepath.ToSlash(path))
}
return nil
})
sort.Strings(files)
return files, err
}
func excludePaths(paths []string, prefixes ...string) []string {
var filtered []string
for _, path := range paths {
normalized := strings.TrimPrefix(path, "../../")
normalized = strings.TrimPrefix(normalized, "./")
matched := false
for _, prefix := range prefixes {
if strings.HasPrefix(normalized, prefix) {
matched = true
break
}
}
if !matched {
filtered = append(filtered, path)
}
}
return filtered
}
func fileImports(path string, importPath string) (bool, error) {
imports, err := fileImportPaths(path)
if err != nil {
return false, err
}
for _, current := range imports {
if current == importPath {
return true, nil
}
}
return false, nil
}
func fileImportPaths(path string) ([]string, error) {
parsed, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
if err != nil {
return nil, err
}
var imports []string
for _, spec := range parsed.Imports {
if spec.Path == nil {
continue
}
imports = append(imports, strings.Trim(spec.Path.Value, `"`))
}
return imports, nil
}
func matchesAny(importPath string, prefixes []string) bool {
for _, prefix := range prefixes {
if importPath == prefix || strings.HasPrefix(importPath, prefix+"/") {
return true
}
}
return false
}
func matchesAnyImport(imports []string, prefix string) bool {
for _, importPath := range imports {
if importPath == prefix || strings.HasPrefix(importPath, prefix+"/") {
return true
}
}
return false
}