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"]) } }