mirror of
https://github.com/rishikanthc/Scriberr.git
synced 2026-07-01 08:15:46 +00:00
494 lines
14 KiB
Go
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
|
|
}
|