package project import ( "errors" "os" "path/filepath" "slices" "strings" "testing" ) func TestTopDataAssetsDirPrefersExplicitConfig(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "src")) mkdirAll(t, filepath.Join(root, "assets")) mkdirAll(t, filepath.Join(root, "build")) mkdirAll(t, filepath.Join(root, "custom-assets")) proj := &Project{ Root: root, Config: Config{ Module: ModuleConfig{Name: "Test", ResRef: "test"}, Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"}, TopData: TopDataConfig{ Assets: "custom-assets", }, }, } result := proj.TopDataAssetsDir() expected := filepath.Join(root, "custom-assets") if result != expected { t.Errorf("expected %s, got %s", expected, result) } } func TestTopDataAssetsDirDoesNotAssumeSiblingRepo(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "src")) mkdirAll(t, filepath.Join(root, "assets")) mkdirAll(t, filepath.Join(root, "build")) parentDir := filepath.Dir(root) siblingPath := filepath.Join(parentDir, "sow-assets") mkdirAll(t, siblingPath) mkdirAll(t, filepath.Join(siblingPath, "assets", "part", "belt")) proj := &Project{ Root: root, Config: Config{ Module: ModuleConfig{Name: "Test", ResRef: "test"}, Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"}, TopData: TopDataConfig{ Assets: "sow-assets", }, }, } result := proj.TopDataAssetsDir() expected := filepath.Join(root, "sow-assets") if result != expected { t.Errorf("expected unresolved local path %s, got %s", expected, result) } } func TestTopDataAssetsDirReturnsEmptyForNoConfig(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "src")) mkdirAll(t, filepath.Join(root, "assets")) mkdirAll(t, filepath.Join(root, "build")) proj := &Project{ Root: root, Config: Config{ Module: ModuleConfig{Name: "Test", ResRef: "test"}, Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"}, TopData: TopDataConfig{}, }, } result := proj.TopDataAssetsDir() if result != "" { t.Errorf("expected empty string, got %s", result) } } func TestModuleArchivePathUsesConfiguredBuildDirAndResRef(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "output")) proj := &Project{ Root: root, Config: Config{ Module: ModuleConfig{Name: "Test", ResRef: "testmod"}, Paths: PathConfig{Build: "output"}, }, } got := proj.ModuleArchivePath() want := filepath.Join(root, "output", "testmod.mod") if got != want { t.Fatalf("expected %s, got %s", want, got) } } func TestHAKPathsUseConfiguredBuildDir(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "output")) proj := &Project{ Root: root, Config: Config{ Paths: PathConfig{Build: "output"}, }, } if got, want := proj.HAKManifestPath(), filepath.Join(root, "output", "haks.json"); got != want { t.Fatalf("expected manifest path %s, got %s", want, got) } if got, want := proj.HAKArchivePath("core_01"), filepath.Join(root, "output", "core_01.hak"); got != want { t.Fatalf("expected hak path %s, got %s", want, got) } } func TestTopDataPackagePathsUseConfiguredNamesAndBuildDir(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "output")) proj := &Project{ Root: root, Config: Config{ Paths: PathConfig{Build: "output"}, TopData: TopDataConfig{ PackageHAK: "custom_top.hak", PackageTLK: "custom_dialog.tlk", }, }, } if got, want := proj.TopDataPackageHAKPath(), filepath.Join(root, "output", "custom_top.hak"); got != want { t.Fatalf("expected top hak path %s, got %s", want, got) } if got, want := proj.TopDataPackageTLKPath(), filepath.Join(root, "output", "custom_dialog.tlk"); got != want { t.Fatalf("expected top tlk path %s, got %s", want, got) } } func TestValidateLayoutAllowsMissingAssetsDir(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "src")) mkdirAll(t, filepath.Join(root, "build")) proj := &Project{ Root: root, Config: Config{ Module: ModuleConfig{Name: "Test", ResRef: "test"}, Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"}, }, } if err := proj.ValidateLayout(); err != nil { t.Fatalf("ValidateLayout returned error: %v", err) } if _, err := os.Stat(filepath.Join(root, "assets")); !errors.Is(err, os.ErrNotExist) { t.Fatalf("expected missing assets dir to remain absent, got err=%v", err) } } func TestValidateLayoutRejectsInvalidTopDataPackageOutputNames(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "src")) mkdirAll(t, filepath.Join(root, "build")) proj := &Project{ Root: root, Config: Config{ Module: ModuleConfig{Name: "Test", ResRef: "test"}, Paths: PathConfig{Source: "src", Build: "build"}, TopData: TopDataConfig{ Source: "topdata", PackageHAK: "nested/custom_top.hak", PackageTLK: "custom_dialog.txt", }, }, } err := proj.ValidateLayout() if err == nil { t.Fatal("expected topdata output validation error") } if !strings.Contains(err.Error(), "topdata.package_hak") { t.Fatalf("expected package_hak validation error, got %v", err) } if !strings.Contains(err.Error(), "topdata.package_tlk") { t.Fatalf("expected package_tlk validation error, got %v", err) } } func TestScanAllowsMissingAssetsDir(t *testing.T) { root := t.TempDir() mkdirAll(t, filepath.Join(root, "src")) mkdirAll(t, filepath.Join(root, "build")) proj := &Project{ Root: root, Config: Config{ Module: ModuleConfig{Name: "Test", ResRef: "test"}, Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"}, }, } if err := proj.Scan(); err != nil { t.Fatalf("Scan returned error: %v", err) } if len(proj.Inventory.AssetFiles) != 0 { t.Fatalf("expected no asset files, got %#v", proj.Inventory.AssetFiles) } } func mkdirAll(t *testing.T, path string) { t.Helper() if err := os.MkdirAll(path, 0o755); err != nil { t.Fatalf("mkdir %s: %v", path, err) } } func TestCloneWithHAKNamesFiltersConfiguredHAKs(t *testing.T) { proj := &Project{ Root: "/tmp/test", Config: Config{ Module: ModuleConfig{Name: "Test", ResRef: "test"}, Paths: PathConfig{Source: "src", Assets: "assets", Build: "build"}, HAKs: []HAKConfig{ {Name: "sow_over", Priority: 1, Include: []string{"over/**"}}, {Name: "sow_core", Priority: 2, Include: []string{"core/**"}}, {Name: "sow_item", Priority: 3, Include: []string{"item/**"}}, }, }, } filtered, err := proj.CloneWithHAKNames([]string{"sow_core", "sow_item"}) if err != nil { t.Fatalf("CloneWithHAKNames returned error: %v", err) } if filtered == proj { t.Fatalf("expected filtered clone, got original pointer") } got := make([]string, 0, len(filtered.Config.HAKs)) for _, hak := range filtered.Config.HAKs { got = append(got, hak.Name) } want := []string{"sow_core", "sow_item"} if !slices.Equal(got, want) { t.Fatalf("unexpected filtered haks: got %v want %v", got, want) } if len(proj.Config.HAKs) != 3 { t.Fatalf("original project config should remain unchanged, got %d haks", len(proj.Config.HAKs)) } } func TestCloneWithHAKNamesRejectsUnknownHAKs(t *testing.T) { proj := &Project{ Config: Config{ HAKs: []HAKConfig{ {Name: "sow_core", Priority: 1, Include: []string{"core/**"}}, }, }, } if _, err := proj.CloneWithHAKNames([]string{"missing_hak"}); err == nil { t.Fatal("expected error for unknown hak name") } }