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>
101 lines
3.4 KiB
Go
101 lines
3.4 KiB
Go
package topdata
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
|
)
|
|
|
|
// PARITY GUARD: a bare project (no manifest_file, no NWN_ROOT, no token) with a
|
|
// cdn_channel consumer must produce accessory rows. If accessory resolution ever
|
|
// drifts back out of Crucible into a wrapper, this fails.
|
|
func TestParityGuardCDNChannelProducesAccessoryRows(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
srv := cdnServer(t, map[string]string{
|
|
"/releases/haks/channels.json": `{"current":"v1.0.0"}`,
|
|
"/releases/haks/v1.0.0/vfxs.yml": `assets:
|
|
- path: vfxs/head_accessories/hat/hfx_bandana.mdl
|
|
restype: mdl
|
|
sha256: aaa
|
|
- path: vfxs/chest_accessories/cape/cfx_cloak.mdl
|
|
restype: mdl
|
|
sha256: bbb
|
|
`,
|
|
})
|
|
t.Setenv("BUNNY_CDN_BASE", srv.URL)
|
|
t.Setenv("SOW_TOPDATA_ASSET_CHANNEL", "current")
|
|
// Prove R3: no NWN_ROOT in the environment.
|
|
t.Setenv("NWN_ROOT", "")
|
|
|
|
c := cdnConsumer()
|
|
c.Source.CDNBaseEnv = "BUNNY_CDN_BASE"
|
|
c.Source.ChannelEnv = "SOW_TOPDATA_ASSET_CHANNEL"
|
|
c.AccessoryVisualeffects.ModelColumn = "Imp_HeadCon_Node"
|
|
|
|
p := testProject(root)
|
|
p.Config.Autogen.Consumers = []project.AutogenConsumerConfig{c}
|
|
|
|
collected := []nativeCollectedDataset{{
|
|
Dataset: nativeDataset{Name: "visualeffects", Kind: nativeDatasetBase},
|
|
Columns: []string{"Label", "Imp_HeadCon_Node", "Imp_Root_S_Node"},
|
|
Rows: nil,
|
|
LockData: map[string]int{},
|
|
}}
|
|
|
|
got, err := applyAutogenConsumers(p, collected, nil)
|
|
if err != nil {
|
|
t.Fatalf("applyAutogenConsumers failed: %v", err)
|
|
}
|
|
if len(got[0].Rows) != 2 {
|
|
t.Fatalf("PARITY GUARD: expected 2 accessory rows, got %d (%#v)", len(got[0].Rows), got[0].Rows)
|
|
}
|
|
wantKey := "visualeffects:head_accessories/hat/hfx_bandana.mdl"
|
|
if _, ok := got[0].LockData[wantKey]; !ok {
|
|
t.Fatalf("PARITY GUARD: expected lock id for %s, got %#v", wantKey, got[0].LockData)
|
|
}
|
|
}
|
|
|
|
// PARITY GUARD (parts): a bare project whose only parts source is the offline
|
|
// CDN-equivalent (SOW_PART_MANIFEST -> a part.yml), with no NWN_ROOT, no token,
|
|
// no asset checkout, must produce parts rows from the filtered inventory. If
|
|
// parts resolution ever drifts back into a wrapper, this fails.
|
|
func TestParityGuardCDNChannelProducesPartsRows(t *testing.T) {
|
|
root := testProjectRoot(t)
|
|
checkout := filepath.Join(root, "manifest-checkout")
|
|
if err := os.MkdirAll(filepath.Join(checkout, "assets"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFile(t, filepath.Join(checkout, "assets", "part.yml"),
|
|
"assets:\n - {path: part/belt/m/pfa0_belt017.mdl, restype: mdl}\n - {path: part/belt/m/pfa0_belt018.mdl, restype: mdl}\n")
|
|
t.Setenv("SOW_PART_MANIFEST", checkout)
|
|
t.Setenv("NWN_ROOT", "")
|
|
|
|
c := projectPartsConsumer()
|
|
c.Source = project.AutogenSourceConfig{
|
|
Kind: "cdn_channel",
|
|
ChannelsPath: "releases/haks/channels.json",
|
|
ManifestPath: "releases/haks/{tag}/part.yml",
|
|
ReleaseMarkerPath: "releases/haks/{tag}/haks.json",
|
|
OfflineOverrideEnv: "SOW_PART_MANIFEST",
|
|
}
|
|
|
|
p := testProject(root)
|
|
p.Config.Autogen.Consumers = []project.AutogenConsumerConfig{c}
|
|
|
|
collected := []nativeCollectedDataset{partsDatasetWithIDs("parts/belt", nil)}
|
|
|
|
got, err := applyAutogenConsumers(p, collected, nil)
|
|
if err != nil {
|
|
t.Fatalf("applyAutogenConsumers failed: %v", err)
|
|
}
|
|
ids := map[int]bool{}
|
|
for _, row := range got[0].Rows {
|
|
ids[row["id"].(int)] = true
|
|
}
|
|
if !ids[17] || !ids[18] {
|
|
t.Fatalf("PARITY GUARD: expected belt rows 17 and 18 from inventory, got %#v", got[0].Rows)
|
|
}
|
|
}
|