Configuration Hardening

Key changes:

  - Added internal/project/effective.go with normalized effective config, defaults, provenance, and
    JSON output.
  - Added config subcommands:
      - config validate
      - config effective [--json|--yaml]
      - config inspect [<key>]
      - config explain <key>
      - config sources
  - Added YAML schema fields for configurable outputs, cache roots, script cache, inventory extensions,
    topdata output/wiki paths, and autogen cache root.
  - Routed existing hardcoded paths through effective config wrappers for module archives, HAK
    manifests/archives, script cache, music cache/credits, topdata outputs, and autogen caches.
  - Added validation for unsafe generated output/cache paths.
  - Updated command descriptions to avoid fixed build/, .cache, sow_top, etc.
  - Added regression tests for defaults, provenance, deterministic effective config, YAML-controlled
    derivation, config commands, and invalid paths.
This commit is contained in:
2026-05-07 14:09:20 +02:00
parent 000d31ad24
commit 60d2de9f2d
12 changed files with 2481 additions and 62 deletions
+155
View File
@@ -1,6 +1,7 @@
package project
import (
"bytes"
"errors"
"os"
"path/filepath"
@@ -43,6 +44,128 @@ haks:
}
}
func TestEffectiveConfigAppliesVisibleToolkitDefaults(t *testing.T) {
root := t.TempDir()
writeProjectFile(t, filepath.Join(root, ConfigFile), `
module:
name: Test Module
resref: testmod
paths:
source: src
`)
proj, err := Load(root)
if err != nil {
t.Fatalf("Load returned error: %v", err)
}
effective := proj.EffectiveConfig()
if got, want := effective.Paths.Build, "build"; got != want {
t.Fatalf("expected default build path %q, got %q", want, got)
}
if got, want := effective.Outputs.HAKManifest, "haks.json"; got != want {
t.Fatalf("expected default HAK manifest %q, got %q", want, got)
}
if got, want := effective.TopData.PackageHAK, "sow_top.hak"; got != want {
t.Fatalf("expected default topdata HAK %q, got %q", want, got)
}
if prov := effective.Provenance["paths.build"]; prov.Source != "toolkit default" {
t.Fatalf("expected paths.build toolkit default provenance, got %#v", prov)
}
if prov := effective.Provenance["module.name"]; prov.Source != "yaml" {
t.Fatalf("expected module.name YAML provenance, got %#v", prov)
}
}
func TestEffectiveConfigHonorsConfiguredOutputAndCacheFields(t *testing.T) {
root := t.TempDir()
writeProjectFile(t, filepath.Join(root, ConfigFile), `
module:
name: Test Module
resref: testmod
paths:
build: output
cache: cache
outputs:
module_archive: modules/{module.resref}.mod
hak_manifest: manifests/haks.json
hak_archive: haks/{hak.name}.hak
scripts:
cache: "{paths.cache}/compiled"
topdata:
source: topdata
build: "{paths.cache}"
compiled_2da_dir: tables
compiled_tlk: dialog.tlk
package_hak: custom_top.hak
package_tlk: custom_dialog.tlk
wiki:
output_root: docs
pages_dir: pages
state_file: wiki-state.json
autogen:
cache:
root: "{paths.cache}/autogen"
`)
proj, err := Load(root)
if err != nil {
t.Fatalf("Load returned error: %v", err)
}
if got, want := proj.ModuleArchivePath(), filepath.Join(root, "output", "modules", "testmod.mod"); got != want {
t.Fatalf("expected module archive path %s, got %s", want, got)
}
if got, want := proj.HAKManifestPath(), filepath.Join(root, "output", "manifests", "haks.json"); got != want {
t.Fatalf("expected HAK manifest path %s, got %s", want, got)
}
if got, want := proj.HAKArchivePath("core_01"), filepath.Join(root, "output", "haks", "core_01.hak"); got != want {
t.Fatalf("expected HAK archive path %s, got %s", want, got)
}
if got, want := proj.ScriptCacheDir(), filepath.Join(root, "cache", "compiled"); got != want {
t.Fatalf("expected script cache path %s, got %s", want, got)
}
if got, want := proj.TopDataCompiled2DADir(), filepath.Join(root, "cache", "tables"); got != want {
t.Fatalf("expected topdata 2da path %s, got %s", want, got)
}
if got, want := proj.TopDataWikiStatePath(), filepath.Join(root, "cache", "docs", "wiki-state.json"); got != want {
t.Fatalf("expected wiki state path %s, got %s", want, got)
}
if got, want := proj.AutogenCachePath("manifest.json"), filepath.Join(root, "cache", "autogen", "manifest.json"); got != want {
t.Fatalf("expected autogen cache path %s, got %s", want, got)
}
}
func TestEffectiveConfigJSONIsDeterministic(t *testing.T) {
root := t.TempDir()
writeProjectFile(t, filepath.Join(root, ConfigFile), `
module:
name: Test Module
resref: testmod
paths:
source: src
`)
proj, err := Load(root)
if err != nil {
t.Fatalf("Load returned error: %v", err)
}
first, err := proj.EffectiveConfigJSON()
if err != nil {
t.Fatalf("EffectiveConfigJSON returned error: %v", err)
}
second, err := proj.EffectiveConfigJSON()
if err != nil {
t.Fatalf("EffectiveConfigJSON returned error: %v", err)
}
if !bytes.Equal(first, second) {
t.Fatalf("effective config JSON is not deterministic")
}
if !bytes.Contains(first, []byte(`"hak_manifest": "haks.json"`)) {
t.Fatalf("effective config JSON missing HAK manifest default: %s", first)
}
}
func TestLoadYMLConfig(t *testing.T) {
root := t.TempDir()
writeProjectFile(t, filepath.Join(root, ConfigFileYML), `
@@ -346,6 +469,38 @@ func TestValidateLayoutRejectsInvalidTopDataPackageOutputNames(t *testing.T) {
}
}
func TestValidateLayoutRejectsUnsafeGeneratedOutputPaths(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src"))
mkdirAll(t, filepath.Join(root, "build"))
proj := &Project{
Root: root,
Config: Config{
Module: ModuleConfig{Name: "Test", ResRef: "test"},
Paths: PathConfig{Source: "src", Build: "build"},
Outputs: OutputConfig{
HAKManifest: "../haks.json",
},
TopData: TopDataConfig{
Source: "topdata",
Compiled2DADir: "../2da",
},
},
}
err := proj.ValidateLayout()
if err == nil {
t.Fatal("expected unsafe output path validation error")
}
if !strings.Contains(err.Error(), "outputs.hak_manifest") {
t.Fatalf("expected HAK manifest path validation error, got %v", err)
}
if !strings.Contains(err.Error(), "topdata.compiled_2da_dir") {
t.Fatalf("expected topdata path validation error, got %v", err)
}
}
func TestValidateLayoutRejectsInvalidAutogenConsumerConfig(t *testing.T) {
root := t.TempDir()
mkdirAll(t, filepath.Join(root, "src"))