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:
2026-05-21 21:03:23 +02:00
committed by archvillainette
parent 59ec5cb41d
commit 74c5ea34e1
5 changed files with 396 additions and 8 deletions
@@ -68,6 +68,10 @@ Compact family-expansion shape:
"dataset": "skills", "dataset": "skills",
"predicate": "accessible" "predicate": "accessible"
}, },
"title_style": {
"child_case": "lower",
"child_parenthetical": "comma"
},
"overrides": { "overrides": {
"skills:craft_alchemy": { "skills:craft_alchemy": {
"ICON": "ife_foc_alchm" "ICON": "ife_foc_alchm"
@@ -103,6 +107,10 @@ Weapon family shape:
"dataset": "baseitems", "dataset": "baseitems",
"column": "WeaponFocusFeat" "column": "WeaponFocusFeat"
}, },
"title_style": {
"child_case": "lower",
"child_parenthetical": "preserve"
},
"overrides": { "overrides": {
"baseitems:heavymace": { "baseitems:heavymace": {
"ICON": "ife_wepfoc_Lma" "ICON": "ife_wepfoc_Lma"
@@ -118,7 +126,7 @@ Rules:
- `family` is descriptive only; the builder does not hardcode family names - `family` is descriptive only; the builder does not hardcode family names
- family behavior is driven by authored fields such as `template_fields`, - family behavior is driven by authored fields such as `template_fields`,
`default_fields`, `apply_after_modules`, `identity_source`, `child_ref_field`, `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 - `default_fields` is a family-wide shared layer; it applies to existing and newly
generated child rows before per-child overrides are merged generated child rows before per-child overrides are merged
- `apply_after_modules: true` makes the generated family the final authoritative shared - `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` - 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.column` is used when expansion is gated by a non-null source field
- `child_source.predicate: "accessible"` currently means `HideFromLevelUp != 1` - `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 - skill focus families intentionally do not set `allow_existing_only`, so any new
accessible skill automatically receives matching focus rows accessible skill automatically receives matching focus rows
- compact `overrides` are keyed by source dataset key: - compact `overrides` are keyed by source dataset key:
+44 -1
View File
@@ -16,6 +16,11 @@ type familyExpansionSource struct {
Predicate string Predicate string
} }
type familyExpansionTitleStyle struct {
ChildCase string
ChildParenthetical string
}
type familyExpansionSpec struct { type familyExpansionSpec struct {
Family string Family string
FamilyKey string FamilyKey string
@@ -32,6 +37,7 @@ type familyExpansionSpec struct {
AllowExistingOnly bool AllowExistingOnly bool
AutoPrereqFields map[string]string AutoPrereqFields map[string]string
LegacyFamilyKeys []string LegacyFamilyKeys []string
TitleStyle familyExpansionTitleStyle
} }
func splitFamilyExpansionIdentity(text string) familyIdentity { 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 { if err := validateLegacyFamilyKeys(familyKey, legacyFamilyKeys); err != nil {
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err) 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") allowExistingOnly, err := parseOptionalBoolField(obj["allow_existing_only"], "allow_existing_only")
if err != nil { if err != nil {
return familyExpansionSpec{}, fmt.Errorf("generated file %s: %w", path, err) 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, AllowExistingOnly: allowExistingOnly,
AutoPrereqFields: autoPrereqFields, AutoPrereqFields: autoPrereqFields,
LegacyFamilyKeys: legacyFamilyKeys, LegacyFamilyKeys: legacyFamilyKeys,
TitleStyle: titleStyle,
}, nil }, 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 { func validateLegacyFamilyKeys(familyKey string, legacyFamilyKeys []string) error {
seen := map[string]string{} seen := map[string]string{}
normalizedFamilyKey := normalizeKeyIdentity(familyKey) normalizedFamilyKey := normalizeKeyIdentity(familyKey)
@@ -176,9 +218,10 @@ func isFamilyExpansionObject(obj map[string]any) bool {
_, hasAllowExistingOnly := obj["allow_existing_only"] _, hasAllowExistingOnly := obj["allow_existing_only"]
_, hasAutoPrereqFields := obj["auto_prereq_fields"] _, hasAutoPrereqFields := obj["auto_prereq_fields"]
_, hasLegacyFamilyKeys := obj["legacy_family_keys"] _, hasLegacyFamilyKeys := obj["legacy_family_keys"]
_, hasTitleStyle := obj["title_style"]
return hasFamilyKey || hasTemplate || hasChildSource || hasNamePrefix || hasLabelPrefix || return hasFamilyKey || hasTemplate || hasChildSource || hasNamePrefix || hasLabelPrefix ||
hasConstantPrefix || hasTemplateFields || hasDefaultFields || hasApplyAfterModules || hasChildRefField || hasConstantPrefix || hasTemplateFields || hasDefaultFields || hasApplyAfterModules || hasChildRefField ||
hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields || hasLegacyFamilyKeys hasIdentitySource || hasAllowExistingOnly || hasAutoPrereqFields || hasLegacyFamilyKeys || hasTitleStyle
} }
func optionalTrimmedString(obj map[string]any, field string) (string, bool) { func optionalTrimmedString(obj map[string]any, field string) (string, bool) {
+71 -6
View File
@@ -1355,6 +1355,7 @@ type featGeneratedContext struct {
dataDir string dataDir string
lockData map[string]int lockData map[string]int
supplementalID map[string]int supplementalID map[string]int
baseDialog *legacyTLKData
tlkStateKeys map[string]struct{} tlkStateKeys map[string]struct{}
existingFeat map[string]struct{} existingFeat map[string]struct{}
datasets map[string]nativeDataset datasets map[string]nativeDataset
@@ -1384,6 +1385,10 @@ func newFeatGeneratedContext(dataset nativeDataset, lockData map[string]int) (*f
if err != nil { if err != nil {
return nil, err 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)) tlkState, err := loadTLKState(filepath.Join(sourceDir, tlkStateFile))
if err != nil { if err != nil {
return nil, err return nil, err
@@ -1415,6 +1420,7 @@ func newFeatGeneratedContext(dataset nativeDataset, lockData map[string]int) (*f
dataDir: dataDir, dataDir: dataDir,
lockData: lockCopy, lockData: lockCopy,
supplementalID: supplementalID, supplementalID: supplementalID,
baseDialog: baseDialog,
tlkStateKeys: tlkStateKeys, tlkStateKeys: tlkStateKeys,
existingFeat: existingFeat, existingFeat: existingFeat,
datasets: datasetMap, 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) return nil, fmt.Errorf("generated feat file %s: %w", path, err)
} }
childToken := childTokenFromExpandedKey(featKey, spec.FamilyKey) childToken := childTokenFromExpandedKey(featKey, spec.FamilyKey)
displayName := displayNameForGeneratedSource(spec.ChildSource.Dataset, row) displayName := ctx.displayNameForGeneratedSource(spec.ChildSource.Dataset, row)
if spec.IdentitySource == "child_source_value" && childToken != "" && normalizeKeyIdentity(childToken) != normalizeKeyIdentity(slug) { titleChildName := applyFamilyExpansionTitleStyle(displayName, spec.TitleStyle)
displayName = displayNameFromSlug(childToken)
}
labelSuffix := upperSnake(displayName) labelSuffix := upperSnake(displayName)
override := map[string]any{ override := map[string]any{
"key": featKey, "key": featKey,
@@ -1902,7 +1906,7 @@ func buildFamilyExpansionGeneratedModule(path string, obj map[string]any, ctx *f
override["FEAT"] = map[string]any{ override["FEAT"] = map[string]any{
"tlk": map[string]any{ "tlk": map[string]any{
"key": featKey + ".name", "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" 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 { switch dataset {
case "skills": case "skills":
return displayNameForSkill(row) 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 { func displayNameForRaceFile(obj map[string]any) string {
core, ok := obj["core"].(map[string]any) core, ok := obj["core"].(map[string]any)
if ok { if ok {
+178
View File
@@ -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) { func TestValidateBaseitemsWeaponFeatColumnCompletenessRejectsPartialCoreFamilies(t *testing.T) {
ctx := &featGeneratedContext{ ctx := &featGeneratedContext{
rowsByDataset: map[string]map[string]map[string]any{ rowsByDataset: map[string]map[string]map[string]any{
+84
View File
@@ -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) { func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t *testing.T) {
root := t.TempDir() root := t.TempDir()
sourceDir := filepath.Join(root, "pages") sourceDir := filepath.Join(root, "pages")