2026-07-17 05:05:48 +00:00
package modules_test
import (
"fmt"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"runtime"
"strconv"
"strings"
"testing"
)
const moduleImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/modules/"
2026-07-18 23:50:24 +00:00
const internalImportPrefix = "gitea.maximumdirect.net/eric/notarius/internal/"
2026-07-17 05:05:48 +00:00
func TestProductionImportBoundaries ( t * testing . T ) {
repositoryRoot := testRepositoryRoot ( t )
err := filepath . WalkDir ( repositoryRoot , func ( path string , entry fs . DirEntry , walkErr error ) error {
if walkErr != nil {
return walkErr
}
if entry . IsDir ( ) {
if entry . Name ( ) == ".git" || entry . Name ( ) == "testdata" || entry . Name ( ) == "vendor" {
return filepath . SkipDir
}
return nil
}
if filepath . Ext ( path ) != ".go" {
return nil
}
return checkImportBoundaries ( repositoryRoot , path )
} )
if err != nil {
t . Fatal ( err )
}
}
func TestImportBoundaryFixtureIsRejected ( t * testing . T ) {
repositoryRoot := testRepositoryRoot ( t )
fixture := filepath . Join ( repositoryRoot , "internal" , "modules" , "generic" , "testdata" , "importboundaries" , "imports_dnd.go" )
err := checkImportBoundaries ( repositoryRoot , fixture )
if err == nil {
2026-07-17 13:58:50 +00:00
t . Fatal ( "fixture import was accepted, want generic-to-concrete violation" )
2026-07-17 05:05:48 +00:00
}
2026-07-17 13:58:50 +00:00
if ! strings . Contains ( err . Error ( ) , "generic family must not import concrete family" ) {
t . Fatalf ( "fixture error = %q, want generic-to-concrete violation" , err )
2026-07-17 05:05:48 +00:00
}
}
func TestImportBoundaryRules ( t * testing . T ) {
tests := [ ] struct {
2026-07-17 13:58:50 +00:00
name string
filename string
sourcePackage string
importPath string
wantError bool
2026-07-17 05:05:48 +00:00
} {
{
2026-07-17 13:58:50 +00:00
name : "concrete implementation cannot import peer concrete family" ,
filename : "internal/modules/dnd/extract/example/extractor.go" ,
sourcePackage : "example" ,
importPath : moduleImportPrefix + "seriatim/input/transcript" ,
wantError : true ,
2026-07-17 05:05:48 +00:00
} ,
{
2026-07-17 13:58:50 +00:00
name : "future concrete family cannot import current concrete family" ,
filename : "internal/modules/almanac/extract/example/extractor.go" ,
sourcePackage : "example" ,
importPath : moduleImportPrefix + "dnd/shared" ,
wantError : true ,
2026-07-17 05:05:48 +00:00
} ,
{
2026-07-17 13:58:50 +00:00
name : "current concrete family cannot import future concrete family" ,
filename : "internal/modules/seriatim/input/example/adapter.go" ,
sourcePackage : "example" ,
importPath : moduleImportPrefix + "almanac/shared" ,
wantError : true ,
2026-07-17 05:05:48 +00:00
} ,
{
2026-07-17 13:58:50 +00:00
name : "generic implementation cannot import current concrete family" ,
filename : "internal/modules/generic/merge/example/merger.go" ,
sourcePackage : "example" ,
importPath : moduleImportPrefix + "dnd/shared" ,
wantError : true ,
2026-07-17 05:05:48 +00:00
} ,
{
2026-07-17 13:58:50 +00:00
name : "generic implementation cannot import future concrete family" ,
filename : "internal/modules/generic/merge/example/merger.go" ,
sourcePackage : "example" ,
importPath : moduleImportPrefix + "almanac/shared" ,
wantError : true ,
2026-07-17 05:05:48 +00:00
} ,
{
2026-07-17 13:58:50 +00:00
name : "concrete implementation cannot import generic implementation" ,
filename : "internal/modules/dnd/extract/example/extractor.go" ,
sourcePackage : "example" ,
importPath : moduleImportPrefix + "generic/normalize/noop" ,
wantError : true ,
2026-07-17 05:05:48 +00:00
} ,
{
2026-07-17 13:58:50 +00:00
name : "concrete white-box test follows production rules" ,
filename : "internal/modules/dnd/extract/example/extractor_test.go" ,
sourcePackage : "example" ,
importPath : moduleImportPrefix + "generic/normalize/noop" ,
wantError : true ,
2026-07-17 05:05:48 +00:00
} ,
{
2026-07-17 13:58:50 +00:00
name : "concrete registrar may compose generic implementation" ,
filename : "internal/modules/almanac/register/register.go" ,
sourcePackage : "register" ,
importPath : moduleImportPrefix + "generic/normalize/noop" ,
} ,
{
name : "family root cannot import child implementation" ,
filename : "internal/modules/almanac/types.go" ,
sourcePackage : "almanac" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
wantError : true ,
} ,
{
name : "family root cannot import registrar" ,
filename : "internal/modules/almanac/types.go" ,
sourcePackage : "almanac" ,
importPath : moduleImportPrefix + "almanac/register" ,
wantError : true ,
} ,
{
name : "child may import family root" ,
filename : "internal/modules/almanac/extract/events/extractor.go" ,
sourcePackage : "events" ,
importPath : moduleImportPrefix + "almanac" ,
} ,
{
name : "child may import same-family sibling" ,
filename : "internal/modules/almanac/extract/events/extractor.go" ,
sourcePackage : "events" ,
importPath : moduleImportPrefix + "almanac/shared" ,
} ,
{
name : "concrete registrar may compose own child" ,
filename : "internal/modules/almanac/register/register.go" ,
sourcePackage : "register" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
} ,
{
name : "generic registrar may compose generic child" ,
filename : "internal/modules/generic/register/register.go" ,
sourcePackage : "register" ,
importPath : moduleImportPrefix + "generic/output/json" ,
} ,
{
name : "application composition root may compose registrars" ,
filename : "internal/cli/catalog.go" ,
sourcePackage : "cli" ,
importPath : moduleImportPrefix + "almanac/register" ,
} ,
2026-07-17 15:27:17 +00:00
{
name : "production CLI cannot import family root" ,
filename : "internal/cli/catalog.go" ,
sourcePackage : "cli" ,
importPath : moduleImportPrefix + "almanac" ,
wantError : true ,
} ,
{
name : "production CLI cannot import concrete leaf" ,
filename : "internal/cli/catalog.go" ,
sourcePackage : "cli" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
wantError : true ,
} ,
{
name : "production CLI cannot import generic leaf" ,
filename : "internal/cli/catalog.go" ,
sourcePackage : "cli" ,
importPath : moduleImportPrefix + "generic/output/json" ,
wantError : true ,
} ,
{
name : "production CLI cannot import non-registrar package" ,
filename : "internal/cli/catalog.go" ,
sourcePackage : "cli" ,
importPath : moduleImportPrefix + "almanac/register/helpers" ,
wantError : true ,
} ,
{
name : "CLI test may import concrete leaf" ,
filename : "internal/cli/compatibility_test.go" ,
sourcePackage : "cli" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
} ,
2026-07-17 11:29:52 -05:00
{
name : "command production cannot import registrar" ,
filename : "cmd/notarius/main.go" ,
sourcePackage : "main" ,
importPath : moduleImportPrefix + "almanac/register" ,
wantError : true ,
} ,
{
name : "command production cannot import concrete leaf" ,
filename : "cmd/notarius/main.go" ,
sourcePackage : "main" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
wantError : true ,
} ,
{
name : "unknown production package cannot import concrete leaf" ,
filename : "internal/application/bootstrap.go" ,
sourcePackage : "application" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
wantError : true ,
} ,
{
name : "unknown production package cannot import registrar" ,
filename : "internal/application/bootstrap.go" ,
sourcePackage : "application" ,
importPath : moduleImportPrefix + "almanac/register" ,
wantError : true ,
} ,
{
name : "unknown test package is not a compatibility root" ,
filename : "internal/application/bootstrap_test.go" ,
sourcePackage : "application" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
wantError : true ,
} ,
2026-07-17 15:27:17 +00:00
{
name : "framework production cannot import concrete module" ,
filename : "internal/framework/pipeline/runner.go" ,
sourcePackage : "pipeline" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
wantError : true ,
} ,
{
name : "framework production cannot import generic module" ,
filename : "internal/framework/pipeline/runner.go" ,
sourcePackage : "pipeline" ,
importPath : moduleImportPrefix + "generic/normalize/noop" ,
wantError : true ,
} ,
{
name : "core production cannot import concrete module" ,
filename : "internal/core/source/source.go" ,
sourcePackage : "source" ,
importPath : moduleImportPrefix + "almanac" ,
wantError : true ,
} ,
{
name : "core production cannot import generic module" ,
filename : "internal/core/source/source.go" ,
sourcePackage : "source" ,
importPath : moduleImportPrefix + "generic/chunk/units" ,
wantError : true ,
} ,
2026-07-18 23:50:24 +00:00
{
name : "source production may import another core package" ,
filename : "internal/core/source/source.go" ,
sourcePackage : "source" ,
importPath : internalImportPrefix + "artifacts" ,
} ,
{
name : "source production cannot import framework" ,
filename : "internal/core/source/source.go" ,
sourcePackage : "source" ,
importPath : internalImportPrefix + "framework/contracts" ,
wantError : true ,
} ,
{
name : "source production cannot import modules" ,
filename : "internal/core/source/source.go" ,
sourcePackage : "source" ,
importPath : moduleImportPrefix + "dnd" ,
wantError : true ,
} ,
{
name : "chunkplan production may import framework" ,
filename : "internal/framework/chunkplan/store.go" ,
sourcePackage : "chunkplan" ,
importPath : internalImportPrefix + "framework/contracts" ,
} ,
{
name : "chunkplan production cannot import modules" ,
filename : "internal/framework/chunkplan/store.go" ,
sourcePackage : "chunkplan" ,
importPath : moduleImportPrefix + "dnd" ,
wantError : true ,
} ,
2026-07-17 15:27:17 +00:00
{
name : "framework test may import module implementation" ,
filename : "internal/framework/pipeline/compatibility_test.go" ,
sourcePackage : "pipeline" ,
importPath : moduleImportPrefix + "generic/chunk/units" ,
} ,
{
name : "core test may import module implementation" ,
filename : "internal/core/source/compatibility_test.go" ,
sourcePackage : "source" ,
importPath : moduleImportPrefix + "almanac" ,
} ,
{
name : "module production cannot import integration infrastructure" ,
filename : "internal/modules/almanac/register/register.go" ,
sourcePackage : "register" ,
importPath : moduleImportPrefix + "integration" ,
wantError : true ,
} ,
{
name : "non-module production cannot import integration infrastructure" ,
filename : "cmd/notarius/main.go" ,
sourcePackage : "main" ,
importPath : moduleImportPrefix + "integration/helpers" ,
wantError : true ,
} ,
{
name : "test may import integration infrastructure" ,
filename : "internal/cli/compatibility_test.go" ,
sourcePackage : "cli" ,
importPath : moduleImportPrefix + "integration" ,
} ,
2026-07-17 13:58:50 +00:00
{
name : "black-box integration test may compose families" ,
filename : "internal/modules/integration/example_test.go" ,
sourcePackage : "integration_test" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
} ,
{
name : "white-box integration test is not exempt" ,
filename : "internal/modules/integration/example_test.go" ,
sourcePackage : "integration" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
wantError : true ,
} ,
{
name : "integration production file is not exempt" ,
filename : "internal/modules/integration/example.go" ,
sourcePackage : "integration" ,
importPath : moduleImportPrefix + "almanac/extract/events" ,
wantError : true ,
2026-07-17 05:05:48 +00:00
} ,
}
for _ , tt := range tests {
t . Run ( tt . name , func ( t * testing . T ) {
2026-07-17 13:58:50 +00:00
err := validateImport ( tt . filename , tt . sourcePackage , tt . importPath )
2026-07-17 05:05:48 +00:00
if tt . wantError && err == nil {
t . Fatal ( "validateImport() error = nil, want boundary violation" )
}
2026-07-17 15:27:17 +00:00
if tt . wantError && ( ! strings . Contains ( err . Error ( ) , tt . filename ) || ! strings . Contains ( err . Error ( ) , tt . importPath ) ) {
t . Fatalf ( "validateImport() error = %q, want importing file and import target" , err )
}
2026-07-17 05:05:48 +00:00
if ! tt . wantError && err != nil {
t . Fatalf ( "validateImport() error = %v, want nil" , err )
}
} )
}
}
func checkImportBoundaries ( repositoryRoot string , filename string ) error {
parsed , err := parser . ParseFile ( token . NewFileSet ( ) , filename , nil , parser . ImportsOnly )
if err != nil {
return fmt . Errorf ( "parse %s: %w" , filename , err )
}
relative , err := filepath . Rel ( repositoryRoot , filename )
if err != nil {
return fmt . Errorf ( "resolve relative path for %s: %w" , filename , err )
}
relative = filepath . ToSlash ( relative )
for _ , imported := range parsed . Imports {
importPath , err := strconv . Unquote ( imported . Path . Value )
if err != nil {
return fmt . Errorf ( "parse import in %s: %w" , relative , err )
}
2026-07-17 13:58:50 +00:00
if err := validateImport ( relative , parsed . Name . Name , importPath ) ; err != nil {
2026-07-17 15:27:17 +00:00
return err
2026-07-17 05:05:48 +00:00
}
}
return nil
}
2026-07-17 13:58:50 +00:00
func validateImport ( filename string , sourcePackage string , importPath string ) error {
2026-07-18 23:50:24 +00:00
if ! strings . HasSuffix ( filename , "_test.go" ) && strings . HasPrefix ( filename , "internal/core/source/" ) && strings . HasPrefix ( importPath , internalImportPrefix + "framework/" ) {
return importBoundaryViolation ( filename , importPath , "core/source production code must not import framework or module implementations" )
}
2026-07-17 15:27:17 +00:00
target , ok := moduleTargetForImport ( importPath )
if ! ok {
2026-07-17 13:58:50 +00:00
return nil
}
2026-07-17 15:27:17 +00:00
isTest := strings . HasSuffix ( filename , "_test.go" )
if target . integration && ! isTest {
return importBoundaryViolation ( filename , importPath , "module integration infrastructure is not a production dependency target" )
2026-07-17 05:05:48 +00:00
}
2026-07-17 13:58:50 +00:00
if isIntegrationFile ( filename ) {
2026-07-17 15:27:17 +00:00
if isBlackBoxIntegrationTest ( filename , sourcePackage ) {
return nil
}
return importBoundaryViolation ( filename , importPath , "module integration composition is allowed only in black-box tests" )
}
2026-07-17 11:29:52 -05:00
sourceFamily , sourceRoot , sourceRegistrar := moduleFamilyForFile ( filename )
if sourceFamily != "" {
if sourceRoot && sourceFamily == target . family && target . child {
return importBoundaryViolation ( filename , importPath , "family root must not import child packages" )
}
if sourceFamily == target . family {
2026-07-17 15:27:17 +00:00
return nil
}
2026-07-17 11:29:52 -05:00
if sourceFamily == "generic" {
return importBoundaryViolation ( filename , importPath , fmt . Sprintf ( "generic family must not import concrete family %q" , target . family ) )
}
if target . family == "generic" {
if sourceRegistrar {
return nil
}
return importBoundaryViolation ( filename , importPath , fmt . Sprintf ( "concrete family %q may import generic implementations only from its registrar" , sourceFamily ) )
}
return importBoundaryViolation ( filename , importPath , fmt . Sprintf ( "concrete family %q must not import concrete family %q" , sourceFamily , target . family ) )
2026-07-17 05:05:48 +00:00
}
2026-07-17 11:29:52 -05:00
if isTest {
if isCompatibilityTestFile ( filename ) {
return nil
}
return importBoundaryViolation ( filename , importPath , "direct module imports from non-module tests are allowed only in CLI, core, and framework compatibility-test roots" )
2026-07-17 05:05:48 +00:00
}
2026-07-18 23:50:24 +00:00
if strings . HasPrefix ( filename , "internal/core/source/" ) && strings . HasPrefix ( importPath , moduleImportPrefix ) {
return importBoundaryViolation ( filename , importPath , "core/source production code must not import framework or module implementations" )
}
if strings . HasPrefix ( filename , "internal/framework/chunkplan/" ) && strings . HasPrefix ( importPath , moduleImportPrefix ) {
return importBoundaryViolation ( filename , importPath , "framework/chunkplan production code must not import module implementations" )
}
2026-07-17 11:29:52 -05:00
if strings . HasPrefix ( filename , "internal/framework/" ) || strings . HasPrefix ( filename , "internal/core/" ) {
return importBoundaryViolation ( filename , importPath , "core and framework production code must not import module implementations" )
2026-07-17 05:05:48 +00:00
}
2026-07-17 11:29:52 -05:00
if strings . HasPrefix ( filename , "internal/cli/" ) {
if target . registrar {
2026-07-17 13:58:50 +00:00
return nil
}
2026-07-17 11:29:52 -05:00
return importBoundaryViolation ( filename , importPath , "CLI production code may import only exact module family registrar packages" )
2026-07-17 05:05:48 +00:00
}
2026-07-17 11:29:52 -05:00
return importBoundaryViolation ( filename , importPath , "production code outside module families may import modules only from the CLI composition root through exact registrar packages" )
2026-07-17 15:27:17 +00:00
}
type moduleImportTarget struct {
family string
child bool
registrar bool
integration bool
}
func moduleTargetForImport ( importPath string ) ( moduleImportTarget , bool ) {
if ! strings . HasPrefix ( importPath , moduleImportPrefix ) {
return moduleImportTarget { } , false
}
remainder := strings . TrimPrefix ( importPath , moduleImportPrefix )
parts := strings . Split ( remainder , "/" )
if len ( parts ) == 0 || parts [ 0 ] == "" {
return moduleImportTarget { } , false
}
return moduleImportTarget {
family : parts [ 0 ] ,
child : len ( parts ) > 1 ,
registrar : len ( parts ) == 2 && parts [ 1 ] == "register" ,
integration : parts [ 0 ] == "integration" ,
} , true
}
func importBoundaryViolation ( filename string , importPath string , rule string ) error {
return fmt . Errorf ( "import boundary violation: %s imports %s: %s" , filename , importPath , rule )
2026-07-17 05:05:48 +00:00
}
2026-07-17 13:58:50 +00:00
func moduleFamilyForFile ( filename string ) ( family string , root bool , registrar bool ) {
2026-07-17 05:05:48 +00:00
const prefix = "internal/modules/"
if ! strings . HasPrefix ( filename , prefix ) {
2026-07-17 13:58:50 +00:00
return "" , false , false
2026-07-17 05:05:48 +00:00
}
remainder := strings . TrimPrefix ( filename , prefix )
parts := strings . Split ( remainder , "/" )
2026-07-17 13:58:50 +00:00
if len ( parts ) < 2 || parts [ 0 ] == "integration" {
return "" , false , false
2026-07-17 05:05:48 +00:00
}
2026-07-17 13:58:50 +00:00
return parts [ 0 ] , len ( parts ) == 2 , len ( parts ) > 2 && parts [ 1 ] == "register"
2026-07-17 05:05:48 +00:00
}
2026-07-17 13:58:50 +00:00
func isIntegrationFile ( filename string ) bool {
return strings . HasPrefix ( filename , "internal/modules/integration/" )
2026-07-17 05:05:48 +00:00
}
2026-07-17 13:58:50 +00:00
func isBlackBoxIntegrationTest ( filename string , sourcePackage string ) bool {
return isIntegrationFile ( filename ) && strings . HasSuffix ( filename , "_test.go" ) && sourcePackage == "integration_test"
2026-07-17 05:05:48 +00:00
}
2026-07-17 11:29:52 -05:00
func isCompatibilityTestFile ( filename string ) bool {
if ! strings . HasSuffix ( filename , "_test.go" ) {
return false
}
return strings . HasPrefix ( filename , "internal/cli/" ) ||
strings . HasPrefix ( filename , "internal/core/" ) ||
strings . HasPrefix ( filename , "internal/framework/" )
}
2026-07-17 05:05:48 +00:00
func testRepositoryRoot ( t * testing . T ) string {
t . Helper ( )
_ , filename , _ , ok := runtime . Caller ( 0 )
if ! ok {
t . Fatal ( "resolve import-boundary test location" )
}
return filepath . Clean ( filepath . Join ( filepath . Dir ( filename ) , ".." , ".." ) )
}