adventure: let a player act from the web, not just read about it
The equip queue proved the reverse pipe works. This gives it verbs that play the game: pull out of a run from the adventurer page, take today's bout from the war room. Its own table and its own poll, not more actions on equip_orders. Every column of that table is equip vocabulary (item, slot, tier) and these verbs act on the character rather than on something it is carrying. Nothing in a request names an adventurer. The session maps to one localpart and a localpart to one adventurer, so Pete resolves the character itself and there is no id on the wire to forge. The panel's copy is kept honest by the verdict: an applied action hides the offer it has just spent, a refusal puts the button back. Watching it run is what put that there, along with the strip's layout — gogobee answers a bout with a whole sentence of damage, which the equip strip's two-column row squeezed into a column and wrapped the verb. Claude-Session: https://claude.ai/code/session_012bxpQQJDjC1mTtLN3VVtBQ
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAdvOrderRoundTrip(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
|
||||
o, err := InsertAdvOrder("sub-1", "josie", "tok-josie", "Josie", AdvActionExtract)
|
||||
if err != nil {
|
||||
t.Fatalf("insert: %v", err)
|
||||
}
|
||||
if o.GUID == "" || o.Status != AdvOrderPending {
|
||||
t.Fatalf("order = %+v, want a guid and pending", o)
|
||||
}
|
||||
|
||||
pending, err := PendingAdvOrders(10)
|
||||
if err != nil {
|
||||
t.Fatalf("pending: %v", err)
|
||||
}
|
||||
if len(pending) != 1 || pending[0].GUID != o.GUID || pending[0].Action != AdvActionExtract {
|
||||
t.Fatalf("pending = %+v", pending)
|
||||
}
|
||||
|
||||
got, err := ResolveAdvOrder(o.GUID, AdvOrderApplied, "out on day 3")
|
||||
if err != nil {
|
||||
t.Fatalf("resolve: %v", err)
|
||||
}
|
||||
if got.Status != AdvOrderApplied || got.Detail != "out on day 3" {
|
||||
t.Fatalf("resolved = %+v", got)
|
||||
}
|
||||
if left, _ := PendingAdvOrders(10); len(left) != 0 {
|
||||
t.Fatalf("a resolved order is still pending: %+v", left)
|
||||
}
|
||||
}
|
||||
|
||||
// TestAdvOrderVerdictOnlyMovesAPendingRow is the idempotency mechanic: gogobee
|
||||
// retries its verdict push, so the second one must be a read, not a write.
|
||||
func TestAdvOrderVerdictOnlyMovesAPendingRow(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
o, _ := InsertAdvOrder("sub-1", "josie", "tok-josie", "Josie", AdvActionSiegeJoin)
|
||||
if _, err := ResolveAdvOrder(o.GUID, AdvOrderApplied, "first"); err != nil {
|
||||
t.Fatalf("first verdict: %v", err)
|
||||
}
|
||||
got, err := ResolveAdvOrder(o.GUID, AdvRejectedNoSiege, "second")
|
||||
if err != nil {
|
||||
t.Fatalf("second verdict: %v", err)
|
||||
}
|
||||
if got.Status != AdvOrderApplied || got.Detail != "first" {
|
||||
t.Fatalf("order = %q/%q, want the first verdict to stand", got.Status, got.Detail)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvOrderRejectsBadInput(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
|
||||
if _, err := InsertAdvOrder("sub-1", "josie", "tok", "Josie", "sell_house"); err == nil {
|
||||
t.Fatal("an unknown action was accepted")
|
||||
}
|
||||
o, _ := InsertAdvOrder("sub-1", "josie", "tok", "Josie", AdvActionExtract)
|
||||
if _, err := ResolveAdvOrder(o.GUID, "exploded", ""); err == nil {
|
||||
t.Fatal("an unknown verdict was accepted")
|
||||
}
|
||||
if _, err := AdvOrderByGUID("nope"); !errors.Is(err, ErrNoSuchAdvOrder) {
|
||||
t.Fatalf("unknown guid err = %v, want ErrNoSuchAdvOrder", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestHasPendingAdvOrderIsPerVerb: the guard stops a double-click on one button,
|
||||
// not the other button.
|
||||
func TestHasPendingAdvOrderIsPerVerb(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
o, _ := InsertAdvOrder("sub-1", "josie", "tok", "Josie", AdvActionExtract)
|
||||
|
||||
if got, _ := HasPendingAdvOrder("sub-1", AdvActionExtract); !got {
|
||||
t.Fatal("a pending extract wasn't seen")
|
||||
}
|
||||
if got, _ := HasPendingAdvOrder("sub-1", AdvActionSiegeJoin); got {
|
||||
t.Fatal("a pending extract blocked a bout")
|
||||
}
|
||||
if got, _ := HasPendingAdvOrder("sub-2", AdvActionExtract); got {
|
||||
t.Fatal("one owner's pending order was seen for another")
|
||||
}
|
||||
// A resolved order stops holding the verb.
|
||||
if _, err := ResolveAdvOrder(o.GUID, AdvOrderApplied, ""); err != nil {
|
||||
t.Fatalf("resolve: %v", err)
|
||||
}
|
||||
if got, _ := HasPendingAdvOrder("sub-1", AdvActionExtract); got {
|
||||
t.Fatal("a resolved order still holds its verb")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvOrdersByOwnerScopes(t *testing.T) {
|
||||
setupTestDB(t)
|
||||
if _, err := InsertAdvOrder("sub-A", "alice", "tok-a", "Alice", AdvActionExtract); err != nil {
|
||||
t.Fatalf("insert: %v", err)
|
||||
}
|
||||
if _, err := InsertAdvOrder("sub-B", "bob", "tok-b", "Bob", AdvActionExtract); err != nil {
|
||||
t.Fatalf("insert: %v", err)
|
||||
}
|
||||
got, err := AdvOrdersByOwner("sub-A", 10)
|
||||
if err != nil {
|
||||
t.Fatalf("by owner: %v", err)
|
||||
}
|
||||
if len(got) != 1 || got[0].OwnerLocalpart != "alice" {
|
||||
t.Fatalf("orders = %+v, want only alice's", got)
|
||||
}
|
||||
if n, _ := CountAdvOrdersSince("sub-A", 0); n != 1 {
|
||||
t.Fatalf("count = %d, want 1", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user