feat(ui+legacy): F8-05 owner-feedback round 1 — inspector tweaks + parser
Owner-reported polish on top of #48, plus a legacy-parser gap that prevented verifying stationed ship groups against a real .REP fixture. UI: - Production: drop the empty `(production)` placeholder option. Owned planets always produce something, so the primary select now opens on `industry` by default when `planet.production` is null/unknown, keeping the row inside the four real production kinds at all times. - Production: lock the row to a single line (no flex-wrap) and strip border + padding from the ✓/✗ buttons so the apply/cancel icons read as glyphs and the row no longer breaks into two visual rows for Research / Ship contexts where both selects are present. - Cargo routes: the placeholder option is now an `<option disabled>` styled like a section header (greyed, italic) and reads "manage routes" instead of "cargo routes". The wording shifts the intent from a section label to an action prompt. Legacy parser: - F8-05 (#48 п.32) "Stationed ship groups" couldn't be verified against the dg fixture because the legacy `<Race> Groups` blocks (outside battles) and the `Unidentified Groups` block were dropped by the parser — both are now wired up. Foreign group rows parse the `# T D W S C T Q D P M` columns and resolve the destination against the parsed planet tables (rows with an invisible destination drop, matching the existing local-group convention). The legacy row carries no origin / range columns, so foreign groups surface as stationed at the destination. - Smoke tests on every fixture extended with `otherGroups` and `unidentifiedGroups` counts. New focused unit test `TestParseOtherAndUnidentifiedGroups` covers the column layout, the drop-on-unknown-destination rule, and the `X Y`-only unidentified rows. - `tools/local-dev/reports/dg/KNNTS039.json` and `tools/local-dev/reports/dg/KNNTS041.json` regenerated so the synthetic-loader fixtures carry the new arrays. - README updated: the two sections move out of "Skipped sections" into a "Foreign and unidentified groups" block; package doc-comment reflects the broader scope. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -790,6 +790,87 @@ func TestParseIncomingGroups(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestParseOtherAndUnidentifiedGroups(t *testing.T) {
|
||||
// Mixed fixture: two foreign "<Race> Groups" sections (one row
|
||||
// drops because its destination is not in any parsed planet
|
||||
// table), plus a single "Unidentified Groups" block. The
|
||||
// per-race section header sets `otherOwner`, but the resulting
|
||||
// model carries the destination planet number only — the race
|
||||
// name surfaces through battle rosters and the planet's
|
||||
// `owner` field, not on the group row.
|
||||
in := strings.Join([]string{
|
||||
"Race Report for Galaxy PLUS Turn 1",
|
||||
"",
|
||||
"Your Planets",
|
||||
"",
|
||||
" # X Y N S P I R P $ M C L",
|
||||
" 17 171.05 700.24 Castle 1000.00 1000.00 1000.00 10.00 Capital 0.00 0.68 88.78 1000.00",
|
||||
"",
|
||||
"Aliens Planets",
|
||||
"",
|
||||
" # X Y N S P I R P $ M C L",
|
||||
" 42 200.00 300.00 Far 500.00 500.00 500.00 10.00 Capital 0.00 0.00 0.00 500.00",
|
||||
"",
|
||||
"Aliens Groups",
|
||||
"",
|
||||
" # T D W S C T Q D P M",
|
||||
" 6 Skiff 3.40 0 0 1 COL 2 Castle 68.00 3.00",
|
||||
" 1 Drone 5.10 0 0 0 - 0 Limbo 102.0 1.00",
|
||||
"",
|
||||
"Reds Groups",
|
||||
"",
|
||||
" # T D W S C T Q D P M",
|
||||
" 1 Phantom 4.20 0 0 0 - 0 Far 84.0 9.00",
|
||||
"",
|
||||
"Unidentified Groups",
|
||||
"",
|
||||
" X Y",
|
||||
"100.50 200.25",
|
||||
"333.10 444.20",
|
||||
"",
|
||||
}, "\n")
|
||||
rep, _, err := Parse(strings.NewReader(in))
|
||||
if err != nil {
|
||||
t.Fatalf("Parse: %v", err)
|
||||
}
|
||||
// Skiff → Castle resolves against Your Planets, Phantom → Far
|
||||
// resolves against Other Planets, Drone → Limbo drops (unknown).
|
||||
if got, want := len(rep.OtherGroup), 2; got != want {
|
||||
t.Fatalf("len(OtherGroup) = %d, want %d (Drone → Limbo dropped due to unknown destination); rows=%+v",
|
||||
got, want, rep.OtherGroup)
|
||||
}
|
||||
a := rep.OtherGroup[0]
|
||||
if a.Class != "Skiff" || a.Number != 6 || a.Destination != 17 {
|
||||
t.Errorf("a (Class, Number, Destination) = (%q, %d, %d), want (\"Skiff\", 6, 17)",
|
||||
a.Class, a.Number, a.Destination)
|
||||
}
|
||||
if a.Cargo != "COL" || float64(a.Load) != 2 || float64(a.Mass) != 3 {
|
||||
t.Errorf("a (Cargo, Load, Mass) = (%q, %v, %v), want (\"COL\", 2, 3)",
|
||||
a.Cargo, float64(a.Load), float64(a.Mass))
|
||||
}
|
||||
if a.Origin != nil || a.Range != nil {
|
||||
t.Errorf("a (Origin, Range) = (%v, %v), want (nil, nil) — legacy foreign rows have no origin/range columns",
|
||||
a.Origin, a.Range)
|
||||
}
|
||||
if float64(a.Tech["drive"]) != 3.40 {
|
||||
t.Errorf("a Tech.drive = %v, want 3.40", float64(a.Tech["drive"]))
|
||||
}
|
||||
b := rep.OtherGroup[1]
|
||||
if b.Class != "Phantom" || b.Number != 1 || b.Destination != 42 {
|
||||
t.Errorf("b (Class, Number, Destination) = (%q, %d, %d), want (\"Phantom\", 1, 42)",
|
||||
b.Class, b.Number, b.Destination)
|
||||
}
|
||||
|
||||
if got, want := len(rep.UnidentifiedGroup), 2; got != want {
|
||||
t.Fatalf("len(UnidentifiedGroup) = %d, want %d", got, want)
|
||||
}
|
||||
u := rep.UnidentifiedGroup[0]
|
||||
if float64(u.X) != 100.50 || float64(u.Y) != 200.25 {
|
||||
t.Errorf("u[0] (X, Y) = (%v, %v), want (100.50, 200.25)",
|
||||
float64(u.X), float64(u.Y))
|
||||
}
|
||||
}
|
||||
|
||||
// --- smoke tests -----------------------------------------------------
|
||||
|
||||
type smokeWant struct {
|
||||
@@ -801,6 +882,7 @@ type smokeWant struct {
|
||||
players, extinct, local, other int
|
||||
uninhabited, unidentified, shipClasses int
|
||||
localGroups, localFleets, incomingGroups int
|
||||
otherGroups, unidentifiedGroups int
|
||||
localScience, otherScience, otherShipClass int
|
||||
bombings, shipProductions int
|
||||
battles int
|
||||
@@ -849,6 +931,8 @@ func runSmoke(t *testing.T, path string, want smokeWant) {
|
||||
{"LocalGroup", len(rep.LocalGroup), want.localGroups},
|
||||
{"LocalFleet", len(rep.LocalFleet), want.localFleets},
|
||||
{"IncomingGroup", len(rep.IncomingGroup), want.incomingGroups},
|
||||
{"OtherGroup", len(rep.OtherGroup), want.otherGroups},
|
||||
{"UnidentifiedGroup", len(rep.UnidentifiedGroup), want.unidentifiedGroups},
|
||||
{"LocalScience", len(rep.LocalScience), want.localScience},
|
||||
{"OtherScience", len(rep.OtherScience), want.otherScience},
|
||||
{"OtherShipClass", len(rep.OtherShipClass), want.otherShipClass},
|
||||
@@ -888,6 +972,7 @@ func runSmoke(t *testing.T, path string, want smokeWant) {
|
||||
// silently drops half the data.
|
||||
func TestParseDgKNNTS039(t *testing.T) {
|
||||
runSmoke(t, "../reports/dg/KNNTS039.REP", smokeWant{
|
||||
otherGroups: 723, unidentifiedGroups: 72,
|
||||
race: "KnightErrants", turn: 39,
|
||||
mapW: 800, mapH: 800, planetCount: 700,
|
||||
voteFor: "KnightErrants", votes: 16.02,
|
||||
@@ -908,6 +993,7 @@ func TestParseDgKNNTS039(t *testing.T) {
|
||||
|
||||
func TestParseDgKNNTS040(t *testing.T) {
|
||||
runSmoke(t, "../reports/dg/KNNTS040.REP", smokeWant{
|
||||
otherGroups: 734, unidentifiedGroups: 109,
|
||||
race: "KnightErrants", turn: 40,
|
||||
mapW: 800, mapH: 800, planetCount: 700,
|
||||
players: 91, extinct: 49,
|
||||
@@ -930,6 +1016,7 @@ func TestParseDgKNNTS040(t *testing.T) {
|
||||
// exercises the deferred name-resolution path in [parser.finish].
|
||||
func TestParseDgKNNTS041(t *testing.T) {
|
||||
runSmoke(t, "../reports/dg/KNNTS041.REP", smokeWant{
|
||||
otherGroups: 772, unidentifiedGroups: 349,
|
||||
race: "KnightErrants", turn: 41,
|
||||
mapW: 800, mapH: 800, planetCount: 700,
|
||||
players: 91, extinct: 50,
|
||||
@@ -952,6 +1039,7 @@ func TestParseDgKNNTS041(t *testing.T) {
|
||||
// gplus also sneaks "Incoming Groups" between sections.
|
||||
func TestParseGplus40(t *testing.T) {
|
||||
runSmoke(t, "../reports/gplus/40.REP", smokeWant{
|
||||
otherGroups: 1042, unidentifiedGroups: 44,
|
||||
race: "MbI", turn: 40,
|
||||
mapW: 350, mapH: 350, planetCount: 300,
|
||||
players: 26, extinct: 0,
|
||||
@@ -974,6 +1062,7 @@ func TestParseGplus40(t *testing.T) {
|
||||
// membership shape (no "Incoming Groups" this turn).
|
||||
func TestParseDgKiller031(t *testing.T) {
|
||||
runSmoke(t, "../reports/dg/Killer031.rep", smokeWant{
|
||||
otherGroups: 925, unidentifiedGroups: 34,
|
||||
race: "Killer", turn: 31,
|
||||
mapW: 250, mapH: 250, planetCount: 175,
|
||||
players: 25, extinct: 12,
|
||||
@@ -997,6 +1086,7 @@ func TestParseDgKiller031(t *testing.T) {
|
||||
// deferred name resolution is exercised in production conditions).
|
||||
func TestParseDgTancordia037(t *testing.T) {
|
||||
runSmoke(t, "../reports/dg/Tancordia037.rep", smokeWant{
|
||||
otherGroups: 580, unidentifiedGroups: 24,
|
||||
race: "Tancordia", turn: 37,
|
||||
mapW: 210, mapH: 210, planetCount: 140,
|
||||
players: 18, extinct: 7,
|
||||
|
||||
Reference in New Issue
Block a user