Files
sow-tools/internal/topdata/parts_cdn_channel_test.go
T
archvillainetteandClaude Opus 4.8 2586b9193e
build-binaries / build-binaries (pull_request) Successful in 2m15s
test-image / build-image (pull_request) Successful in 37s
test / test (pull_request) Successful in 1m24s
feat(topdata): parts autogen from CDN part.yml channel
Phase 2 of the parts-row autogen design: Crucible resolves parts 2DA rows
from the immutable part.yml published with the asset HAK release, over the
anonymous Bunny CDN channel, with the parts consumer required (no fail-open).

- generalize resolveCDNChannelManifest: derive manifest basename + provenance
  label from config; drop hardcoded assets/vfxs.yml so part.yml resolves too
- add filterPartsCDNChannelEntries + mode dispatch implementing the inventory
  contract (restype mdl under part/<cat>/, trailing-digit row id, dedup
  l/r/race/gender variants, sort by source, zero accepted rows = hard fail)
- support hand category + parts/hand dataset mapping (12 datasets total)
- native pipeline: single augment -> normalize -> override sequence with the
  configured parts_rows policy; overrides may update but never synthesize a row
- reject multiple parts_rows consumers in project validation
- tests: parts filter contract, category coverage, offline basename, orphan
  override reject, and a parity guard proving a bare CDN-only parts build

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-26 13:18:20 +02:00

88 lines
2.8 KiB
Go

package topdata
import (
"testing"
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
)
func projectPartsConsumer() project.AutogenConsumerConfig {
return project.AutogenConsumerConfig{ID: "parts", Producer: "parts", Mode: "parts_rows"}
}
const partYML = `assets:
- {path: part/belt/m/pfa0_belt017.mdl, restype: mdl}
- {path: part/hand/m/pfh0_handl105.mdl, restype: mdl}
- {path: part/hand/m/pfh0_handr105.mdl, restype: mdl}
- {path: part/leg/m/pfa0_legl001.mdl, restype: mdl}
- {path: part/leg/m/pfa0_legr001.mdl, restype: mdl}
- {path: part/cloak/m/cloak001.mdl, restype: mdl}
- {path: part/belt/m/readme.txt, restype: txt}
`
func TestFilterPartsKeepsSupportedDedupsVariants(t *testing.T) {
consumer := projectPartsConsumer()
entries, err := filterCDNChannelEntries([]byte(partYML), consumer)
if err != nil {
t.Fatalf("filter: %v", err)
}
inv := autogenPartsInventory(entries)
if _, ok := inv["belt"][17]; !ok {
t.Errorf("missing belt 17")
}
if len(inv["hand"]) != 1 {
t.Errorf("hand should dedup l/r to 1 row, got %d", len(inv["hand"]))
}
if _, ok := inv["hand"][105]; !ok {
t.Errorf("missing hand 105")
}
if len(inv["leg"]) != 1 {
t.Errorf("leg should dedup l/r to 1 row, got %d", len(inv["leg"]))
}
if _, ok := inv["cloak"]; ok {
t.Errorf("cloak must be ignored")
}
}
func TestFilterPartsRejectsZeroAccepted(t *testing.T) {
_, err := filterCDNChannelEntries([]byte("assets:\n - {path: part/cloak/m/cloak001.mdl, restype: mdl}\n"), projectPartsConsumer())
if err == nil {
t.Fatal("expected hard error on zero accepted part rows")
}
}
func TestFilterPartsRejectsRowZeroAndNonNumeric(t *testing.T) {
for _, doc := range []string{
"assets:\n - {path: part/belt/m/pfa0_belt000.mdl, restype: mdl}\n",
"assets:\n - {path: part/belt/m/pfa0_belt.mdl, restype: mdl}\n",
} {
if _, err := filterCDNChannelEntries([]byte(doc), projectPartsConsumer()); err == nil {
t.Fatalf("expected error for %q", doc)
}
}
}
func TestFilterPartsRejectsMalformedAndMissingAssets(t *testing.T) {
if _, err := filterCDNChannelEntries([]byte("assets:\n - path: [unterminated"), projectPartsConsumer()); err == nil {
t.Fatal("expected malformed error")
}
if _, err := filterCDNChannelEntries([]byte("other: 1\n"), projectPartsConsumer()); err == nil {
t.Fatal("expected no-assets error")
}
}
func TestSupportedPartCategoriesCoverTwelveDatasets(t *testing.T) {
want := []string{"belt", "bicep", "chest", "foot", "forearm", "hand", "leg", "neck", "pelvis", "robe", "shin", "shoulder"}
for _, c := range want {
if !isSupportedPartCategory(c) {
t.Errorf("category %q not supported", c)
}
}
if partDatasetToAssetCategory["hand"] != "hand" {
t.Errorf("parts/hand must map to asset category hand")
}
if partDatasetToAssetCategory["legs"] != "leg" {
t.Errorf("parts/legs must map to asset category leg")
}
}