test(askowner): red for #41 — ask_owner kind, approval_phrase, fail-safe resolved field
EVENTS.md contract first: ask_owner kind row, approval_phrase and computed resolved payload fields, owner-facing service roles (Quill/Sentry) may post ask_owner, crow ask_owner headers drop to the default tag, 10-minute fail-safe window shared with list approvals. Tests fail: Message has no ApprovalPhrase, Server has no askTimeout.
This commit is contained in:
1 parent
9781bfa3b3
commit
9118aabb85
2 files changed
+151
-6
No files matched your search
@@ -0,0 +1,141 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// askNest posts a standard ask_owner from Quill and returns the nest id and
|
||||
// the stored record. Quill is an owner-facing service role, so this is the
|
||||
// shape the spec (CHATROOM_SPEC section 4/6) calls for.
|
||||
func askNest(t *testing.T, ts *httptest.Server) (string, map[string]any) {
|
||||
t.Helper()
|
||||
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
|
||||
code, ask := postMsg(t, ts, id, `{"author":"Quill","kind":"ask_owner","to":"owner",`+
|
||||
`"text":"Soot wrote fiction on a retrieval job and is quarantined. Reply with the phrase to reinstate the crow.",`+
|
||||
`"approval_phrase":"reinstate Hugin"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("ask_owner post: want 201, got %d %v", code, ask)
|
||||
}
|
||||
return id, ask
|
||||
}
|
||||
|
||||
// askResolved reads the nest log and returns the ask's computed resolved field.
|
||||
func askResolved(t *testing.T, ts *httptest.Server, nestID, askID string) string {
|
||||
t.Helper()
|
||||
for _, m := range nestMessages(t, ts, nestID) {
|
||||
if m["msg_id"] == askID {
|
||||
r, ok := m["resolved"].(string)
|
||||
if !ok {
|
||||
t.Fatalf("ask record has no string resolved field: %v", m)
|
||||
}
|
||||
return r
|
||||
}
|
||||
}
|
||||
t.Fatalf("ask %s not in the nest log", askID)
|
||||
return ""
|
||||
}
|
||||
|
||||
func TestAskOwnerPostedAndSurfaced(t *testing.T) {
|
||||
ts, _ := newTestNestServer(t)
|
||||
id, ask := askNest(t, ts)
|
||||
if ask["kind"] != "ask_owner" || ask["author"] != "Quill" || ask["to"] != "owner" {
|
||||
t.Fatalf("bad ask record: %v", ask)
|
||||
}
|
||||
if ask["approval_phrase"] != "reinstate Hugin" {
|
||||
t.Fatalf("POST record must carry approval_phrase, got %v", ask["approval_phrase"])
|
||||
}
|
||||
if r := ask["resolved"]; r != "" {
|
||||
t.Fatalf("a fresh ask must read unresolved, got %v", r)
|
||||
}
|
||||
for _, m := range nestMessages(t, ts, id) {
|
||||
if m["msg_id"] == ask["msg_id"] {
|
||||
if m["approval_phrase"] != "reinstate Hugin" {
|
||||
t.Fatalf("GET must surface approval_phrase, got %v", m)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
t.Fatalf("ask not in GET log")
|
||||
}
|
||||
|
||||
func TestAskOwnerOnlyOwnerResolves(t *testing.T) {
|
||||
ts, _ := newTestNestServer(t)
|
||||
id, ask := askNest(t, ts)
|
||||
askID := ask["msg_id"].(string)
|
||||
|
||||
code, reply := postMsg(t, ts, id, `{"author":"Hugin","kind":"say","refers_to":"`+askID+`","text":"reinstate Hugin"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("crow reply: %d %v", code, reply)
|
||||
}
|
||||
if r := askResolved(t, ts, id, askID); r == "approved" {
|
||||
t.Fatalf("a crow's reply must never approve an ask_owner, got resolved=%v", r)
|
||||
}
|
||||
|
||||
code, yes := postMsg(t, ts, id, `{"author":"owner","kind":"say","refers_to":"`+askID+`","text":"Reinstate Hugin"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("owner yes: %d %v", code, yes)
|
||||
}
|
||||
if r := askResolved(t, ts, id, askID); r != "approved" {
|
||||
t.Fatalf("the owner's exact phrase must approve (case-insensitive), got resolved=%v", r)
|
||||
}
|
||||
found := false
|
||||
for _, m := range nestMessages(t, ts, id) {
|
||||
if m["author"] == "owner" && m["refers_to"] == askID {
|
||||
found = true
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Fatalf("the owner's reply message must be in the log")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAskOwnerWrongPhraseStaysOpen(t *testing.T) {
|
||||
ts, _ := newTestNestServer(t)
|
||||
id, ask := askNest(t, ts)
|
||||
askID := ask["msg_id"].(string)
|
||||
|
||||
code, out := postMsg(t, ts, id, `{"author":"owner","kind":"say","refers_to":"`+askID+`","text":"sounds good"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("owner reply: %d %v", code, out)
|
||||
}
|
||||
if r := askResolved(t, ts, id, askID); r != "" {
|
||||
t.Fatalf("anything but the exact phrase is not a yes, got resolved=%v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAskOwnerTimeoutFailSafe(t *testing.T) {
|
||||
ts, s := newTestNestServer(t)
|
||||
id := newNest(t, ts, "Lab", `"Hugin"`)
|
||||
stale, err := s.msgs.Append(Message{
|
||||
NestID: id,
|
||||
Kind: "ask_owner",
|
||||
Author: "Sentry",
|
||||
Text: "Inky keeps drifting off task. Retire the crow?",
|
||||
To: "owner",
|
||||
ApprovalPhrase: "retire inky",
|
||||
TaggedBy: "header",
|
||||
Timestamp: time.Now().UTC().Add(-time.Hour).Format(time.RFC3339),
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
s.askTimeout = time.Minute
|
||||
if r := askResolved(t, ts, id, stale.MsgID()); r != "rejected" {
|
||||
t.Fatalf("an ask unanswered past the window is not a yes, got resolved=%v", r)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAskOwnerCrowHeaderRetagged(t *testing.T) {
|
||||
ts, _ := newTestNestServer(t)
|
||||
id := newNest(t, ts, "Lab", `"Hugin"`)
|
||||
code, out := postMsg(t, ts, id, `{"author":"Hugin","kind":"ask_owner","to":"owner","text":"may I retire the old flyer template?"}`)
|
||||
if code != http.StatusCreated {
|
||||
t.Fatalf("crow ask_owner header: want 201 with a retag, got %d %v", code, out)
|
||||
}
|
||||
if out["kind"] != "say" {
|
||||
t.Fatalf("crows may not post ask_owner: the header must drop to the default tag, got kind=%v", out["kind"])
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user