package topdata import ( "path/filepath" "reflect" "slices" "strings" "testing" ) func TestUnsupportedObjectKeysAreSorted(t *testing.T) { obj := map[string]any{ "row": map[string]any{}, "when_present": []any{}, "aaa": true, } got := unsupportedObjectKeys( obj, "row", "require_present", "any_present", "unless_present", ) want := []string{"aaa", "when_present"} if !slices.Equal(got, want) { t.Fatalf("unsupportedObjectKeys() = %v, want %v", got, want) } } func TestUnsupportedObjectKeysErrorNamesObjectAndContract(t *testing.T) { err := unsupportedObjectKeysError( "global injection 10", map[string]any{"row": map[string]any{}, "when_present": []any{}}, "row", "require_present", "any_present", "unless_present", ) if err == nil { t.Fatal("expected unsupported-property error") } for _, want := range []string{ "global injection 10", `"when_present"`, "row", "require_present", "any_present", "unless_present", } { if !strings.Contains(err.Error(), want) { t.Fatalf("error %q does not contain %q", err, want) } } } func TestParseGlobalConditionGroups(t *testing.T) { tests := []struct { name string injection map[string]any want globalConditionGroups wantError string }{ { name: "all groups", injection: map[string]any{ "require_present": []any{ map[string]any{"field": "FeatIndex", "id": "feat:required"}, }, "any_present": []any{ map[string]any{"field": "FeatIndex", "id": "feat:first"}, map[string]any{"field": "FeatIndex", "id": "feat:second"}, }, "unless_present": []any{ map[string]any{"field": "FeatIndex", "id": "feat:blocked"}, }, }, want: globalConditionGroups{ RequirePresent: []globalCondition{ {Field: "FeatIndex", ID: "feat:required"}, }, AnyPresent: []globalCondition{ {Field: "FeatIndex", ID: "feat:first"}, {Field: "FeatIndex", ID: "feat:second"}, }, UnlessPresent: []globalCondition{ {Field: "FeatIndex", ID: "feat:blocked"}, }, }, }, { name: "empty existing groups remain valid", injection: map[string]any{ "require_present": []any{}, "unless_present": []any{}, }, want: globalConditionGroups{ RequirePresent: []globalCondition{}, UnlessPresent: []globalCondition{}, }, }, { name: "require present null is rejected", injection: map[string]any{ "require_present": nil, }, wantError: "require_present must be an array", }, { name: "unless present null is rejected", injection: map[string]any{ "unless_present": nil, }, wantError: "unless_present must be an array", }, { name: "any present must be array", injection: map[string]any{ "any_present": "feat:first", }, wantError: "any_present must be an array", }, { name: "any present must not be empty", injection: map[string]any{ "any_present": []any{}, }, wantError: "any_present must contain at least one condition", }, { name: "condition is closed", injection: map[string]any{ "require_present": []any{ map[string]any{ "field": "FeatIndex", "id": "feat:required", "typo": true, }, }, }, wantError: `require_present[0] contains unsupported key "typo"`, }, { name: "field is required", injection: map[string]any{ "require_present": []any{ map[string]any{"id": "feat:required"}, }, }, wantError: "require_present[0].field is required", }, { name: "id is required", injection: map[string]any{ "require_present": []any{ map[string]any{"field": "FeatIndex"}, }, }, wantError: "require_present[0].id is required", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { got, err := parseGlobalConditionGroups(tc.injection) if tc.wantError != "" { if err == nil { t.Fatalf("parseGlobalConditionGroups() error = nil, want %q", tc.wantError) } if !strings.Contains(err.Error(), tc.wantError) { t.Fatalf("parseGlobalConditionGroups() error = %q, want contains %q", err, tc.wantError) } return } if err != nil { t.Fatalf("parseGlobalConditionGroups() error = %v", err) } if !reflect.DeepEqual(got, tc.want) { t.Fatalf("parseGlobalConditionGroups() = %#v, want %#v", got, tc.want) } }) } t.Run("absent groups remain valid", func(t *testing.T) { got, err := parseGlobalConditionGroups(map[string]any{}) if err != nil { t.Fatalf("parseGlobalConditionGroups() error = %v", err) } if !reflect.DeepEqual(got, globalConditionGroups{}) { t.Fatalf("parseGlobalConditionGroups() = %#v, want zero value", got) } }) } func TestGlobalConditionGroupsMatch(t *testing.T) { rowsWithoutBlocked := []map[string]any{ {"FeatIndex": map[string]any{"id": "feat:required"}}, {"FeatIndex": map[string]any{"id": "feat:second"}}, } rowsWithBlocked := append([]map[string]any(nil), rowsWithoutBlocked...) rowsWithBlocked = append(rowsWithBlocked, map[string]any{"FeatIndex": map[string]any{"id": "feat:blocked"}}) if !globalConditionGroupsMatch(globalConditionGroups{ RequirePresent: []globalCondition{ {Field: "FeatIndex", ID: "feat:required"}, }, AnyPresent: []globalCondition{ {Field: "FeatIndex", ID: "feat:first"}, {Field: "FeatIndex", ID: "feat:second"}, }, }, rowsWithoutBlocked) { t.Fatal("expected required + later any_present alternative to match") } if globalConditionGroupsMatch(globalConditionGroups{ AnyPresent: []globalCondition{ {Field: "FeatIndex", ID: "feat:missing"}, }, }, rowsWithoutBlocked) { t.Fatal("expected missing any_present alternatives to reject") } if globalConditionGroupsMatch(globalConditionGroups{ UnlessPresent: []globalCondition{ {Field: "FeatIndex", ID: "feat:blocked"}, }, }, rowsWithBlocked) { t.Fatal("expected unless_present match to reject") } } func TestValidateProjectRejectsUnsupportedCanonicalJSON(t *testing.T) { tests := []struct { name string setup func(t *testing.T, root string) fragments []string }{ { name: "base root", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsBase(t, root, `{ "output": "spells.2da", "columns": ["Label"], "rows": [ {"id": 0, "key": "spells:acid_fog", "Label": "AcidFog"} ], "typo": true }`+"\n") }, fragments: []string{"base.json root", "typo", "unsupported"}, }, { name: "plain rows root", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "categories.json"), `{ "output": "categories.2da", "columns": ["Category"], "rows": [ {"id": 0, "key": "categories:harmful", "Category": "Harmful"} ], "typo": true }`+"\n") }, fragments: []string{"plain rows root", "typo", "unsupported"}, }, { name: "module root", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "entry.json"), `{ "entries": { "spells:burning_hands": {"Label": "BurningHands"} }, "typo": true }`+"\n") }, fragments: []string{"module root", "typo", "unsupported"}, }, { name: "global root", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{ "position": "append", "typo": true }`+"\n") }, fragments: []string{"global.json root", "typo", "unsupported"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) tc.setup(t, root) report := ValidateProject(testProject(root)) if !report.HasErrors() { t.Fatalf("expected canonical unsupported-key validation error, got:\n%s", diagnosticsText(report.Diagnostics)) } assertDiagnosticTextContainsAll(t, report.Diagnostics, tc.fragments...) }) } } func TestValidateProjectRejectsUnsupportedGlobalJSON(t *testing.T) { tests := []struct { name string globalJSON string fragments []string }{ { name: "injection object is closed", globalJSON: `{ "injections": [ { "row": {}, "when_present": [] } ] }` + "\n", fragments: []string{"global injection 0", "when_present", "unsupported"}, }, { name: "condition object is closed", globalJSON: `{ "injections": [ { "row": {}, "require_present": [ {"field": "FeatIndex", "id": "feat:x", "typo": true} ] } ] }` + "\n", fragments: []string{"require_present[0]", "typo", "unsupported"}, }, { name: "default rule is closed", globalJSON: `{ "defaults": [ { "match": "all", "values": {}, "typo": true } ] }` + "\n", fragments: []string{"global default 0", "typo", "unsupported"}, }, { name: "default match object is closed", globalJSON: `{ "defaults": [ { "match": {"source": "entries", "typo": true}, "values": {} } ] }` + "\n", fragments: []string{"global default 0 match", "typo", "unsupported"}, }, { name: "default format object is closed", globalJSON: `{ "defaults": [ { "match": "all", "values": { "ImpactScript": {"format": "ss_{id}", "typo": true} } } ] }` + "\n", fragments: []string{"global default 0 values.ImpactScript", "typo", "unsupported"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), tc.globalJSON) report := ValidateProject(testProject(root)) if !report.HasErrors() { t.Fatalf("expected global unsupported-key validation error, got:\n%s", diagnosticsText(report.Diagnostics)) } assertDiagnosticTextContainsAll(t, report.Diagnostics, tc.fragments...) }) } } func TestValidateProjectAcceptsAnyPresent(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{ "injections": [ { "row": {"ImpactScript": "ss_new_spell"}, "any_present": [ {"field": "FeatIndex", "id": "feat:gate"} ] } ] }`+"\n") report := ValidateProject(testProject(root)) if report.HasErrors() { t.Fatalf("expected non-empty any_present to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics)) } } func TestValidateProjectRejectsInvalidAnyPresent(t *testing.T) { tests := []struct { name string injection string fragments []string }{ { name: "string", injection: `{ "row": {}, "any_present": "feat:gate" }`, fragments: []string{"any_present", "array"}, }, { name: "empty list", injection: `{ "row": {}, "any_present": [] }`, fragments: []string{"any_present", "at least one condition"}, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{ "injections": [ `+tc.injection+` ] }`+"\n") report := ValidateProject(testProject(root)) if !report.HasErrors() { t.Fatalf("expected invalid any_present validation error, got:\n%s", diagnosticsText(report.Diagnostics)) } assertDiagnosticTextContainsAll(t, report.Diagnostics, tc.fragments...) }) } } func TestValidateProjectValidatesEveryCanonicalModuleContainer(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) writeCanonicalRepadjustDataset(t, root) modulePath := filepath.Join(root, "topdata", "data", "repadjust", "modules", "combined.json") writeFile(t, modulePath, `{ "entries": { "repadjust:burning_hands": {"Label": "BurningHands"} }, "overrides": [ {"key": "repadjust:test", "WIKISUMMARY": "deprecated"} ] }`+"\n") report := ValidateProject(testProject(root)) if !report.HasErrors() { t.Fatalf("expected combined entries+overrides module to validate every container, got:\n%s", diagnosticsText(report.Diagnostics)) } assertDiagnosticTextContainsAll(t, report.Diagnostics, "deprecated wiki field", "WIKISUMMARY") writeFile(t, modulePath, `{ "entries": { "repadjust:burning_hands": {"Label": "BurningHands"} }, "overrides": [ {"key": "repadjust:test", "Label": "FixedLabel"} ] }`+"\n") report = ValidateProject(testProject(root)) if report.HasErrors() { t.Fatalf("expected fixed combined entries+overrides module to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics)) } } func TestValidateProjectAcceptsCanonicalJSONContracts(t *testing.T) { tests := []struct { name string setup func(t *testing.T, root string) }{ { name: "columns only module", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "columns.json"), `{ "columns": ["ImpactScript"] }`+"\n") }, }, { name: "entries only module", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "entries.json"), `{ "entries": { "spells:burning_hands": {"Label": "BurningHands"} } }`+"\n") }, }, { name: "overrides only module", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "overrides.json"), `{ "overrides": [ {"key": "spells:acid_fog", "ImpactScript": "ss_acid_fog"} ] }`+"\n") }, }, { name: "rows module", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "modules", "rows.json"), `{ "rows": [ {"key": "spells:burning_hands", "Label": "BurningHands"} ] }`+"\n") }, }, { name: "current base root", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsBase(t, root, `{ "output": "spells.2da", "columns": ["Label"], "rows": [ {"id": 0, "key": "spells:acid_fog", "Label": "AcidFog"} ] }`+"\n") }, }, { name: "loose plain rows root", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "ruleset.json"), `{ "columns": ["Name", "Value"], "rows": [ {"id": 0, "Name": "TEST_RULE", "Value": "1"} ] }`+"\n") }, }, { name: "plain rows root compare reference false", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "categories.json"), `{ "output": "categories.2da", "compare_reference": false, "columns": ["Category"], "rows": [ {"id": 0, "key": "categories:harmful", "Category": "Harmful"} ] }`+"\n") }, }, { name: "row like payload helpers remain valid", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsBase(t, root, `{ "output": "spells.2da", "columns": ["Name", "Parent", "ImpactScript"], "rows": [ { "id": 0, "key": "spells:acid_fog", "Parent": "spells:acid_fog", "ImpactScript": "ss_acid_fog", "inherit": {"from": "Parent", "fields": ["ImpactScript"]}, "_tlk": {"name": "Acid Fog"}, "meta": {} } ] }`+"\n") }, }, { name: "global injection row payload stays open", setup: func(t *testing.T, root string) { writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{ "injections": [ { "row": {"ImpactScript": "ss_new_spell"} } ] }`+"\n") }, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) tc.setup(t, root) report := ValidateProject(testProject(root)) if report.HasErrors() { t.Fatalf("expected canonical contract to validate cleanly, got:\n%s", diagnosticsText(report.Diagnostics)) } }) } } func writeCanonicalContractScaffold(t *testing.T, root string) { t.Helper() mkdirAll(t, filepath.Join(root, "topdata", "data")) writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n") } func writeCanonicalSpellsDataset(t *testing.T, root string) { t.Helper() writeCanonicalSpellsBase(t, root, `{ "output": "spells.2da", "columns": ["Label", "ImpactScript"], "rows": [ {"id": 0, "key": "spells:acid_fog", "Label": "AcidFog", "ImpactScript": "ss_acid_fog"} ] }`+"\n") writeFile(t, filepath.Join(root, "topdata", "data", "spells", "lock.json"), `{"spells:acid_fog":0}`+"\n") mkdirAll(t, filepath.Join(root, "topdata", "data", "spells", "modules")) } func writeCanonicalSpellsBase(t *testing.T, root string, content string) { t.Helper() mkdirAll(t, filepath.Join(root, "topdata", "data", "spells")) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "base.json"), content) } func writeCanonicalRepadjustDataset(t *testing.T, root string) { t.Helper() mkdirAll(t, filepath.Join(root, "topdata", "data", "repadjust", "modules")) writeFile(t, filepath.Join(root, "topdata", "data", "repadjust", "base.json"), `{ "output": "repadjust.2da", "columns": ["Label"], "rows": [ {"id": 0, "key": "repadjust:test", "Label": "TestLabel"} ] }`+"\n") } func assertDiagnosticTextContainsAll(t *testing.T, diags []Diagnostic, fragments ...string) { t.Helper() text := diagnosticsText(diags) for _, fragment := range fragments { if !strings.Contains(text, fragment) { t.Fatalf("expected diagnostics to contain %q, got:\n%s", fragment, text) } } }