Closing gaps and cleaning house

This commit is contained in:
2026-05-14 21:08:38 +02:00
parent 4199d615a3
commit 59994c8b29
6 changed files with 483 additions and 19 deletions
+271
View File
@@ -2606,6 +2606,132 @@ func TestResolveGeneratedFeatIdentityUsesLegacyAliasForNoSourceFamily(t *testing
}
}
func TestResolveGeneratedFeatIdentityUsesLegacyAliasForUnderscoreNoSourceFamily(t *testing.T) {
ctx := &featGeneratedContext{
lockData: map[string]int{
"feat:greater_skill_focus_appraise": 1316,
"feat:epic_skill_focus_appraise": 588,
"feat:epic_skill_focus_animal_empathy": 587,
},
supplementalID: map[string]int{},
tlkStateKeys: map[string]struct{}{},
familySpecs: map[string]familyExpansionSpec{
"greater_skill_focus": {
FamilyKey: "greater_skill_focus",
LegacyFamilyKeys: []string{"epic_skill_focus"},
},
},
}
spec := familyExpansionSpec{FamilyKey: "greater_skill_focus"}
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentity(spec, "appraise", map[string]any{})
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentity: %v", err)
}
if got != "feat:greater_skill_focus_appraise" || !hasID || rowID != 588 {
t.Fatalf("expected underscore greater skill focus to take legacy alias id 588, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
got, rowID, hasID, err = ctx.resolveGeneratedFeatIdentity(spec, "animal_handling", map[string]any{})
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentity renamed skill: %v", err)
}
if got != "feat:greater_skill_focus_animal_handling" || !hasID || rowID != 587 {
t.Fatalf("expected renamed underscore greater skill focus to take legacy alias id 587, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
}
func TestResolveGeneratedFeatIdentityTranslatesLegacyWeaponFamilySourceIDWithUnderscoreKey(t *testing.T) {
ctx := &featGeneratedContext{
lockData: map[string]int{
"feat:epic_weapon_focus_club": 619,
"feat:epic_weapon_specialization_club": 650,
"feat:epic_overwhelming_critical_club": 900,
},
supplementalID: map[string]int{},
tlkStateKeys: map[string]struct{}{},
existingFeat: map[string]struct{}{
"feat:epic_weapon_focus_club": {},
"feat:epic_weapon_specialization_club": {},
"feat:epic_overwhelming_critical_club": {},
},
familySpecs: map[string]familyExpansionSpec{
"greater_weapon_focus": {
FamilyKey: "greater_weapon_focus",
LegacyFamilyKeys: []string{"epic_weapon_focus"},
},
"greater_weapon_specialization": {
FamilyKey: "greater_weapon_specialization",
LegacyFamilyKeys: []string{"epic_weapon_specialization"},
},
"overwhelming_critical": {
FamilyKey: "overwhelming_critical",
LegacyFamilyKeys: []string{"epic_overwhelming_critical"},
},
},
}
cases := []struct {
family string
want string
rowID int
}{
{family: "greater_weapon_focus", want: "feat:greater_weapon_focus_club", rowID: 619},
{family: "greater_weapon_specialization", want: "feat:greater_weapon_specialization_club", rowID: 650},
{family: "overwhelming_critical", want: "feat:overwhelming_critical_club", rowID: 900},
}
for _, tc := range cases {
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentityBySource(tc.family, "club", fmt.Sprintf("%d", tc.rowID))
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentityBySource(%s): %v", tc.family, err)
}
if got != tc.want || !hasID || rowID != tc.rowID {
t.Fatalf("expected %s at id %d, got key=%q id=%d hasID=%v", tc.want, tc.rowID, got, rowID, hasID)
}
}
}
func TestResolveGeneratedFeatIdentityUsesConfiguredLegacyFamilyKeys(t *testing.T) {
ctx := &featGeneratedContext{
lockData: map[string]int{
"feat:old_weapon_training_test_club": 4000,
},
supplementalID: map[string]int{},
tlkStateKeys: map[string]struct{}{},
familySpecs: map[string]familyExpansionSpec{
"new_weapon_training": {
FamilyKey: "new_weapon_training",
LegacyFamilyKeys: []string{"old_weapon_training"},
},
},
}
spec := familyExpansionSpec{FamilyKey: "new_weapon_training"}
got, rowID, hasID, err := ctx.resolveGeneratedFeatIdentity(spec, "test_club", map[string]any{})
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentity: %v", err)
}
if got != "feat:new_weapon_training_test_club" || !hasID || rowID != 4000 {
t.Fatalf("expected configured legacy family to donate id 4000, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
if !generatedAliasLockForKey(
"feat:new_weapon_training_test_club",
"feat:old_weapon_training_test_club",
generatedFamilyAliases{"new_weapon_training": []string{"old_weapon_training"}},
) {
t.Fatal("expected configured legacy family lock to be movable")
}
got, rowID, hasID, err = ctx.resolveGeneratedFeatIdentity(familyExpansionSpec{FamilyKey: "newweapontraining"}, "test_club", map[string]any{})
if err != nil {
t.Fatalf("resolveGeneratedFeatIdentity compact family key: %v", err)
}
if got != "feat:newweapontraining_test_club" || !hasID || rowID != 4000 {
t.Fatalf("expected normalized configured legacy lookup to donate id 4000, got key=%q id=%d hasID=%v", got, rowID, hasID)
}
}
func TestBuildFamilyExpansionMovesCanonicalLockToLegacyAliasID(t *testing.T) {
root := testProjectRoot(t)
writeFeatGeneratedHarness(t, root, map[string]string{
@@ -2616,6 +2742,7 @@ func TestBuildFamilyExpansionMovesCanonicalLockToLegacyAliasID(t *testing.T) {
"name_prefix": "Greater Skill Focus",
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
"legacy_family_keys": ["epicskillfocus"],
"child_ref_field": "REQSKILL",
"child_source": {"dataset":"skills","predicate":"accessible"},
"overrides": {"skills:concentration":{"ICON":"ife_greater_concentration"}}
@@ -2650,6 +2777,150 @@ func TestBuildFamilyExpansionMovesCanonicalLockToLegacyAliasID(t *testing.T) {
}
}
func TestBuildFamilyExpansionMovesUnderscoreCanonicalLockToUnderscoreLegacyAliasID(t *testing.T) {
root := testProjectRoot(t)
writeFeatGeneratedHarness(t, root, map[string]string{
"greater_skill_focus.json": `{
"family": "greater_skill_focus",
"family_key": "greater_skill_focus",
"template": "masterfeats:greaterskillfocus",
"name_prefix": "Greater Skill Focus",
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
"legacy_family_keys": ["epic_skill_focus"],
"child_ref_field": "REQSKILL",
"child_source": {"dataset":"skills","predicate":"accessible"},
"overrides": {"skills:concentration":{"ICON":"ife_greater_concentration"}}
}` + "\n",
}, map[string]int{
"feat:greater_skill_focus_concentration": 1317,
"feat:epic_skill_focus_concentration": 589,
})
result, err := BuildNative(testProject(root), nil)
if err != nil {
t.Fatalf("BuildNative: %v", err)
}
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "feat.2da"))
if err != nil {
t.Fatalf("read feat.2da: %v", err)
}
text := string(got)
if !strings.Contains(text, "589\tFEAT_GREATER_SKILL_FOCUS_CONCENTRATION") {
t.Fatalf("expected underscore greater skill focus to move to legacy alias id 589, got:\n%s", text)
}
lockRaw, err := os.ReadFile(filepath.Join(root, "topdata", "data", "feat", "lock.json"))
if err != nil {
t.Fatalf("read feat lock: %v", err)
}
lockText := string(lockRaw)
if !strings.Contains(lockText, `"feat:greater_skill_focus_concentration": 589`) {
t.Fatalf("expected canonical underscore key to own legacy row id 589, got:\n%s", lockText)
}
if strings.Contains(lockText, "epic_skill_focus_concentration") {
t.Fatalf("expected stale underscore epic skill focus lock to be pruned, got:\n%s", lockText)
}
}
func TestBuildFamilyExpansionRegeneratesLegacyAliasIDWithoutFeatLock(t *testing.T) {
root := testProjectRoot(t)
writeFeatGeneratedHarness(t, root, map[string]string{
"greater_skill_focus.json": `{
"family": "greater_skill_focus",
"family_key": "greater_skill_focus",
"template": "masterfeats:greaterskillfocus",
"name_prefix": "Greater Skill Focus",
"label_prefix": "FEAT_GREATER_SKILL_FOCUS",
"constant_prefix": "FEAT_GREATER_SKILL_FOCUS",
"legacy_family_keys": ["epic_skill_focus"],
"child_ref_field": "REQSKILL",
"child_source": {"dataset":"skills","predicate":"accessible"},
"overrides": {"skills:concentration":{"ICON":"ife_greater_concentration"}}
}` + "\n",
}, map[string]int{})
writeFile(t, filepath.Join(root, "topdata", "data", "feat", "base.json"), `{
"output": "feat.2da",
"compare_reference": false,
"columns": [
"LABEL","FEAT","DESCRIPTION","ICON","MINATTACKBONUS","MINSTR","MINDEX","MININT","MINWIS","MINCON","MINCHA",
"MINSPELLLVL","PREREQFEAT1","PREREQFEAT2","GAINMULTIPLE","EFFECTSSTACK","ALLCLASSESCANUSE","CATEGORY","MAXCR",
"SPELLID","SUCCESSOR","CRValue","USESPERDAY","MASTERFEAT","TARGETSELF","OrReqFeat0","OrReqFeat1","OrReqFeat2",
"OrReqFeat3","OrReqFeat4","REQSKILL","ReqSkillMinRanks","REQSKILL2","ReqSkillMinRanks2","Constant","TOOLSCATEGORIES",
"HostileFeat","MinLevel","MinLevelClass","MaxLevel","MinFortSave","PreReqEpic","ReqAction"
],
"rows": [
{
"id": 589,
"key": "feat:epic_skill_focus_concentration",
"LABEL": "FEAT_EPIC_SKILL_FOCUS_CONCENTRATION",
"REQSKILL": 1,
"MASTERFEAT": 5,
"Constant": "FEAT_EPIC_SKILL_FOCUS_CONCENTRATION"
}
]
}`+"\n")
result, err := BuildNative(testProject(root), nil)
if err != nil {
t.Fatalf("BuildNative: %v", err)
}
got, err := os.ReadFile(filepath.Join(result.Output2DADir, "feat.2da"))
if err != nil {
t.Fatalf("read feat.2da: %v", err)
}
text := string(got)
if !strings.Contains(text, "589\tFEAT_GREATER_SKILL_FOCUS_CONCENTRATION") {
t.Fatalf("expected empty lockfile rebuild to preserve legacy row id 589, got:\n%s", text)
}
lockRaw, err := os.ReadFile(filepath.Join(root, "topdata", "data", "feat", "lock.json"))
if err != nil {
t.Fatalf("read feat lock: %v", err)
}
lockText := string(lockRaw)
if !strings.Contains(lockText, `"feat:greater_skill_focus_concentration": 589`) {
t.Fatalf("expected regenerated lock to assign greater key to row 589, got:\n%s", lockText)
}
if strings.Contains(lockText, "epic_skill_focus_concentration") {
t.Fatalf("expected regenerated lock to prune legacy epic key, got:\n%s", lockText)
}
}
func TestParseFamilyExpansionRejectsInvalidLegacyFamilyKeys(t *testing.T) {
base := map[string]any{
"family": "greater_skill_focus",
"family_key": "greater_skill_focus",
"template": "masterfeats:greaterskillfocus",
"child_source": map[string]any{"dataset": "skills", "predicate": "accessible"},
}
cases := []struct {
name string
keys []any
errMsg string
}{
{
name: "self alias",
keys: []any{"greater_skill_focus"},
errMsg: "legacy_family_keys must not include family_key",
},
{
name: "normalized duplicate",
keys: []any{"epicskillfocus", "epic_skill_focus"},
errMsg: "legacy_family_keys contains duplicate-equivalent keys",
},
}
for _, tc := range cases {
obj := map[string]any{}
for key, value := range base {
obj[key] = value
}
obj["legacy_family_keys"] = tc.keys
_, err := parseFamilyExpansionSpec(tc.name+".json", obj)
if err == nil || !strings.Contains(err.Error(), tc.errMsg) {
t.Fatalf("%s: expected %q error, got %v", tc.name, tc.errMsg, err)
}
}
}
func TestPruneRetiredGeneratedFeatTLKEntries(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "feat", "generated"))