package topdata import ( "bytes" "errors" "os" "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 TestValidateProjectRejectsUnsupportedInvariantBaseRoots(t *testing.T) { tests := []struct { name string dataset string base string lock string }{ { name: "feat", dataset: "feat", base: `{ "output": "feat.2da", "columns": ["LABEL", "FEAT", "DESCRIPTION"], "rows": [ {"id": 0, "key": "feat:test", "LABEL": "FEAT_TEST", "FEAT": "100", "DESCRIPTION": "200"} ], "typo": true }` + "\n", lock: `{"feat:test":0}` + "\n", }, { name: "masterfeats", dataset: "masterfeats", base: `{ "output": "masterfeats.2da", "columns": ["LABEL", "STRREF", "DESCRIPTION"], "rows": [ {"id": 0, "key": "masterfeats:test", "LABEL": "Test", "STRREF": "100", "DESCRIPTION": "200"} ], "typo": true }` + "\n", lock: `{"masterfeats:test":0}` + "\n", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) datasetDir := filepath.Join(root, "topdata", "data", tc.dataset) mkdirAll(t, datasetDir) writeFile(t, filepath.Join(datasetDir, "base.json"), tc.base) writeFile(t, filepath.Join(datasetDir, "lock.json"), tc.lock) report := ValidateProject(testProject(root)) if !report.HasErrors() { t.Fatalf("expected unsupported invariant base-root key to fail validation, got:\n%s", diagnosticsText(report.Diagnostics)) } assertDiagnosticTextContainsAll(t, report.Diagnostics, "base.json root", "typo", "unsupported") }) } } 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{"global injection 0 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 TestValidateProjectReportsUnsupportedGlobalInjectionKeysAndInvalidAnyPresent(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{ "injections": [ { "row": {}, "when_present": [], "any_present": [] } ] }`+"\n") report := ValidateProject(testProject(root)) if !report.HasErrors() { t.Fatalf("expected invalid global injection to fail validation, got:\n%s", diagnosticsText(report.Diagnostics)) } assertDiagnosticTextContainsAll(t, report.Diagnostics, `global injection 0 contains unsupported key "when_present"`, "global injection 0 any_present must contain at least one condition", ) } func TestValidateProjectValidatesGlobalColumnsOnce(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) writeCanonicalSpellsDataset(t, root) writeFile(t, filepath.Join(root, "topdata", "data", "spells", "global.json"), `{ "columns": "ImpactScript", "entries": {}, "overrides": [] }`+"\n") report := ValidateProject(testProject(root)) if !report.HasErrors() { t.Fatalf("expected invalid global columns file to fail validation, got:\n%s", diagnosticsText(report.Diagnostics)) } text := diagnosticsText(report.Diagnostics) if got := strings.Count(text, "columns must be a JSON array when present"); got != 1 { t.Fatalf("expected one global columns diagnostic, got %d:\n%s", got, text) } } 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 TestBuildNativeAnyPresent(t *testing.T) { tests := []struct { name string rows string wantInjected bool }{ { name: "first alternative", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"} ]`, wantInjected: true, }, { name: "later alternative", rows: `[ {"id": 0, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"} ]`, wantInjected: true, }, { name: "no alternatives", rows: `[ {"id": 0, "SkillLabel": "Concentration", "SkillIndex": {"id": "skills:concentration"}, "ClassSkill": "1"} ]`, wantInjected: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{ "injections": [ { "row": { "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1" }, "any_present": [ {"field": "SkillIndex", "id": "skills:athletics"}, {"field": "SkillIndex", "id": "skills:tumble"} ] } ] }`+"\n") proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err != nil { t.Fatalf("BuildNativeWithOptions failed: %v", err) } table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da")) if got := tableContainsCellValue(table, "SkillLabel", "ProfessionFarmer"); got != tc.wantInjected { t.Fatalf("ProfessionFarmer presence = %v, want %v", got, tc.wantInjected) } }) } } func TestBuildNativeCombinesGlobalConditionGroups(t *testing.T) { t.Run("require_present and any_present", func(t *testing.T) { tests := []struct { name string rows string wantInjected bool }{ { name: "both pass", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}, {"id": 1, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"} ]`, wantInjected: true, }, { name: "require_present fails", rows: `[ {"id": 0, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"} ]`, wantInjected: false, }, { name: "any_present fails", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"} ]`, wantInjected: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{ "injections": [ { "row": { "SkillLabel": "DualGate", "SkillIndex": {"id": "skills:lore"}, "ClassSkill": "1" }, "require_present": [ {"field": "SkillIndex", "id": "skills:athletics"} ], "any_present": [ {"field": "SkillIndex", "id": "skills:tumble"} ] } ] }`+"\n") proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err != nil { t.Fatalf("BuildNativeWithOptions failed: %v", err) } table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da")) if got := tableContainsCellValue(table, "SkillLabel", "DualGate"); got != tc.wantInjected { t.Fatalf("DualGate presence = %v, want %v", got, tc.wantInjected) } }) } }) t.Run("any_present and unless_present", func(t *testing.T) { tests := []struct { name string rows string wantInjected bool }{ { name: "both pass", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"} ]`, wantInjected: true, }, { name: "any_present fails", rows: `[ {"id": 0, "SkillLabel": "Concentration", "SkillIndex": {"id": "skills:concentration"}, "ClassSkill": "1"} ]`, wantInjected: false, }, { name: "unless_present fails", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}, {"id": 1, "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"} ]`, wantInjected: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{ "injections": [ { "row": { "SkillLabel": "NeedsAnyWithoutBlocked", "SkillIndex": {"id": "skills:lore"}, "ClassSkill": "1" }, "any_present": [ {"field": "SkillIndex", "id": "skills:athletics"} ], "unless_present": [ {"field": "SkillIndex", "id": "skills:profession_farmer"} ] } ] }`+"\n") proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err != nil { t.Fatalf("BuildNativeWithOptions failed: %v", err) } table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da")) if got := tableContainsCellValue(table, "SkillLabel", "NeedsAnyWithoutBlocked"); got != tc.wantInjected { t.Fatalf("NeedsAnyWithoutBlocked presence = %v, want %v", got, tc.wantInjected) } }) } }) t.Run("require_present keeps all semantics", func(t *testing.T) { tests := []struct { name string rows string wantInjected bool }{ { name: "all present", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}, {"id": 1, "SkillLabel": "Tumble", "SkillIndex": {"id": "skills:tumble"}, "ClassSkill": "1"} ]`, wantInjected: true, }, { name: "missing one required row", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"} ]`, wantInjected: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{ "injections": [ { "row": { "SkillLabel": "AllRequired", "SkillIndex": {"id": "skills:lore"}, "ClassSkill": "1" }, "require_present": [ {"field": "SkillIndex", "id": "skills:athletics"}, {"field": "SkillIndex", "id": "skills:tumble"} ] } ] }`+"\n") proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err != nil { t.Fatalf("BuildNativeWithOptions failed: %v", err) } table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da")) if got := tableContainsCellValue(table, "SkillLabel", "AllRequired"); got != tc.wantInjected { t.Fatalf("AllRequired presence = %v, want %v", got, tc.wantInjected) } }) } }) t.Run("unless_present keeps none semantics", func(t *testing.T) { tests := []struct { name string rows string wantInjected bool }{ { name: "none present", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"} ]`, wantInjected: true, }, { name: "one blocked row present", rows: `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"}, {"id": 1, "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1"} ]`, wantInjected: false, }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalPlainSkillsFixture(t, root, tc.rows, `{ "injections": [ { "row": { "SkillLabel": "NoneBlocked", "SkillIndex": {"id": "skills:lore"}, "ClassSkill": "1" }, "unless_present": [ {"field": "SkillIndex", "id": "skills:profession_farmer"}, {"field": "SkillIndex", "id": "skills:stealth"} ] } ] }`+"\n") proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err != nil { t.Fatalf("BuildNativeWithOptions failed: %v", err) } table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da")) if got := tableContainsCellValue(table, "SkillLabel", "NoneBlocked"); got != tc.wantInjected { t.Fatalf("NoneBlocked presence = %v, want %v", got, tc.wantInjected) } }) } }) } func TestBuildNativeUsesCurrentRowsForAnyPresent(t *testing.T) { root := testProjectRoot(t) writeCanonicalPlainSkillsFixture(t, root, `[ {"id": 0, "SkillLabel": "Athletics", "SkillIndex": {"id": "skills:athletics"}, "ClassSkill": "1"} ]`, `{ "injections": [ { "row": { "SkillLabel": "ProfessionFarmer", "SkillIndex": {"id": "skills:profession_farmer"}, "ClassSkill": "1" } }, { "row": { "SkillLabel": "Lore", "SkillIndex": {"id": "skills:lore"}, "ClassSkill": "1" }, "any_present": [ {"field": "SkillIndex", "id": "skills:profession_farmer"} ] } ] }`+"\n") proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" result, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err != nil { t.Fatalf("BuildNativeWithOptions failed: %v", err) } table := read2DATable(t, filepath.Join(result.Output2DADir, "cls_skill_guard.2da")) for _, label := range []string{"ProfessionFarmer", "Lore"} { if !tableContainsCellValue(table, "SkillLabel", label) { t.Fatalf("expected %s injection to apply after earlier mutation", label) } } } func TestBuildNativeRejectsUnsupportedGlobalControlSyntax(t *testing.T) { tests := []struct { name string globalJSON string fragments []string }{ { name: "when_present injection key", globalJSON: `{ "injections": [ { "row": {"ImpactScript": "ss_new_spell"}, "when_present": [] } ] }` + "\n", fragments: []string{"global injection 0", "when_present", "unsupported"}, }, { name: "unknown condition key", globalJSON: `{ "injections": [ { "row": {"ImpactScript": "ss_new_spell"}, "require_present": [ {"field": "FeatIndex", "id": "feat:gate", "typo": true} ] } ] }` + "\n", fragments: []string{"require_present[0]", "typo", "unsupported"}, }, { name: "empty any_present", globalJSON: `{ "injections": [ { "row": {"ImpactScript": "ss_new_spell"}, "any_present": [] } ] }` + "\n", 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"), tc.globalJSON) proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" _, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err == nil { t.Fatal("expected BuildNativeWithOptions to fail") } for _, fragment := range tc.fragments { if !strings.Contains(err.Error(), fragment) { t.Fatalf("error %q does not contain %q", err, fragment) } } if _, statErr := os.Stat(filepath.Join(compiled2DAOutputDir(proj), "spells.2da")); !errors.Is(statErr, os.ErrNotExist) { t.Fatalf("expected spells.2da to remain absent, got err %v", statErr) } }) } } func TestBuildNativeRejectsUnsupportedCanonicalRoot(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) writeCanonicalSpellsBase(t, root, `{ "output": "spells.2da", "columns": ["Label"], "rows": [ {"id": 0, "key": "spells:acid_fog", "Label": "AcidFog"} ], "typo": true }`+"\n") proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" _, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err == nil { t.Fatal("expected BuildNativeWithOptions to fail") } for _, fragment := range []string{"base.json root", "typo", "unsupported"} { if !strings.Contains(err.Error(), fragment) { t.Fatalf("error %q does not contain %q", err, fragment) } } if _, statErr := os.Stat(filepath.Join(compiled2DAOutputDir(proj), "spells.2da")); !errors.Is(statErr, os.ErrNotExist) { t.Fatalf("expected spells.2da to remain absent, got err %v", statErr) } } func TestBuildNativeRejectsUnsupportedInvariantBaseRootsWithoutChangingOutput(t *testing.T) { tests := []struct { name string dataset string outputName string base string lock string }{ { name: "feat", dataset: "feat", outputName: "feat.2da", base: `{ "output": "feat.2da", "columns": ["LABEL", "FEAT", "DESCRIPTION"], "rows": [ {"id": 0, "key": "feat:test", "LABEL": "FEAT_TEST", "FEAT": "100", "DESCRIPTION": "200"} ], "typo": true }` + "\n", lock: `{"feat:test":0}` + "\n", }, { name: "masterfeats", dataset: "masterfeats", outputName: "masterfeats.2da", base: `{ "output": "masterfeats.2da", "columns": ["LABEL", "STRREF", "DESCRIPTION"], "rows": [ {"id": 0, "key": "masterfeats:test", "LABEL": "Test", "STRREF": "100", "DESCRIPTION": "200"} ], "typo": true }` + "\n", lock: `{"masterfeats:test":0}` + "\n", }, } for _, tc := range tests { t.Run(tc.name, func(t *testing.T) { root := testProjectRoot(t) writeCanonicalContractScaffold(t, root) datasetDir := filepath.Join(root, "topdata", "data", tc.dataset) mkdirAll(t, datasetDir) writeFile(t, filepath.Join(datasetDir, "base.json"), tc.base) writeFile(t, filepath.Join(datasetDir, "lock.json"), tc.lock) proj := testProject(root) proj.Config.TopData.ReferenceBuilder = "" outputPath := filepath.Join(compiled2DAOutputDir(proj), tc.outputName) mkdirAll(t, filepath.Dir(outputPath)) sentinel := []byte("pre-existing output sentinel\n") writeBytes(t, outputPath, sentinel) _, err := BuildNativeWithOptions(proj, NativeBuildOptions{BuildWiki: false}, nil) if err == nil { t.Fatal("expected BuildNativeWithOptions to fail") } for _, fragment := range []string{"base.json root", "typo", "unsupported"} { if !strings.Contains(err.Error(), fragment) { t.Fatalf("error %q does not contain %q", err, fragment) } } got, readErr := os.ReadFile(outputPath) if readErr != nil { t.Fatalf("read preserved output: %v", readErr) } if !bytes.Equal(got, sentinel) { t.Fatalf("output changed after invalid build error:\ngot: %q\nwant: %q", got, sentinel) } }) } } func tableContainsCellValue(table twoDATable, column, want string) bool { for _, row := range table.rows { if row[column] == want { return true } } return false } 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 writeCanonicalPlainSkillsFixture(t *testing.T, root, datasetRows, globalJSON string) { t.Helper() writeCanonicalContractScaffold(t, root) mkdirAll(t, filepath.Join(root, "topdata", "data", "classes", "skills")) writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "global.json"), globalJSON) writeFile(t, filepath.Join(root, "topdata", "data", "classes", "skills", "guard.json"), `{ "key": "classes/skills:guard", "output": "cls_skill_guard.2da", "columns": ["SkillLabel", "SkillIndex", "ClassSkill"], "rows": `+datasetRows+` }`+"\n") mkdirAll(t, filepath.Join(root, "topdata", "data", "skills")) writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{ "output": "skills.2da", "columns": ["Label"], "rows": [ {"id": 1, "key": "skills:athletics", "Label": "Athletics"}, {"id": 2, "key": "skills:tumble", "Label": "Tumble"}, {"id": 3, "key": "skills:concentration", "Label": "Concentration"}, {"id": 4, "key": "skills:profession_farmer", "Label": "ProfessionFarmer"}, {"id": 5, "key": "skills:lore", "Label": "Lore"}, {"id": 6, "key": "skills:stealth", "Label": "Stealth"} ] }`+"\n") writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{ "skills:athletics": 1, "skills:tumble": 2, "skills:concentration": 3, "skills:profession_farmer": 4, "skills:lore": 5, "skills:stealth": 6 }`+"\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) } } }