Align topdata wiki canonical paths (#11)

Reviewed-on: https://gitea.westgate.pw/ShadowsOverWestgate/sow-tools/pulls/11
Co-authored-by: Michał Piasecki <mpiasecki720@protonmail.com>
Co-committed-by: Michał Piasecki <mpiasecki720@protonmail.com>
This commit is contained in:
2026-05-23 16:38:02 +02:00
committed by archvillainette
parent 7288d7e2b7
commit 6d6367ad18
6 changed files with 754 additions and 76 deletions
+162 -1
View File
@@ -180,6 +180,65 @@ func TestBuildWikiRegeneratesMissingCachedPages(t *testing.T) {
}
}
func TestBuildNativeIndexesUnregisteredStatusPagesWithDeclaredNamespacePath(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
mkdirAll(t, filepath.Join(root, "topdata", "wiki"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "wiki", "namespaces.yaml"), `
namespaces:
- id: meta
title: Wiki Status
canonical_path: Wiki/Status
category_env: SOW_WIKI_META_CID
edit_policy: generated_only
`)
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "base.json"), `{
"output": "skills.2da",
"columns": ["Label", "Name", "Description", "HideFromLevelUp", "Untrained", "KeyAbility", "ArmorCheckPenalty", "Constant"],
"rows": [
{
"id": 0,
"key": "skills:athletics",
"Label": "Athletics",
"Name": {"tlk": {"key": "skills:athletics.name", "text": "Athletics"}},
"Description": {"tlk": {"key": "skills:athletics.description", "text": "Ability: Strength."}},
"HideFromLevelUp": "0",
"Untrained": "1",
"KeyAbility": "STR",
"ArmorCheckPenalty": "1",
"Constant": "SKILL_ATHLETICS"
}
]
}`+"\n")
writeFile(t, filepath.Join(root, "topdata", "data", "skills", "lock.json"), `{"skills:athletics":0}`+"\n")
proj := testProject(root)
proj.Config.TopData.ReferenceBuilder = ""
if _, err := BuildNative(proj, nil); err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
indexRaw, err := os.ReadFile(filepath.Join(root, ".cache", "wiki", "page-index.json"))
if err != nil {
t.Fatalf("read page index: %v", err)
}
var pageIndex wikiPageIndex
if err := json.Unmarshal(indexRaw, &pageIndex); err != nil {
t.Fatalf("parse page index: %v", err)
}
paths := map[string]string{}
for _, page := range pageIndex.Pages {
paths[page.PageID] = page.PublicPath
}
if got, want := paths["meta:wikistatus"], "Wiki/Status/Wiki_Status"; got != want {
t.Fatalf("expected declared canonical path for status page, got %q want %q", got, want)
}
if got := paths["meta:wikistatus:skills:vanilla"]; !strings.HasPrefix(got, "Wiki/Status/") {
t.Fatalf("expected declared canonical path for status listing, got %q", got)
}
}
func TestBuildNativeCanOmitAggregateWikiStatusListings(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "skills"))
@@ -1367,6 +1426,29 @@ func TestSaveWikiPageIndexRejectsDuplicatePublicPaths(t *testing.T) {
}
}
func TestSaveWikiPageIndexRejectsFoldedDuplicatePublicPaths(t *testing.T) {
err := saveWikiPageIndex(filepath.Join(t.TempDir(), "page-index.json"), wikiPageIndex{
Version: wikiGeneratorVersion,
Pages: []wikiPageIndexEntry{
{PageID: "spells:fear", PublicPath: "Magic/Fear", OutputPath: "pages/spells/fear.html"},
{PageID: "spells:accented_fear", PublicPath: "magic/fear", OutputPath: "pages/spells/accented_fear.html"},
},
})
if err == nil || !strings.Contains(err.Error(), "duplicate wiki page-index public_path") {
t.Fatalf("expected folded duplicate public_path validation error, got %v", err)
}
}
func TestPublicWikiPathSplitsTitlePathWithoutWhitespaceAroundSeparator(t *testing.T) {
ctx := &wikiContext{namespaceTitles: map[string]string{"spells": "Magic"}}
if got := ctx.publicWikiPath("spells", "Parent::Child"); got != "Magic/Parent/Child" {
t.Fatalf("expected NodeBB-style title path split, got %q", got)
}
if got := wikiPageOutputRelPath("spells:parent_child", "Parent::Child"); got != filepath.FromSlash("spells/Parent/Child.html") {
t.Fatalf("expected title-based output path split, got %q", got)
}
}
func TestParseWikiPagePathDeclarationsRejectsLegacySlugOverrides(t *testing.T) {
_, err := parseWikiPagePathDeclarations([]byte(`
page_paths:
@@ -1401,16 +1483,95 @@ func TestCanonicalWikiSegmentPreservesCaseAndUnderscoreSpaces(t *testing.T) {
}
}
func TestCanonicalWikiSegmentFoldedKeyMatchesPluginFixture(t *testing.T) {
tests := []struct {
source string
canonical string
folded string
}{
{source: "Inspire Competence", canonical: "Inspire_Competence", folded: "inspire competence"},
{source: "Grandmaster's Battle Momentum", canonical: "Grandmasters_Battle_Momentum", folded: "grandmasters battle momentum"},
{source: "Æther Œuvre Øresund Straße Þorn Łódź Đelta", canonical: "Aether_Oeuvre_Oresund_Strasse_Thorn_Lodz_Delta", folded: "aether oeuvre oresund strasse thorn lodz delta"},
}
for _, tt := range tests {
if got := canonicalWikiSegment(tt.source); got != tt.canonical {
t.Fatalf("canonicalWikiSegment(%q) = %q, want %q", tt.source, got, tt.canonical)
}
if got := canonicalWikiSegmentFoldedKey(tt.source); got != tt.folded {
t.Fatalf("canonicalWikiSegmentFoldedKey(%q) = %q, want %q", tt.source, got, tt.folded)
}
}
}
func TestWikiStatusPagesEmitCanonicalLinks(t *testing.T) {
ctx := &wikiContext{}
report := renderWikiStatusReportPage(map[string]map[string]any{
"skills": {
"status": wikiStatusVanilla,
"new": 0,
"modified": 0,
"vanilla": 1,
"total": 1,
},
}, []string{"skills"}, ctx)
if !strings.Contains(report, "[[Skills|Skills]]") || strings.Contains(report, "[[skills:index|Skills]]") {
t.Fatalf("expected status report namespace link to use canonical public path, got:\n%s", report)
}
listing := renderWikiStatusListingPage("Skills Vanilla Pages", []string{"skills:inspire_competence", "feat:grandmaster_battle_momentum"}, map[string]string{
"skills:inspire_competence": "Inspire Competence",
"feat:grandmaster_battle_momentum": "Grandmaster's Battle Momentum",
}, ctx)
for _, want := range []string{
"[[Skills/Inspire_Competence|Inspire Competence]]",
"[[Feats/Grandmasters_Battle_Momentum|Grandmaster's Battle Momentum]]",
} {
if !strings.Contains(listing, want) {
t.Fatalf("expected status listing canonical link %q, got:\n%s", want, listing)
}
}
if strings.Contains(listing, "[[skills:inspire_competence|") || strings.Contains(listing, "[[feat:grandmaster_battle_momentum|") {
t.Fatalf("expected status listing to omit generated page IDs as public targets, got:\n%s", listing)
}
customCtx := &wikiContext{
namespaceTitles: map[string]string{
"spells": "Magic",
},
}
customReport := renderWikiStatusReportPage(map[string]map[string]any{
"spells": {
"status": wikiStatusNew,
"new": 1,
"modified": 0,
"vanilla": 0,
"total": 1,
},
}, []string{"spells"}, customCtx)
if !strings.Contains(customReport, "[[Magic|Magic]]") || strings.Contains(customReport, "[[Spells|Spells]]") {
t.Fatalf("expected custom namespace status report link to use declared canonical title, got:\n%s", customReport)
}
customListing := renderWikiStatusListingPage("Magic New Pages", []string{"spells:fear"}, map[string]string{
"spells:fear": "Fear",
}, customCtx)
if !strings.Contains(customListing, "[[Magic/Fear|Fear]]") || strings.Contains(customListing, "[[Spells/Fear|Fear]]") {
t.Fatalf("expected custom namespace status listing link to use declared canonical title, got:\n%s", customListing)
}
}
func TestPublicWikiTargetUsesCanonicalNamespaceAndTitlePath(t *testing.T) {
ctx := &wikiContext{
namespaceTitles: map[string]string{
"feat": "Feats",
},
namespacePaths: map[string]string{
"feat": "Wiki/Feats",
},
rowsByKey: map[string]map[string]any{
"feat:hardiness_versus_enchantments": {"FEAT": map[string]any{"tlk": map[string]any{"text": "Hardiness vs. Enchantments"}}},
},
}
if got, want := ctx.publicWikiTargetForKey("feat:hardiness_versus_enchantments", "Display Alias"), "Feats/Hardiness_vs_Enchantments"; got != want {
if got, want := ctx.publicWikiTargetForKey("feat:hardiness_versus_enchantments", "Display Alias"), "Wiki/Feats/Hardiness_vs_Enchantments"; got != want {
t.Fatalf("expected canonical title path %q, got %q", want, got)
}
}