Parts-Based Manifest Publishing

This commit is contained in:
2026-04-09 21:22:26 +02:00
parent 94ffcc5868
commit 8f0b8a5d66
8 changed files with 596 additions and 89 deletions
+224 -8
View File
@@ -2,7 +2,10 @@ package topdata
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"os/exec"
"path/filepath"
"strings"
"testing"
@@ -7966,6 +7969,91 @@ func TestAugmentWithAutogeneratedPartsSortsRows(t *testing.T) {
}
}
func TestAugmentWithAutogeneratedRobePartsDefaultsHideColumns(t *testing.T) {
collected := []nativeCollectedDataset{
{
Dataset: nativeDataset{
Name: "parts/robe",
OutputName: "parts_robe.2da",
},
Columns: []string{
"COSTMODIFIER", "ACBONUS", "HIDEFOOTR", "HIDEFOOTL", "HIDECHEST",
},
Rows: []map[string]any{},
LockData: map[string]int{},
},
}
autogenerated := map[string]map[int]struct{}{
"robe": {95: {}},
}
result := augmentWithAutogeneratedParts(collected, autogenerated)
robeDataset := result[0]
if len(robeDataset.Rows) != 1 {
t.Fatalf("expected 1 autogenerated robe row, got %d", len(robeDataset.Rows))
}
row := robeDataset.Rows[0]
if got := row["id"]; got != 95 {
t.Fatalf("expected autogenerated robe row id 95, got %v", got)
}
if got := row["COSTMODIFIER"]; got != "0" {
t.Fatalf("expected COSTMODIFIER=0, got %v", got)
}
if got := row["ACBONUS"]; got != "0.00" {
t.Fatalf("expected ACBONUS=0.00, got %v", got)
}
for _, column := range []string{"HIDEFOOTR", "HIDEFOOTL", "HIDECHEST"} {
if got := row[column]; got != "0" {
t.Fatalf("expected %s=0, got %v", column, got)
}
}
}
func TestAugmentWithAutogeneratedRobePartsActivatesUnsetHideColumns(t *testing.T) {
collected := []nativeCollectedDataset{
{
Dataset: nativeDataset{
Name: "parts/robe",
OutputName: "parts_robe.2da",
},
Columns: []string{
"COSTMODIFIER", "ACBONUS", "HIDEFOOTR", "HIDEFOOTL", "HIDECHEST",
},
Rows: []map[string]any{
{
"id": 95,
"COSTMODIFIER": "****",
"ACBONUS": "****",
"HIDEFOOTR": "****",
"HIDEFOOTL": "****",
"HIDECHEST": "****",
},
},
LockData: map[string]int{},
},
}
autogenerated := map[string]map[int]struct{}{
"robe": {95: {}},
}
result := augmentWithAutogeneratedParts(collected, autogenerated)
row := result[0].Rows[0]
if got := row["COSTMODIFIER"]; got != "0" {
t.Fatalf("expected COSTMODIFIER=0, got %v", got)
}
if got := row["ACBONUS"]; got != "0.00" {
t.Fatalf("expected ACBONUS=0.00, got %v", got)
}
for _, column := range []string{"HIDEFOOTR", "HIDEFOOTL", "HIDECHEST"} {
if got := row[column]; got != "0" {
t.Fatalf("expected %s=0, got %v", column, got)
}
}
}
func TestBuildNativeWithAutogeneratedParts(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "parts"))
@@ -8007,7 +8095,41 @@ func TestBuildNativeWithAutogeneratedParts(t *testing.T) {
}
}
func TestBuildNativeWithSiblingAssetsRepo(t *testing.T) {
func TestBuildNativeWithAutogeneratedRobeParts(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "parts"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "parts", "robe.json"), `{
"output": "parts_robe.2da",
"columns": ["COSTMODIFIER", "ACBONUS", "HIDEFOOTR", "HIDEFOOTL", "HIDECHEST"],
"rows": [{"id": 0, "COSTMODIFIER": "1", "ACBONUS": "0.25", "HIDEFOOTR": "1", "HIDEFOOTL": "1", "HIDECHEST": "1"}]
}`+"\n")
mkdirAll(t, filepath.Join(root, "reference"))
writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n")
mkdirAll(t, filepath.Join(root, "assets", "part", "robe"))
writeFile(t, filepath.Join(root, "assets", "part", "robe", "pfa0_robe095.mdl"), "")
proj := testProject(root)
proj.Config.TopData.Assets = filepath.Join(root, "assets")
result, err := BuildNative(proj, nil)
if err != nil {
t.Fatalf("BuildNative failed: %v", err)
}
partsBytes, err := os.ReadFile(filepath.Join(result.Output2DADir, "parts_robe.2da"))
if err != nil {
t.Fatalf("read parts_robe.2da: %v", err)
}
partsText := string(partsBytes)
if !strings.Contains(partsText, "95\t0\t0.00\t0\t0\t0") {
t.Fatalf("expected autogenerated robe row 95 with zeroed hide defaults, got:\n%s", partsText)
}
}
func TestBuildNativeWithReleasedPartsManifest(t *testing.T) {
root := t.TempDir()
projRoot := filepath.Join(root, "project")
mkdirAll(t, filepath.Join(projRoot, "src"))
@@ -8023,10 +8145,31 @@ func TestBuildNativeWithSiblingAssetsRepo(t *testing.T) {
mkdirAll(t, filepath.Join(projRoot, "reference"))
writeFile(t, filepath.Join(projRoot, "reference", "build.py"), "print('ok')\n")
siblingAssets := filepath.Join(root, "sow-assets")
mkdirAll(t, filepath.Join(siblingAssets, "assets", "part", "belt"))
writeFile(t, filepath.Join(siblingAssets, "assets", "part", "belt", "pfa0_belt018.mdl"), "")
writeFile(t, filepath.Join(siblingAssets, "assets", "part", "belt", "pfa0_belt171.mdl"), "")
manifestJSON := `{
"repo": "ShadowsOverWestgate/sow-assets",
"ref": "test-manifest",
"generated_at": "2026-04-09T00:00:00Z",
"categories": {
"belt": [18, 171]
}
}` + "\n"
var server *httptest.Server
server = httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/v1/repos/ShadowsOverWestgate/sow-assets/releases/tags/parts-manifest-current":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(`{"assets":[{"name":"sow-parts-manifest.json","browser_download_url":"` + server.URL + `/downloads/sow-parts-manifest.json"}]}`))
case "/downloads/sow-parts-manifest.json":
w.Header().Set("Content-Type", "application/json")
_, _ = w.Write([]byte(manifestJSON))
default:
http.NotFound(w, r)
}
}))
defer server.Close()
runGitTest(t, "", "init", "-b", "main", projRoot)
runGitTest(t, projRoot, "remote", "add", "origin", server.URL+"/ShadowsOverWestgate/sow-module.git")
proj := &project.Project{
Root: projRoot,
@@ -8036,7 +8179,6 @@ func TestBuildNativeWithSiblingAssetsRepo(t *testing.T) {
TopData: project.TopDataConfig{
Source: "topdata",
Build: "build/topdata",
Assets: "sow-assets",
},
},
}
@@ -8056,10 +8198,71 @@ func TestBuildNativeWithSiblingAssetsRepo(t *testing.T) {
t.Errorf("expected existing row 0 to be preserved, got:\n%s", partsText)
}
if !strings.Contains(partsText, "18\t0\t0.00") {
t.Errorf("expected autogenerated row 18 from sibling repo, got:\n%s", partsText)
t.Errorf("expected autogenerated row 18 from released manifest, got:\n%s", partsText)
}
if !strings.Contains(partsText, "171\t0\t0.00") {
t.Errorf("expected autogenerated row 171 from sibling repo, got:\n%s", partsText)
t.Errorf("expected autogenerated row 171 from released manifest, got:\n%s", partsText)
}
}
func TestBuildNativeRejectsGitRepoTopDataAssetsOverride(t *testing.T) {
root := testProjectRoot(t)
mkdirAll(t, filepath.Join(root, "topdata", "data", "parts"))
writeFile(t, filepath.Join(root, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(root, "topdata", "data", "parts", "belt.json"), `{
"output": "parts_belt.2da",
"columns": ["COSTMODIFIER", "ACBONUS"],
"rows": []
}`+"\n")
mkdirAll(t, filepath.Join(root, "reference"))
writeFile(t, filepath.Join(root, "reference", "build.py"), "print('ok')\n")
proj := testProject(root)
proj.Config.TopData.Assets = "ssh://git@gitea.westgate.pw:2222/ShadowsOverWestgate/sow-assets.git"
_, err := BuildNative(proj, nil)
if err == nil || !strings.Contains(err.Error(), "no longer supports git repo specs") {
t.Fatalf("expected git repo topdata.assets override to be rejected, got %v", err)
}
}
func TestBuildNativeFailsWhenReleasedPartsManifestUnavailable(t *testing.T) {
root := t.TempDir()
projRoot := filepath.Join(root, "project")
mkdirAll(t, filepath.Join(projRoot, "src"))
mkdirAll(t, filepath.Join(projRoot, "assets"))
mkdirAll(t, filepath.Join(projRoot, "build"))
mkdirAll(t, filepath.Join(projRoot, "topdata", "data", "parts"))
writeFile(t, filepath.Join(projRoot, "topdata", "base_dialog.json"), "{}\n")
writeFile(t, filepath.Join(projRoot, "topdata", "data", "parts", "belt.json"), `{
"output": "parts_belt.2da",
"columns": ["COSTMODIFIER", "ACBONUS"],
"rows": []
}`+"\n")
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
http.NotFound(w, r)
}))
defer server.Close()
runGitTest(t, "", "init", "-b", "main", projRoot)
runGitTest(t, projRoot, "remote", "add", "origin", server.URL+"/ShadowsOverWestgate/sow-module.git")
proj := &project.Project{
Root: projRoot,
Config: project.Config{
Module: project.ModuleConfig{Name: "Test", ResRef: "test"},
Paths: project.PathConfig{Source: "src", Assets: "assets", Build: "build"},
TopData: project.TopDataConfig{
Source: "topdata",
Build: "build/topdata",
},
},
}
_, err := BuildNative(proj, nil)
if err == nil || !strings.Contains(err.Error(), "fetch parts manifest release metadata") {
t.Fatalf("expected missing released parts manifest to fail, got %v", err)
}
}
@@ -8214,6 +8417,18 @@ func diagnosticsText(diags []Diagnostic) string {
return strings.Join(lines, "\n")
}
func runGitTest(t *testing.T, dir string, args ...string) {
t.Helper()
cmd := exec.Command("git", args...)
if dir != "" {
cmd.Dir = dir
}
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %v failed: %v\n%s", args, err, string(output))
}
}
func testProjectRoot(t *testing.T) string {
t.Helper()
root := t.TempDir()
@@ -8233,6 +8448,7 @@ func testProject(root string) *project.Project {
Source: "topdata",
Build: "build/topdata",
ReferenceBuilder: "reference",
Assets: ".",
},
},
}