Normalize feat names (#8)
Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/8 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
This commit is contained in:
@@ -68,6 +68,10 @@ Compact family-expansion shape:
|
||||
"dataset": "skills",
|
||||
"predicate": "accessible"
|
||||
},
|
||||
"title_style": {
|
||||
"child_case": "lower",
|
||||
"child_parenthetical": "comma"
|
||||
},
|
||||
"overrides": {
|
||||
"skills:craft_alchemy": {
|
||||
"ICON": "ife_foc_alchm"
|
||||
@@ -103,6 +107,10 @@ Weapon family shape:
|
||||
"dataset": "baseitems",
|
||||
"column": "WeaponFocusFeat"
|
||||
},
|
||||
"title_style": {
|
||||
"child_case": "lower",
|
||||
"child_parenthetical": "preserve"
|
||||
},
|
||||
"overrides": {
|
||||
"baseitems:heavymace": {
|
||||
"ICON": "ife_wepfoc_Lma"
|
||||
@@ -118,7 +126,7 @@ Rules:
|
||||
- `family` is descriptive only; the builder does not hardcode family names
|
||||
- family behavior is driven by authored fields such as `template_fields`,
|
||||
`default_fields`, `apply_after_modules`, `identity_source`, `child_ref_field`,
|
||||
and `auto_prereq_fields`
|
||||
`auto_prereq_fields`, and optional `title_style`
|
||||
- `default_fields` is a family-wide shared layer; it applies to existing and newly
|
||||
generated child rows before per-child overrides are merged
|
||||
- `apply_after_modules: true` makes the generated family the final authoritative shared
|
||||
@@ -131,6 +139,16 @@ Rules:
|
||||
- family-expansion files must declare `template`, `family_key`, and `child_source.dataset`
|
||||
- `child_source.column` is used when expansion is gated by a non-null source field
|
||||
- `child_source.predicate: "accessible"` currently means `HideFromLevelUp != 1`
|
||||
- `title_style` only changes generated child display text while composing titles;
|
||||
generated feat keys, TLK keys, row IDs, family metadata, and source references
|
||||
remain tied to generated family identity
|
||||
- `title_style.child_case` defaults to `preserve`; `lower` lowercases the resolved
|
||||
child display text after parenthetical formatting
|
||||
- `title_style.child_parenthetical` defaults to `preserve`; `comma` flattens one
|
||||
child parenthetical suffix such as `Knowledge (local)` into
|
||||
`Knowledge, local` before the child text is wrapped by the family title
|
||||
- unsupported `title_style` values fail generated-family validation instead of
|
||||
silently changing title behavior
|
||||
- skill focus families intentionally do not set `allow_existing_only`, so any new
|
||||
accessible skill automatically receives matching focus rows
|
||||
- compact `overrides` are keyed by source dataset key:
|
||||
|
||||
@@ -16,6 +16,11 @@ type familyExpansionSource struct {
|
||||
Predicate string
|
||||
}
|
||||
|
||||
type familyExpansionTitleStyle struct {
|
||||
ChildCase string
|
||||
ChildParenthetical string
|
||||
}
|
||||
|
||||
type familyExpansionSpec struct {
|
||||
Family string
|
||||
FamilyKey string
|
||||
@@ -32,6 +37,7 @@ type familyExpansionSpec struct {
|
||||
AllowExistingOnly bool
|
||||
AutoPrereqFields map[string]string
|
||||
LegacyFamilyKeys []string
|
||||
TitleStyle familyExpansionTitleStyle
|
||||
}
|
||||
|
||||
func splitFamilyExpansionIdentity(text string) familyIdentity {
|
||||
@@ -122,6 +128,10 @@ func parseFamilyExpansionSpec(path string, obj map[string]any) (familyExpansionS
|
||||
if err := validateLegacyFamilyKeys(familyKey, legacyFamilyKeys); err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
}
|
||||
titleStyle, err := parseFamilyExpansionTitleStyle(obj["title_style"])
|
||||
if err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
}
|
||||
allowExistingOnly, err := parseOptionalBoolField(obj["allow_existing_only"], "allow_existing_only")
|
||||
if err != nil {
|
||||
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err)
|
||||
@@ -142,9 +152,41 @@ func parseFamilyExpansionSpec(path string, obj map[string]any) (familyExpansionS
|
||||
AllowExistingOnly: allowExistingOnly,
|
||||
AutoPrereqFields: autoPrereqFields,
|
||||
LegacyFamilyKeys: legacyFamilyKeys,
|
||||
TitleStyle: titleStyle,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func parseFamilyExpansionTitleStyle(raw any) (familyExpansionTitleStyle, error) {
|
||||
style := familyExpansionTitleStyle{
|
||||
ChildCase: "preserve",
|
||||
ChildParenthetical: "preserve",
|
||||
}
|
||||
if raw == nil {
|
||||
return style, nil
|
||||
}
|
||||
obj, ok := raw.(map[string]any)
|
||||
if !ok {
|
||||
return familyExpansionTitleStyle{}, fmt.Errorf("title_style must be an object")
|
||||
}
|
||||
if childCase, ok := obj["child_case"].(string); ok && strings.TrimSpace(childCase) != "" {
|
||||
style.ChildCase = strings.TrimSpace(childCase)
|
||||
}
|
||||
switch style.ChildCase {
|
||||
case "preserve", "lower":
|
||||
default:
|
||||
return familyExpansionTitleStyle{}, fmt.Errorf("title_style.child_case must be preserve or lower")
|
||||
}
|
||||
if childParenthetical, ok := obj["child_parenthetical"].(string); ok && strings.TrimSpace(childParenthetical) != "" {
|
||||
style.ChildParenthetical = strings.TrimSpace(childParenthetical)
|
||||
}
|
||||
switch style.ChildParenthetical {
|
||||
case "preserve", "comma":
|
||||
default:
|
||||
return familyExpansionTitleStyle{}, fmt.Errorf("title_style.child_parenthetical must be preserve or comma")
|
||||
}
|
||||
return style, nil
|
||||
}
|
||||
|
||||
func validateLegacyFamilyKeys(familyKey string, legacyFamilyKeys []string) error {
|
||||
seen := map[string]string{}
|
||||
normalizedFamilyKey := normalizeKeyIdentity(familyKey)
|
||||
@@ -176,9 +218,10 @@ func isFamilyExpansionObject(obj map[string]any) bool {
|
||||
_, hasAllowExistingOnly := obj["allow_existing_only"]
|
||||
_, hasAutoPrereqFields := obj["auto_prereq_fields"]
|
||||
_, hasLegacyFamilyKeys := obj["legacy_family_keys"]
|
||||
_, hasTitleStyle := obj["title_style"]
|
||||
return hasFamilyKey || hasTemplate || hasChildSource || hasNamePrefix || hasLabelPrefix ||
|
||||
hasConstantPrefix || hasTemplateFields || hasDefaultFields || hasApplyAfterModules || hasChildRefField ||
|
||||
hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields || hasLegacyFamilyKeys
|
||||
hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields || hasLegacyFamilyKeys || hasTitleStyle
|
||||
}
|
||||
|
||||
func optionalTrimmedString(obj map[string]any, field string) (string, bool) {
|
||||
|
||||
@@ -1355,6 +1355,7 @@ type featGeneratedContext struct {
|
||||
dataDir string
|
||||
lockData map[string]int
|
||||
supplementalID map[string]int
|
||||
baseDialog *legacyTLKData
|
||||
tlkStateKeys map[string]struct{}
|
||||
existingFeat map[string]struct{}
|
||||
datasets map[string]nativeDataset
|
||||
@@ -1384,6 +1385,10 @@ func newFeatGeneratedContext(dataset nativeDataset, lockData map[string]int) (*f
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
baseDialog, err := loadBaseDialogData(filepath.Join(sourceDir, "base_dialog.json"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tlkState, err := loadTLKState(filepath.Join(sourceDir, tlkStateFile))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1415,6 +1420,7 @@ func newFeatGeneratedContext(dataset nativeDataset, lockData map[string]int) (*f
|
||||
dataDir: dataDir,
|
||||
lockData: lockCopy,
|
||||
supplementalID: supplementalID,
|
||||
baseDialog: baseDialog,
|
||||
tlkStateKeys: tlkStateKeys,
|
||||
existingFeat: existingFeat,
|
||||
datasets: datasetMap,
|
||||
@@ -1870,10 +1876,8 @@ func buildFamilyExpansionGeneratedModule(path string, obj map[string]any, ctx *f
|
||||
return nil, fmt.Errorf("generated feat file %s: %w", path, err)
|
||||
}
|
||||
childToken := childTokenFromExpandedKey(featKey, spec.FamilyKey)
|
||||
displayName := displayNameForGeneratedSource(spec.ChildSource.Dataset, row)
|
||||
if spec.IdentitySource == "child_source_value" && childToken != "" && normalizeKeyIdentity(childToken) != normalizeKeyIdentity(slug) {
|
||||
displayName = displayNameFromSlug(childToken)
|
||||
}
|
||||
displayName := ctx.displayNameForGeneratedSource(spec.ChildSource.Dataset, row)
|
||||
titleChildName := applyFamilyExpansionTitleStyle(displayName, spec.TitleStyle)
|
||||
labelSuffix := upperSnake(displayName)
|
||||
override := map[string]any{
|
||||
"key": featKey,
|
||||
@@ -1902,7 +1906,7 @@ func buildFamilyExpansionGeneratedModule(path string, obj map[string]any, ctx *f
|
||||
override["FEAT"] = map[string]any{
|
||||
"tlk": map[string]any{
|
||||
"key": featKey + ".name",
|
||||
"text": fmt.Sprintf("%s (%s)", spec.NamePrefix, displayName),
|
||||
"text": fmt.Sprintf("%s (%s)", spec.NamePrefix, titleChildName),
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2160,7 +2164,10 @@ func displayNameForBaseitem(row map[string]any) string {
|
||||
return "Unknown Weapon"
|
||||
}
|
||||
|
||||
func displayNameForGeneratedSource(dataset string, row map[string]any) string {
|
||||
func (c *featGeneratedContext) displayNameForGeneratedSource(dataset string, row map[string]any) string {
|
||||
if text, ok := c.displayTextFromGeneratedValue(row["Name"]); ok {
|
||||
return text
|
||||
}
|
||||
switch dataset {
|
||||
case "skills":
|
||||
return displayNameForSkill(row)
|
||||
@@ -2183,6 +2190,64 @@ func displayNameForGeneratedSource(dataset string, row map[string]any) string {
|
||||
}
|
||||
}
|
||||
|
||||
func (c *featGeneratedContext) displayTextFromGeneratedValue(value any) (string, bool) {
|
||||
if text, ok := displayTextFromValue(value); ok {
|
||||
return text, true
|
||||
}
|
||||
if c == nil || c.baseDialog == nil {
|
||||
return "", false
|
||||
}
|
||||
key := ""
|
||||
switch typed := value.(type) {
|
||||
case string:
|
||||
if !isNumericText(typed) {
|
||||
return "", false
|
||||
}
|
||||
key = strings.TrimSpace(typed)
|
||||
case int:
|
||||
key = strconv.Itoa(typed)
|
||||
case float64:
|
||||
if typed != float64(int(typed)) {
|
||||
return "", false
|
||||
}
|
||||
key = strconv.Itoa(int(typed))
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
entry, ok := c.baseDialog.Entries[key]
|
||||
if !ok || strings.TrimSpace(entry.Text) == "" {
|
||||
return "", false
|
||||
}
|
||||
return normalizeDisplayName(strings.TrimSpace(entry.Text)), true
|
||||
}
|
||||
|
||||
func applyFamilyExpansionTitleStyle(displayName string, style familyExpansionTitleStyle) string {
|
||||
if style.ChildParenthetical == "comma" {
|
||||
displayName = flattenGeneratedTitleChildParenthetical(displayName)
|
||||
}
|
||||
if style.ChildCase == "lower" {
|
||||
displayName = strings.ToLower(displayName)
|
||||
}
|
||||
return displayName
|
||||
}
|
||||
|
||||
func flattenGeneratedTitleChildParenthetical(text string) string {
|
||||
text = strings.TrimSpace(text)
|
||||
if !strings.HasSuffix(text, ")") || strings.Count(text, "(") != 1 || strings.Count(text, ")") != 1 {
|
||||
return text
|
||||
}
|
||||
open := strings.LastIndex(text, "(")
|
||||
if open <= 0 || open >= len(text)-2 {
|
||||
return text
|
||||
}
|
||||
prefix := strings.TrimSpace(text[:open])
|
||||
suffix := strings.TrimSpace(strings.TrimSuffix(text[open+1:], ")"))
|
||||
if prefix == "" || suffix == "" {
|
||||
return text
|
||||
}
|
||||
return prefix + ", " + suffix
|
||||
}
|
||||
|
||||
func displayNameForRaceFile(obj map[string]any) string {
|
||||
core, ok := obj["core"].(map[string]any)
|
||||
if ok {
|
||||
|
||||
@@ -2954,6 +2954,184 @@ func TestParseFamilyExpansionRejectsInvalidLegacyFamilyKeys(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedWeaponFeatTitleStyleUsesSourceDisplayNameWithStableIdentity(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||
"weapon_focus.json": `{
|
||||
"family": "weapon_focus",
|
||||
"family_key": "weapon_focus",
|
||||
"template": "masterfeats:weaponfocus",
|
||||
"name_prefix": "Weapon Focus",
|
||||
"label_prefix": "FEAT_WEAPON_FOCUS",
|
||||
"constant_prefix": "FEAT_WEAPON_FOCUS",
|
||||
"identity_source": "child_source_value",
|
||||
"child_source": {"dataset":"baseitems","column":"WeaponFocusFeat"},
|
||||
"title_style": {"child_case":"lower","child_parenthetical":"preserve"},
|
||||
"overrides": {}
|
||||
}` + "\n",
|
||||
}, map[string]int{"feat:weapon_focus_dwaxe": 3003})
|
||||
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), `{
|
||||
"entries": {
|
||||
"83310": {"text": "Dwarven Waraxe"}
|
||||
}
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "base.json"), `{
|
||||
"output": "baseitems.2da",
|
||||
"columns": ["label", "Name", "WeaponFocusFeat"],
|
||||
"rows": [
|
||||
{"id": 108, "key": "baseitems:dwarvenwaraxe", "label": "dwaxe", "Name": "83310", "WeaponFocusFeat": "3003"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "baseitems", "lock.json"), `{"baseitems:dwarvenwaraxe":108}`+"\n")
|
||||
|
||||
module := buildGeneratedFeatModuleForTest(t, root, "weapon_focus.json")
|
||||
override := generatedFeatOverrideByKey(t, module, "feat:weapon_focus_dwaxe")
|
||||
if text := generatedFeatOverrideTitle(t, override); text != "Weapon Focus (dwarven waraxe)" {
|
||||
t.Fatalf("expected source display title for stable generated feat identity, got %q", text)
|
||||
}
|
||||
if id, ok := override["id"].(int); !ok || id != 3003 {
|
||||
t.Fatalf("expected generated title style to preserve compact row id 3003, got %#v", override["id"])
|
||||
}
|
||||
}
|
||||
|
||||
func TestGeneratedSkillFeatTitleStyleFlattensChildParenthetical(t *testing.T) {
|
||||
root := testProjectRoot(t)
|
||||
writeFeatGeneratedHarness(t, root, map[string]string{
|
||||
"skill_focus.json": `{
|
||||
"family": "skill_focus",
|
||||
"family_key": "skill_focus",
|
||||
"template": "masterfeats:skillfocus",
|
||||
"name_prefix": "Skill Focus",
|
||||
"label_prefix": "FEAT_SKILL_FOCUS",
|
||||
"constant_prefix": "FEAT_SKILL_FOCUS",
|
||||
"child_ref_field": "REQSKILL",
|
||||
"child_source": {"dataset":"skills","predicate":"accessible"},
|
||||
"title_style": {"child_case":"lower","child_parenthetical":"comma"},
|
||||
"overrides": {}
|
||||
}` + "\n",
|
||||
}, map[string]int{})
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
|
||||
"output": "skills.2da",
|
||||
"columns": ["Label", "Name", "HideFromLevelUp"],
|
||||
"rows": [
|
||||
{"id": 10, "key": "skills:knowledge_local", "Label": "KnowledgeLocal", "Name": {"tlk": {"text": "Knowledge (local)"}}, "HideFromLevelUp": "0"}
|
||||
]
|
||||
}`+"\n")
|
||||
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:knowledge_local":10}`+"\n")
|
||||
|
||||
module := buildGeneratedFeatModuleForTest(t, root, "skill_focus.json")
|
||||
override := generatedFeatOverrideByKey(t, module, "feat:skill_focus_knowledge_local")
|
||||
if text := generatedFeatOverrideTitle(t, override); text != "Skill Focus (knowledge, local)" {
|
||||
t.Fatalf("expected flattened generated skill title, got %q", text)
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseFamilyExpansionTitleStyleDefaultsAndValidation(t *testing.T) {
|
||||
base := map[string]any{
|
||||
"family": "skill_focus",
|
||||
"family_key": "skill_focus",
|
||||
"template": "masterfeats:skillfocus",
|
||||
"child_source": map[string]any{"dataset": "skills", "predicate": "accessible"},
|
||||
}
|
||||
spec, err := parseFamilyExpansionSpec("default-title-style.json", base)
|
||||
if err != nil {
|
||||
t.Fatalf("parse default title style: %v", err)
|
||||
}
|
||||
if spec.TitleStyle.ChildCase != "preserve" || spec.TitleStyle.ChildParenthetical != "preserve" {
|
||||
t.Fatalf("expected default title style to preserve legacy behavior, got %#v", spec.TitleStyle)
|
||||
}
|
||||
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
titleStyle map[string]any
|
||||
want string
|
||||
}{
|
||||
{name: "child case", titleStyle: map[string]any{"child_case": "headline"}, want: "title_style.child_case"},
|
||||
{name: "child parenthetical", titleStyle: map[string]any{"child_parenthetical": "nested"}, want: "title_style.child_parenthetical"},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
obj := map[string]any{}
|
||||
for key, value := range base {
|
||||
obj[key] = value
|
||||
}
|
||||
obj["title_style"] = tc.titleStyle
|
||||
_, err := parseFamilyExpansionSpec("invalid-title-style.json", obj)
|
||||
if err == nil || !strings.Contains(err.Error(), tc.want) || !strings.Contains(err.Error(), "invalid-title-style.json") {
|
||||
t.Fatalf("expected %q validation error with file context, got %v", tc.want, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func buildGeneratedFeatModuleForTest(t *testing.T, root, name string) map[string]any {
|
||||
t.Helper()
|
||||
datasets, err := discoverNativeDatasets(filepath.Join(root, "topdata", "data"))
|
||||
if err != nil {
|
||||
t.Fatalf("discover datasets: %v", err)
|
||||
}
|
||||
var feat nativeDataset
|
||||
for _, dataset := range datasets {
|
||||
if dataset.Name == "feat" {
|
||||
feat = dataset
|
||||
break
|
||||
}
|
||||
}
|
||||
if feat.Name == "" {
|
||||
t.Fatal("expected feat dataset")
|
||||
}
|
||||
lock, err := loadLockfile(feat.LockPath)
|
||||
if err != nil {
|
||||
t.Fatalf("load feat lock: %v", err)
|
||||
}
|
||||
ctx, err := newFeatGeneratedContext(feat, lock)
|
||||
if err != nil {
|
||||
t.Fatalf("new generated feat context: %v", err)
|
||||
}
|
||||
path := filepath.Join(feat.GeneratedDir, name)
|
||||
obj, err := loadJSONObject(path)
|
||||
if err != nil {
|
||||
t.Fatalf("load generated family %s: %v", name, err)
|
||||
}
|
||||
module, err := buildFamilyExpansionGeneratedModule(path, obj, ctx)
|
||||
if err != nil {
|
||||
t.Fatalf("build generated family %s: %v", name, err)
|
||||
}
|
||||
return module
|
||||
}
|
||||
|
||||
func generatedFeatOverrideByKey(t *testing.T, module map[string]any, key string) map[string]any {
|
||||
t.Helper()
|
||||
overrides, ok := module["overrides"].([]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected generated overrides, got %#v", module)
|
||||
}
|
||||
for _, raw := range overrides {
|
||||
override, ok := raw.(map[string]any)
|
||||
if ok && override["key"] == key {
|
||||
return override
|
||||
}
|
||||
}
|
||||
t.Fatalf("expected generated override %q, got %#v", key, overrides)
|
||||
return nil
|
||||
}
|
||||
|
||||
func generatedFeatOverrideTitle(t *testing.T, override map[string]any) string {
|
||||
t.Helper()
|
||||
feat, ok := override["FEAT"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected generated FEAT TLK data, got %#v", override)
|
||||
}
|
||||
tlk, ok := feat["tlk"].(map[string]any)
|
||||
if !ok {
|
||||
t.Fatalf("expected generated FEAT tlk block, got %#v", feat)
|
||||
}
|
||||
text, ok := tlk["text"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("expected generated FEAT text, got %#v", tlk)
|
||||
}
|
||||
return text
|
||||
}
|
||||
|
||||
func TestValidateBaseitemsWeaponFeatColumnCompletenessRejectsPartialCoreFamilies(t *testing.T) {
|
||||
ctx := &featGeneratedContext{
|
||||
rowsByDataset: map[string]map[string]map[string]any{
|
||||
|
||||
@@ -474,6 +474,90 @@ func TestDeployWikiRenamesExistingPrefixedTopicWhenTitleIsLongEnough(t *testing.
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployWikiRenamesManagedTopicWhenGeneratedTitleChanges(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
sourceDir := filepath.Join(root, "pages")
|
||||
if err := os.MkdirAll(filepath.Join(sourceDir, "feat"), 0755); err != nil {
|
||||
t.Fatalf("create source dir: %v", err)
|
||||
}
|
||||
generated := `<!-- sow-topdata-wiki:page=feat:weapon:focus:dwaxe -->
|
||||
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
|
||||
<h1>Weapon Focus (dwarven waraxe)</h1>
|
||||
<p>Generated weapon focus page</p>
|
||||
<!-- sow-topdata-wiki:managed:end -->
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(sourceDir, "feat", "weapon:focus:dwaxe.html"), []byte(generated), 0644); err != nil {
|
||||
t.Fatalf("write source page: %v", err)
|
||||
}
|
||||
manifestPath := filepath.Join(root, "deploy-manifest.json")
|
||||
if err := saveDeployManifest(manifestPath, wikiDeployManifest{
|
||||
Version: "nodebb-v1",
|
||||
Pages: map[string]wikiDeployManifestPage{
|
||||
"feat:weapon:focus:dwaxe": {
|
||||
Hash: computeManagedHash(generated),
|
||||
Title: "Weapon Focus (Dwaxe)",
|
||||
TopicTitle: "Weapon Focus (Dwaxe)",
|
||||
Namespace: "feat",
|
||||
TID: 7,
|
||||
PID: 42,
|
||||
CID: 5,
|
||||
},
|
||||
},
|
||||
}); err != nil {
|
||||
t.Fatalf("write deploy manifest: %v", err)
|
||||
}
|
||||
|
||||
renameCalls := 0
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
switch {
|
||||
case r.Method == http.MethodGet && r.URL.Path == "/api/v3/plugins/westgate-wiki/namespace/5/pages":
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||
"response": map[string]any{
|
||||
"pages": []map[string]any{
|
||||
{"tid": 7, "title": "Weapon Focus (Dwaxe)", "titleLeaf": "Weapon Focus (Dwaxe)"},
|
||||
},
|
||||
},
|
||||
})
|
||||
case r.Method == http.MethodPut && r.URL.Path == "/api/v3/plugins/westgate-wiki/page/move":
|
||||
renameCalls++
|
||||
var req struct {
|
||||
TID int `json:"tid"`
|
||||
CID int `json:"cid"`
|
||||
Title string `json:"title"`
|
||||
}
|
||||
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
|
||||
t.Fatalf("decode rename request: %v", err)
|
||||
}
|
||||
if req.TID != 7 || req.CID != 5 || req.Title != "Weapon Focus (dwarven waraxe)" {
|
||||
t.Fatalf("unexpected rename request: %#v", req)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
_ = json.NewEncoder(w).Encode(map[string]any{"response": map[string]any{"tid": 7}})
|
||||
default:
|
||||
t.Fatalf("unexpected request %s %s", r.Method, r.URL.String())
|
||||
}
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
result, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
|
||||
SourceDir: sourceDir,
|
||||
Endpoint: server.URL,
|
||||
Token: "nodebb-token",
|
||||
ManifestPath: manifestPath,
|
||||
}, nil)
|
||||
if err != nil {
|
||||
t.Fatalf("DeployWikiWithOptions title rename failed: %v", err)
|
||||
}
|
||||
if result.Renamed != 1 || result.Updated != 0 || result.Created != 0 || result.Archived != 0 || result.Purged != 0 || renameCalls != 1 {
|
||||
t.Fatalf("expected generated title rename only, result=%#v renameCalls=%d", result, renameCalls)
|
||||
}
|
||||
entry := loadDeployManifest(manifestPath).Pages["feat:weapon:focus:dwaxe"]
|
||||
if entry.Title != "Weapon Focus (dwarven waraxe)" || entry.TopicTitle != "Weapon Focus (dwarven waraxe)" {
|
||||
t.Fatalf("expected renamed title state in manifest, got %#v", entry)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
sourceDir := filepath.Join(root, "pages")
|
||||
|
||||
Reference in New Issue
Block a user