Wiki title prefix policy

This commit is contained in:
2026-05-16 23:58:46 +02:00
parent d34a640efe
commit faf1459dba
6 changed files with 329 additions and 92 deletions
+8
View File
@@ -215,6 +215,7 @@ topdata:
managed_namespaces: [classes, feat, itemtypes, races, skills, spells, meta]
deploy_manifest: .wiki_deploy_manifest.json
deploy_edit_summary: Auto-generated from native builder
title_prefix_min_length: 3
autogen:
cache:
@@ -332,6 +333,13 @@ instead. Updates and stale-page archives acquire a Westgate Wiki edit lock
before saving, then send the returned `wikiEditLockToken` with the NodeBB post
edit request.
For newly created NodeBB topics, `topdata.wiki.title_prefix_min_length`
controls when the deployer prefixes the namespace in the topic title. The
default is `3`, so one- and two-character page titles are created as
`Namespace: Title`, while titles with three or more characters keep the page
title alone. Existing bot-managed pages with older prefixed titles are renamed
on later deploy passes when their current title no longer matches this policy.
`build-changelog` renders a release changelog from tagged pushes. Pull-request
style commits keep their PR links and authors from the Gitea API; direct pushes
are rendered as commit links using the commit author. It defaults to
+67 -57
View File
@@ -13,51 +13,52 @@ import (
)
const (
DefaultBuildPath = "build"
DefaultCachePath = ".cache"
DefaultToolsPath = "tools"
DefaultModuleArchiveTemplate = "{module.resref}.mod"
DefaultHAKManifest = "haks.json"
DefaultHAKArchiveTemplate = "{hak.name}.hak"
DefaultSourceJSONPattern = "{resref}.{extension}.json"
DefaultValidationProfile = "nwn_module"
DefaultScriptsSourceDir = "scripts"
DefaultScriptsCache = "{paths.cache}/ncs"
DefaultScriptCompilerEnvPath = "SOW_NWN_SCRIPT_COMPILER"
DefaultMusicStageRoot = "{paths.cache}/music"
DefaultCreditsRoot = "{paths.cache}/credits"
DefaultCreditsOverlayFile = "CREDITS.md"
DefaultMusicNamingScheme = "nwn_bmu"
DefaultMusicOutputExtension = ".bmu"
DefaultTopDataBuild = "{paths.build}/topdata"
DefaultTopDataCompiled2DADir = "2da"
DefaultTopDataCompiledTLK = "sow_tlk.tlk"
DefaultTopDataWikiOutputRoot = "wiki"
DefaultTopDataWikiPagesDir = "pages"
DefaultTopDataWikiStateFile = "state.json"
DefaultTopDataWikiSource = "topdata/wiki"
DefaultTopDataWikiRenderer = "nodebb_tiptap_html"
DefaultTopDataWikiLinkStrategy = "preserve_westgate_wiki_links"
DefaultTopDataWikiNamespacesFile = "namespaces.yaml"
DefaultTopDataWikiTemplatesDir = "templates"
DefaultTopDataWikiManualSectionsDir = "manual-sections"
DefaultTopDataWikiStaleDefault = "report"
DefaultTopDataWikiStaleLiveCleanup = "archive"
DefaultTopDataWikiMarkerFormat = "html_comments"
DefaultTopDataWikiPageMarkerPrefix = "sow-topdata-wiki"
DefaultWikiDeployManifest = ".wiki_deploy_manifest.json"
DefaultWikiDeployEditSummary = "Auto-generated from native builder"
DefaultTopDataPackageHAK = "sow_top.hak"
DefaultTopDataPackageTLK = "sow_tlk.tlk"
DefaultAutogenCacheRoot = "{paths.cache}"
DefaultAutogenCacheMaxAge = time.Hour
DefaultAutogenRefreshEnv = "SOW_AUTOGEN_MANIFEST_REFRESH"
DefaultPartsManifestRefreshEnv = "SOW_PARTS_MANIFEST_REFRESH"
DefaultAutogenReleaseProvider = "gitea"
DefaultAutogenServerURLEnv = "SOW_ASSETS_SERVER_URL"
DefaultAutogenRepoEnv = "SOW_ASSETS_REPO"
DefaultExtractLayout = "nwn_canonical_json"
DefaultExtractHAKDiscovery = "build_glob"
DefaultBuildPath = "build"
DefaultCachePath = ".cache"
DefaultToolsPath = "tools"
DefaultModuleArchiveTemplate = "{module.resref}.mod"
DefaultHAKManifest = "haks.json"
DefaultHAKArchiveTemplate = "{hak.name}.hak"
DefaultSourceJSONPattern = "{resref}.{extension}.json"
DefaultValidationProfile = "nwn_module"
DefaultScriptsSourceDir = "scripts"
DefaultScriptsCache = "{paths.cache}/ncs"
DefaultScriptCompilerEnvPath = "SOW_NWN_SCRIPT_COMPILER"
DefaultMusicStageRoot = "{paths.cache}/music"
DefaultCreditsRoot = "{paths.cache}/credits"
DefaultCreditsOverlayFile = "CREDITS.md"
DefaultMusicNamingScheme = "nwn_bmu"
DefaultMusicOutputExtension = ".bmu"
DefaultTopDataBuild = "{paths.build}/topdata"
DefaultTopDataCompiled2DADir = "2da"
DefaultTopDataCompiledTLK = "sow_tlk.tlk"
DefaultTopDataWikiOutputRoot = "wiki"
DefaultTopDataWikiPagesDir = "pages"
DefaultTopDataWikiStateFile = "state.json"
DefaultTopDataWikiSource = "topdata/wiki"
DefaultTopDataWikiRenderer = "nodebb_tiptap_html"
DefaultTopDataWikiLinkStrategy = "preserve_westgate_wiki_links"
DefaultTopDataWikiNamespacesFile = "namespaces.yaml"
DefaultTopDataWikiTemplatesDir = "templates"
DefaultTopDataWikiManualSectionsDir = "manual-sections"
DefaultTopDataWikiStaleDefault = "report"
DefaultTopDataWikiStaleLiveCleanup = "archive"
DefaultTopDataWikiMarkerFormat = "html_comments"
DefaultTopDataWikiPageMarkerPrefix = "sow-topdata-wiki"
DefaultTopDataWikiTitlePrefixMinLength = 3
DefaultWikiDeployManifest = ".wiki_deploy_manifest.json"
DefaultWikiDeployEditSummary = "Auto-generated from native builder"
DefaultTopDataPackageHAK = "sow_top.hak"
DefaultTopDataPackageTLK = "sow_tlk.tlk"
DefaultAutogenCacheRoot = "{paths.cache}"
DefaultAutogenCacheMaxAge = time.Hour
DefaultAutogenRefreshEnv = "SOW_AUTOGEN_MANIFEST_REFRESH"
DefaultPartsManifestRefreshEnv = "SOW_PARTS_MANIFEST_REFRESH"
DefaultAutogenReleaseProvider = "gitea"
DefaultAutogenServerURLEnv = "SOW_ASSETS_SERVER_URL"
DefaultAutogenRepoEnv = "SOW_ASSETS_REPO"
DefaultExtractLayout = "nwn_canonical_json"
DefaultExtractHAKDiscovery = "build_glob"
)
type ConfigProvenance map[string]ConfigValueProvenance
@@ -232,18 +233,19 @@ func (p *Project) EffectiveConfig() EffectiveConfig {
PackageHAK: defaultString(p.Config.TopData.PackageHAK, DefaultTopDataPackageHAK),
PackageTLK: defaultString(p.Config.TopData.PackageTLK, DefaultTopDataPackageTLK),
Wiki: TopDataWikiConfig{
OutputRoot: defaultString(p.Config.TopData.Wiki.OutputRoot, DefaultTopDataWikiOutputRoot),
PagesDir: defaultString(p.Config.TopData.Wiki.PagesDir, DefaultTopDataWikiPagesDir),
StateFile: defaultString(p.Config.TopData.Wiki.StateFile, DefaultTopDataWikiStateFile),
ManagedNamespaces: defaultStringSlice(p.Config.TopData.Wiki.ManagedNamespaces, []string{"classes", "feat", "itemtypes", "races", "skills", "spells", "meta"}),
DeployManifest: defaultString(p.Config.TopData.Wiki.DeployManifest, DefaultWikiDeployManifest),
DeployEditSummary: defaultString(p.Config.TopData.Wiki.DeployEditSummary, DefaultWikiDeployEditSummary),
Source: defaultString(p.Config.TopData.Wiki.Source, DefaultTopDataWikiSource),
Renderer: defaultString(p.Config.TopData.Wiki.Renderer, DefaultTopDataWikiRenderer),
LinkStrategy: defaultString(p.Config.TopData.Wiki.LinkStrategy, DefaultTopDataWikiLinkStrategy),
NamespacesFile: defaultString(p.Config.TopData.Wiki.NamespacesFile, DefaultTopDataWikiNamespacesFile),
TemplatesDir: defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir),
ManualSectionsDir: defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir),
OutputRoot: defaultString(p.Config.TopData.Wiki.OutputRoot, DefaultTopDataWikiOutputRoot),
PagesDir: defaultString(p.Config.TopData.Wiki.PagesDir, DefaultTopDataWikiPagesDir),
StateFile: defaultString(p.Config.TopData.Wiki.StateFile, DefaultTopDataWikiStateFile),
ManagedNamespaces: defaultStringSlice(p.Config.TopData.Wiki.ManagedNamespaces, []string{"classes", "feat", "itemtypes", "races", "skills", "spells", "meta"}),
DeployManifest: defaultString(p.Config.TopData.Wiki.DeployManifest, DefaultWikiDeployManifest),
DeployEditSummary: defaultString(p.Config.TopData.Wiki.DeployEditSummary, DefaultWikiDeployEditSummary),
Source: defaultString(p.Config.TopData.Wiki.Source, DefaultTopDataWikiSource),
Renderer: defaultString(p.Config.TopData.Wiki.Renderer, DefaultTopDataWikiRenderer),
LinkStrategy: defaultString(p.Config.TopData.Wiki.LinkStrategy, DefaultTopDataWikiLinkStrategy),
NamespacesFile: defaultString(p.Config.TopData.Wiki.NamespacesFile, DefaultTopDataWikiNamespacesFile),
TemplatesDir: defaultString(p.Config.TopData.Wiki.TemplatesDir, DefaultTopDataWikiTemplatesDir),
ManualSectionsDir: defaultString(p.Config.TopData.Wiki.ManualSectionsDir, DefaultTopDataWikiManualSectionsDir),
TitlePrefixMinLength: defaultInt(p.Config.TopData.Wiki.TitlePrefixMinLength, DefaultTopDataWikiTitlePrefixMinLength),
StalePages: TopDataWikiStalePagesConfig{
Default: defaultString(p.Config.TopData.Wiki.StalePages.Default, DefaultTopDataWikiStaleDefault),
LiveCleanup: defaultString(p.Config.TopData.Wiki.StalePages.LiveCleanup, DefaultTopDataWikiStaleLiveCleanup),
@@ -471,6 +473,7 @@ func markMissingDefaults(provenance ConfigProvenance) {
"topdata.wiki.namespaces_file": DefaultTopDataWikiNamespacesFile,
"topdata.wiki.templates_dir": DefaultTopDataWikiTemplatesDir,
"topdata.wiki.manual_sections_dir": DefaultTopDataWikiManualSectionsDir,
"topdata.wiki.title_prefix_min_length": "3",
"topdata.wiki.stale_pages.default": DefaultTopDataWikiStaleDefault,
"topdata.wiki.stale_pages.live_cleanup": DefaultTopDataWikiStaleLiveCleanup,
"topdata.wiki.managed_region.marker_format": DefaultTopDataWikiMarkerFormat,
@@ -511,6 +514,13 @@ func defaultString(value, fallback string) string {
return trimmed
}
func defaultInt(value, fallback int) int {
if value == 0 {
return fallback
}
return value
}
func defaultStringSlice(value, fallback []string) []string {
if len(value) == 0 {
return slices.Clone(fallback)
+18 -14
View File
@@ -204,20 +204,21 @@ type TopDataConfig struct {
}
type TopDataWikiConfig struct {
OutputRoot string `json:"output_root" yaml:"output_root"`
PagesDir string `json:"pages_dir" yaml:"pages_dir"`
StateFile string `json:"state_file" yaml:"state_file"`
ManagedNamespaces []string `json:"managed_namespaces" yaml:"managed_namespaces"`
DeployManifest string `json:"deploy_manifest" yaml:"deploy_manifest"`
DeployEditSummary string `json:"deploy_edit_summary" yaml:"deploy_edit_summary"`
Source string `json:"source" yaml:"source"`
Renderer string `json:"renderer" yaml:"renderer"`
LinkStrategy string `json:"link_strategy" yaml:"link_strategy"`
NamespacesFile string `json:"namespaces_file" yaml:"namespaces_file"`
TemplatesDir string `json:"templates_dir" yaml:"templates_dir"`
ManualSectionsDir string `json:"manual_sections_dir" yaml:"manual_sections_dir"`
StalePages TopDataWikiStalePagesConfig `json:"stale_pages" yaml:"stale_pages"`
ManagedRegion TopDataWikiManagedRegionConfig `json:"managed_region" yaml:"managed_region"`
OutputRoot string `json:"output_root" yaml:"output_root"`
PagesDir string `json:"pages_dir" yaml:"pages_dir"`
StateFile string `json:"state_file" yaml:"state_file"`
ManagedNamespaces []string `json:"managed_namespaces" yaml:"managed_namespaces"`
DeployManifest string `json:"deploy_manifest" yaml:"deploy_manifest"`
DeployEditSummary string `json:"deploy_edit_summary" yaml:"deploy_edit_summary"`
Source string `json:"source" yaml:"source"`
Renderer string `json:"renderer" yaml:"renderer"`
LinkStrategy string `json:"link_strategy" yaml:"link_strategy"`
NamespacesFile string `json:"namespaces_file" yaml:"namespaces_file"`
TemplatesDir string `json:"templates_dir" yaml:"templates_dir"`
ManualSectionsDir string `json:"manual_sections_dir" yaml:"manual_sections_dir"`
TitlePrefixMinLength int `json:"title_prefix_min_length" yaml:"title_prefix_min_length"`
StalePages TopDataWikiStalePagesConfig `json:"stale_pages" yaml:"stale_pages"`
ManagedRegion TopDataWikiManagedRegionConfig `json:"managed_region" yaml:"managed_region"`
}
type TopDataWikiStalePagesConfig struct {
@@ -530,6 +531,9 @@ func (p *Project) ValidateLayout() error {
default:
failures = append(failures, fmt.Errorf("topdata.wiki.link_strategy %q is not supported", effective.TopData.Wiki.LinkStrategy))
}
if effective.TopData.Wiki.TitlePrefixMinLength < 1 {
failures = append(failures, fmt.Errorf("topdata.wiki.title_prefix_min_length must be at least 1"))
}
for key, policy := range map[string]string{
"topdata.wiki.stale_pages.default": effective.TopData.Wiki.StalePages.Default,
"topdata.wiki.stale_pages.live_cleanup": effective.TopData.Wiki.StalePages.LiveCleanup,
+4
View File
@@ -266,6 +266,7 @@ topdata:
manual_sections_dir: manual-sections
deploy_manifest: wiki-manifest.json
deploy_edit_summary: Custom summary
title_prefix_min_length: 4
stale_pages:
default: report
live_cleanup: archive
@@ -330,6 +331,9 @@ autogen:
if got, want := effective.TopData.Wiki.StalePages.LiveCleanup, "archive"; got != want {
t.Fatalf("expected wiki stale live cleanup %q, got %q", want, got)
}
if got, want := effective.TopData.Wiki.TitlePrefixMinLength, 4; got != want {
t.Fatalf("expected wiki title prefix minimum length %d, got %d", want, got)
}
if got, want := effective.Autogen.Cache.MaxAge, "30m"; got != want {
t.Fatalf("expected autogen cache max age %q, got %q", want, got)
}
+96 -18
View File
@@ -33,20 +33,21 @@ const (
)
type DeployWikiOptions struct {
SourceDir string
Endpoint string
Token string
Version string
Namespaces []string
CategoryIDs map[string]int
ManifestPath string
DryRun bool
Force bool
AllowCreates bool
StalePolicy string
Username string
Password string
NotesDelimiter string
SourceDir string
Endpoint string
Token string
Version string
Namespaces []string
CategoryIDs map[string]int
ManifestPath string
DryRun bool
Force bool
AllowCreates bool
StalePolicy string
Username string
Password string
NotesDelimiter string
TitlePrefixMinLength int
}
type deployResult struct {
@@ -57,6 +58,7 @@ type deployResult struct {
Archived int
Skipped int
Drifted int
Renamed int
Manifest string
}
@@ -78,6 +80,7 @@ type wikiDeployManifestPage struct {
LastSeenHash string `json:"last_seen_hash,omitempty"`
ArchivedHash string `json:"archived_hash,omitempty"`
Title string `json:"title,omitempty"`
TopicTitle string `json:"topic_title,omitempty"`
Namespace string `json:"namespace,omitempty"`
Stale bool `json:"stale,omitempty"`
TID int `json:"tid,omitempty"`
@@ -91,6 +94,7 @@ type wikiDeployPlan struct {
Action string
Content string
RemoteHash string
Title string
}
type wikiNamespacesDocument struct {
@@ -128,6 +132,9 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
if opts.StalePolicy != "" && opts.StalePolicy != "report" && opts.StalePolicy != "archive" {
return deployResult{}, fmt.Errorf("wiki stale policy %q is not supported", opts.StalePolicy)
}
if opts.TitlePrefixMinLength <= 0 {
opts.TitlePrefixMinLength = p.EffectiveConfig().TopData.Wiki.TitlePrefixMinLength
}
namespaces := opts.Namespaces
if len(namespaces) == 0 {
@@ -173,7 +180,7 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
result.LocalPages = len(pages)
result.Manifest = manifestPath
progress(fmt.Sprintf("NodeBB wiki plan: create %d, update %d, skip %d, stale %d, archive %d, drift %d", result.Created, result.Updated, result.Skipped, result.Stale, result.Archived, result.Drifted))
progress(fmt.Sprintf("NodeBB wiki plan: create %d, update %d, rename %d, skip %d, stale %d, archive %d, drift %d", result.Created, result.Updated, result.Renamed, result.Skipped, result.Stale, result.Archived, result.Drifted))
if opts.DryRun {
return result, nil
}
@@ -215,6 +222,10 @@ func DeployWikiWithOptions(p *project.Project, opts DeployWikiOptions, progress
if err := client.updatePost(plan.Entry.TID, plan.Entry.PID, plan.Content, summary); err != nil {
return result, fmt.Errorf("deploy wiki page %q: archive NodeBB post %d: %w", plan.Page.PageID, plan.Entry.PID, err)
}
case "rename":
if err := client.renameWikiPage(plan.Entry.TID, plan.Entry.CID, plan.Title); err != nil {
return result, fmt.Errorf("deploy wiki page %q: rename NodeBB topic %d to %q: %w", plan.Page.PageID, plan.Entry.TID, plan.Title, err)
}
}
}
if err := saveDeployManifest(manifestPath, nextManifest); err != nil {
@@ -346,6 +357,7 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
entry.Hash = page.Hash
entry.LastSeenHash = page.Hash
entry.Title = page.Title
entry.TopicTitle = nodeBBTopicTitle(page, opts.TitlePrefixMinLength)
entry.Namespace = page.Namespace
entry.Stale = false
if entry.CID == 0 {
@@ -376,11 +388,22 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
return nil, result, next, fmt.Errorf("wiki page %q requires --category %s=<cid> for creation", pageID, page.Namespace)
}
result.Created++
page.Title = nodeBBTopicTitle(page)
page.Title = entry.TopicTitle
plans = append(plans, wikiDeployPlan{Page: page, Entry: entry, Action: "create", Content: page.Content})
continue
}
if entry.TID != 0 && entry.CID != 0 && shouldCheckRemoteTopicTitle(manifest.Pages[pageID], page.Title, entry.TopicTitle, opts.TitlePrefixMinLength) {
remotePage, ok, err := findMappedRemotePage(entry, remotePagesByCID, client)
if err != nil {
return nil, result, next, err
}
if ok && titleNeedsRename(remotePageTitle(remotePage), entry.TopicTitle) {
result.Renamed++
plans = append(plans, wikiDeployPlan{Page: page, Entry: entry, Action: "rename", Title: entry.TopicTitle})
}
}
if manifest.Pages[pageID].Hash == page.Hash {
result.Skipped++
continue
@@ -442,6 +465,49 @@ func planNodeBBDeploy(pages map[string]wikiDeployPage, manifest wikiDeployManife
return plans, result, next, nil
}
func findMappedRemotePage(entry wikiDeployManifestPage, remotePagesByCID map[int][]nodeBBWikiPage, client *nodeBBClient) (nodeBBWikiPage, bool, error) {
remotePages, ok := remotePagesByCID[entry.CID]
if !ok {
var err error
remotePages, err = client.listNamespacePages(entry.CID)
if err != nil {
return nodeBBWikiPage{}, false, err
}
remotePagesByCID[entry.CID] = remotePages
}
for _, remotePage := range remotePages {
if remotePage.TID == entry.TID {
return remotePage, true, nil
}
}
return nodeBBWikiPage{}, false, nil
}
func remotePageTitle(page nodeBBWikiPage) string {
if title := strings.TrimSpace(page.Title); title != "" {
return title
}
return strings.TrimSpace(page.TitleLeaf)
}
func titleNeedsRename(current, desired string) bool {
current = strings.TrimSpace(current)
desired = strings.TrimSpace(desired)
return current != "" && desired != "" && current != desired
}
func shouldCheckRemoteTopicTitle(previous wikiDeployManifestPage, pageTitle, desiredTopicTitle string, minLength int) bool {
if strings.TrimSpace(previous.TopicTitle) != "" {
return strings.TrimSpace(previous.TopicTitle) != strings.TrimSpace(desiredTopicTitle)
}
titleLen := len([]rune(strings.TrimSpace(pageTitle)))
if titleLen == 0 {
return false
}
const legacyPrefixMinLength = 5
return titleLen < legacyPrefixMinLength || titleLen < minLength
}
func recoverCreateCollision(plan wikiDeployPlan, manifest wikiDeployManifest, client *nodeBBClient, summary string) (bool, error) {
entry, ok, err := findExistingNodeBBPage(plan.Page, plan.Entry.CID, client)
if err != nil || !ok {
@@ -461,6 +527,7 @@ func recoverCreateCollision(plan wikiDeployPlan, manifest wikiDeployManifest, cl
entry.Hash = plan.Entry.Hash
entry.LastSeenHash = plan.Entry.LastSeenHash
entry.Title = plan.Entry.Title
entry.TopicTitle = plan.Entry.TopicTitle
entry.Namespace = plan.Entry.Namespace
entry.Stale = false
manifest.Pages[plan.Page.PageID] = entry
@@ -831,9 +898,12 @@ func extractHTMLTitle(content, fallback string) string {
return namespaceTitle(parts[len(parts)-1])
}
func nodeBBTopicTitle(page wikiDeployPage) string {
func nodeBBTopicTitle(page wikiDeployPage, minLength int) string {
if minLength <= 0 {
minLength = project.DefaultTopDataWikiTitlePrefixMinLength
}
title := strings.TrimSpace(page.Title)
if len([]rune(title)) >= 5 {
if len([]rune(title)) >= minLength {
return title
}
namespace := strings.TrimSpace(namespaceTitle(page.Namespace))
@@ -1007,6 +1077,14 @@ func (c *nodeBBClient) updatePost(tid, pid int, content, summary string) error {
return c.request("PUT", fmt.Sprintf("/api/v3/posts/%d", pid), body, nil)
}
func (c *nodeBBClient) renameWikiPage(tid, cid int, title string) error {
if tid == 0 || cid == 0 {
return fmt.Errorf("NodeBB wiki page rename requires topic id and category id")
}
body := map[string]any{"tid": tid, "cid": cid, "title": title}
return c.request("PUT", "/api/v3/plugins/westgate-wiki/page/move", body, nil)
}
func (c *nodeBBClient) acquireEditLock(tid int) (nodeBBEditLock, error) {
if tid == 0 {
return nodeBBEditLock{}, fmt.Errorf("NodeBB wiki post update requires a topic id for edit locking")
+136 -3
View File
@@ -147,7 +147,7 @@ func TestDeployWikiCreatesNodeBBTopicAndWritesManifest(t *testing.T) {
}
}
func TestDeployWikiCreatesNodeBBTopicWithFallbackForShortTitle(t *testing.T) {
func TestDeployWikiCreatesNodeBBTopicWithoutFallbackForDefaultThreeCharacterTitle(t *testing.T) {
root := t.TempDir()
sourceDir := filepath.Join(root, "pages")
if err := os.MkdirAll(filepath.Join(sourceDir, "spells"), 0755); err != nil {
@@ -174,8 +174,8 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForShortTitle(t *testing.T) {
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("decode create request: %v", err)
}
if req.Title != "Spells: Aid" {
t.Fatalf("expected fallback title, got %q", req.Title)
if req.Title != "Aid" {
t.Fatalf("expected unprefixed title, got %q", req.Title)
}
if req.Content != generated {
t.Fatalf("expected page body to keep short h1 unchanged:\n%s", req.Content)
@@ -206,6 +206,139 @@ func TestDeployWikiCreatesNodeBBTopicWithFallbackForShortTitle(t *testing.T) {
}
}
func TestDeployWikiCreatesNodeBBTopicWithFallbackForTitleShorterThanConfiguredMinimum(t *testing.T) {
root := t.TempDir()
sourceDir := filepath.Join(root, "pages")
if err := os.MkdirAll(filepath.Join(sourceDir, "spells"), 0755); err != nil {
t.Fatalf("create source dir: %v", err)
}
generated := "[//]: # (sow-topdata-wiki:managed:start)\n# Ox\n\nGenerated spell page\n[//]: # (sow-topdata-wiki:managed:end)\n"
if err := os.WriteFile(filepath.Join(sourceDir, "spells", "ox.md"), []byte(generated), 0644); err != nil {
t.Fatalf("write source page: %v", err)
}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if 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": []any{}, "hasMore": false}})
return
}
if r.Method != http.MethodPost || r.URL.Path != "/api/v3/topics" {
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path)
}
var req struct {
Title string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
t.Fatalf("decode create request: %v", err)
}
if req.Title != "Spells: Ox" {
t.Fatalf("expected fallback title, got %q", req.Title)
}
w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{
"response": map[string]any{
"tid": 11,
"mainPost": map[string]any{
"pid": 42,
"tid": 11,
},
},
})
}))
defer server.Close()
_, err := DeployWikiWithOptions(&project.Project{}, DeployWikiOptions{
SourceDir: sourceDir,
Endpoint: server.URL,
Token: "nodebb-token",
ManifestPath: filepath.Join(root, "deploy-manifest.json"),
CategoryIDs: map[string]int{"spells": 5},
AllowCreates: true,
}, nil)
if err != nil {
t.Fatalf("DeployWikiWithOptions create failed: %v", err)
}
}
func TestDeployWikiRenamesExistingPrefixedTopicWhenTitleIsLongEnough(t *testing.T) {
root := t.TempDir()
sourceDir := filepath.Join(root, "pages")
if err := os.MkdirAll(filepath.Join(sourceDir, "itemtypes"), 0755); err != nil {
t.Fatalf("create source dir: %v", err)
}
generated := `<!-- sow-topdata-wiki:page=itemtypes:belt -->
<!-- sow-topdata-wiki:managed:start hash="sha256:local" -->
<h1>Belt</h1>
<p>Generated belt page</p>
<!-- sow-topdata-wiki:managed:end -->
`
if err := os.WriteFile(filepath.Join(sourceDir, "itemtypes", "belt.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{
"itemtypes:belt": {Hash: computeManagedHash(generated), Title: "Belt", Namespace: "itemtypes", 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": "Itemtypes: Belt", "titleLeaf": "Itemtypes: Belt", "slug": "7/itemtypes-belt", "wikiPath": "/wiki/itemtypes/itemtypes-belt"},
},
"hasMore": false,
},
})
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 != "Belt" {
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, "cid": 5, "title": "Belt"}})
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 rename cleanup failed: %v", err)
}
if result.Renamed != 1 || result.Updated != 0 || result.Skipped != 1 || renameCalls != 1 {
t.Fatalf("expected one rename cleanup and skipped body update, result=%#v renameCalls=%d", result, renameCalls)
}
manifest := loadDeployManifest(manifestPath)
entry := manifest.Pages["itemtypes:belt"]
if entry.TopicTitle != "Belt" || entry.Title != "Belt" {
t.Fatalf("expected manifest to record cleaned topic title, got %#v", entry)
}
}
func TestDeployWikiAdoptsExistingNodeBBPageWhenManifestIsMissingWithoutCreate(t *testing.T) {
root := t.TempDir()
sourceDir := filepath.Join(root, "pages")