Phase 2 of the parts-row autogen design — Crucible (sow-tools) side only. 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). ## Changes - **Resolver (T3):** generalize \`resolveCDNChannelManifest\` — derive manifest basename + provenance label from config; drop hardcoded \`assets/vfxs.yml\` so \`part.yml\` resolves through the same path. VFX behavior unchanged. - **Parts filter (T4):** \`filterCDNChannelEntries\` dispatches on consumer mode; new \`filterPartsCDNChannelEntries\` implements the inventory contract — \`restype: mdl\` under \`part/<supported-cat>/\`, row id from trailing digits, dedup l/r/race/gender variants by (category,rowID), sort by source, **zero accepted rows = hard fail**, reject row id 0 / non-numeric / malformed / no-assets. - **Category (T5):** add \`hand\` + \`parts/hand\` mapping (12 datasets; \`leg→legs\` already present). - **Pipeline (T6):** native build runs one explicit augment → normalize → override sequence under the configured \`parts_rows\` policy; overrides may update an existing/discovered row but **never synthesize** one (orphan override now errors). - **Validation (T7):** reject more than one \`parts_rows\` consumer. ## Tests 9 new tests: parts filter contract, 12-dataset coverage, offline manifest-basename, orphan-override reject + discovered-row update, and a parity guard proving a bare CDN-only parts build needs no wrapper/token/NWN_ROOT/checkout. Gate green: \`go test ./internal/topdata/... ./internal/project/...\` + full \`go build ./... && go test ./...\`. ## Scope / ordering Phase 2 is inert until **Phase 1** (sow-assets-manifest publishes \`part.yml\`) ships and the operational **Gate** (release publish/promote) runs, then **Phase 3** enables the sow-topdata consumer. Do not enable the consumer against a channel whose current release predates \`part.yml\`. Plan: \`sow-topdata/docs/superpowers/plans/2026-06-25-topdata-parts-autogen-channel.md\` 🤖 Generated with [Claude Code](https://claude.com/claude-code) Reviewed-on: #25 Co-authored-by: vickydotbat <vickydotbat@tutamail.com> Co-committed-by: vickydotbat <vickydotbat@tutamail.com>
52 lines
1.7 KiB
Go
52 lines
1.7 KiB
Go
package topdata
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"git.westgate.pw/ShadowsOverWestgate/sow-tools/internal/project"
|
|
)
|
|
|
|
func partsDatasetWithIDs(name string, ids []int) nativeCollectedDataset {
|
|
rows := make([]map[string]any, 0, len(ids))
|
|
for _, id := range ids {
|
|
rows = append(rows, map[string]any{"id": id, "COSTMODIFIER": "0", "ACBONUS": "0.00"})
|
|
}
|
|
return nativeCollectedDataset{
|
|
Dataset: nativeDataset{Name: name, Kind: nativeDatasetBase},
|
|
Columns: []string{"COSTMODIFIER", "ACBONUS"},
|
|
Rows: rows,
|
|
}
|
|
}
|
|
|
|
func TestApplyPartOverridesRejectsOrphan(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(dir, "data", "parts", "overrides"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFile(t, filepath.Join(dir, "data", "parts", "overrides", "belt.json"),
|
|
`{"overrides": [{"id": 999, "COSTMODIFIER": "5"}]}`)
|
|
collected := []nativeCollectedDataset{partsDatasetWithIDs("parts/belt", []int{17})}
|
|
if _, err := applyPartOverridesWithConfig(dir, collected, project.PartsRowsConfig{}); err == nil {
|
|
t.Fatal("expected orphan override to fail, not synthesize a row")
|
|
}
|
|
}
|
|
|
|
func TestApplyPartOverridesUpdatesDiscoveredRow(t *testing.T) {
|
|
dir := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(dir, "data", "parts", "overrides"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeFile(t, filepath.Join(dir, "data", "parts", "overrides", "belt.json"),
|
|
`{"overrides": [{"id": 17, "COSTMODIFIER": "5"}]}`)
|
|
collected := []nativeCollectedDataset{partsDatasetWithIDs("parts/belt", []int{17})}
|
|
out, err := applyPartOverridesWithConfig(dir, collected, project.PartsRowsConfig{})
|
|
if err != nil {
|
|
t.Fatalf("override: %v", err)
|
|
}
|
|
if got := out[0].Rows[0]["COSTMODIFIER"]; got != "5" {
|
|
t.Fatalf("row 17 COSTMODIFIER want 5, got %v", got)
|
|
}
|
|
}
|