fix(game): #59 — per-command rejection on PUT /api/v1/order
Validation of a player's order now applies every command against a transient game-state snapshot and records the per-command outcome (cmdApplied, cmdErrorCode) in each command's meta. The order is persisted even when some commands are rejected, and the response is 202 + UserGamesOrder so clients can surface the partial failure without the chain collapsing into "downstream service is unavailable". Pkg/error consts are reshelved onto three explicit ranges with a package doc and helpers (IsInternalCode/IsInputCode/IsGameStateCode): 1xxx internal/server (500/501), 2xxx structural input (400), 3xxx game-state per-command rejection (400 when escaping HTTP, otherwise recorded as cmdErrorCode). Two pre-existing typos fixed mechanically (ErrBeakGroupNumberNotEnough -> ErrBreakGroupNumberNotEnough, ErrRaceExinct -> ErrRaceExtinct) along with all callsites. Engine errorResponse maps *GenericError by shelf rather than mapping everything to 500. The Quit-not-last structural check in Controller.ValidateOrder is preserved and its type assertion fixed (was a value assertion against a pointer-typed command, so the check silently never fired). Backend, gateway and UI are unchanged — they were already correct on the 202 path; only the engine collapsing per-command rejection into 500 was needed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -89,7 +89,7 @@ func TestShipGroupMerge(t *testing.T) {
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupMerge(Race_Extinct.Name),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
e.GenericErrorText(e.ErrRaceExtinct))
|
||||
|
||||
assert.NoError(t, g.ShipGroupMerge(Race_0.Name))
|
||||
|
||||
@@ -139,7 +139,7 @@ func TestShipGroupBreak(t *testing.T) {
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_Extinct.Name, c.ShipGroup(0).ID, uuid.New(), 1),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
e.GenericErrorText(e.ErrRaceExtinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_0.Name, uuid.New(), uuid.New(), 1),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
@@ -148,7 +148,7 @@ func TestShipGroupBreak(t *testing.T) {
|
||||
e.GenericErrorText(e.ErrInputNewEntityDuplicateIdentifier))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_0.Name, c.ShipGroup(0).ID, uuid.New(), 17),
|
||||
e.GenericErrorText(e.ErrBeakGroupNumberNotEnough))
|
||||
e.GenericErrorText(e.ErrBreakGroupNumberNotEnough))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupBreak(Race_0.Name, c.ShipGroup(1).ID, uuid.New(), 1),
|
||||
e.GenericErrorText(e.ErrShipsBusy))
|
||||
@@ -220,10 +220,10 @@ func TestShipGroupTransfer(t *testing.T) {
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_0.Name, Race_Extinct.Name, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
e.GenericErrorText(e.ErrRaceExtinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_Extinct.Name, Race_1.Name, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
e.GenericErrorText(e.ErrRaceExtinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupTransfer(Race_0.Name, Race_0.Name, c.ShipGroup(1).ID),
|
||||
e.GenericErrorText(e.ErrInputSameRace))
|
||||
@@ -320,7 +320,7 @@ func TestShipGroupLoad(t *testing.T) {
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_Extinct.Name, c.ShipGroup(0).ID, game.CargoMaterial.String(), 0),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
e.GenericErrorText(e.ErrRaceExtinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupLoad(Race_0.Name, c.ShipGroup(0).ID, "GOLD", 0),
|
||||
e.GenericErrorText(e.ErrInputCargoTypeInvalid))
|
||||
@@ -434,7 +434,7 @@ func TestShipGroupUnload(t *testing.T) {
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_Extinct.Name, c.ShipGroup(0).ID, 0),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
e.GenericErrorText(e.ErrRaceExtinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupUnload(Race_0.Name, uuid.New(), 0),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
@@ -509,7 +509,7 @@ func TestShipGroupDismantle(t *testing.T) {
|
||||
e.GenericErrorText(e.ErrInputUnknownRace))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupDismantle(Race_Extinct.Name, c.ShipGroup(0).ID),
|
||||
e.GenericErrorText(e.ErrRaceExinct))
|
||||
e.GenericErrorText(e.ErrRaceExtinct))
|
||||
assert.ErrorContains(t,
|
||||
g.ShipGroupDismantle(Race_0.Name, uuid.New()),
|
||||
e.GenericErrorText(e.ErrInputEntityNotExists))
|
||||
|
||||
Reference in New Issue
Block a user