diff --git a/nestapi_test.go b/nestapi_test.go index acef3f4..fd9d720 100644 --- a/nestapi_test.go +++ b/nestapi_test.go @@ -1,6 +1,7 @@ package main import ( + "context" "encoding/json" "net/http" "net/http/httptest" @@ -9,7 +10,6 @@ import ( "time" ) -// newTestNestServer is newTestServer plus a message store. func newTestNestServer(t *testing.T) (*httptest.Server, *Server) { t.Helper() ts, s := newTestServer(t, DefaultGuardrails(), time.Minute) @@ -21,6 +21,34 @@ func newTestNestServer(t *testing.T) (*httptest.Server, *Server) { return ts, s } +// scriptedBrain answers nest requests with a canned reply per call. +type scriptedBrain struct { + replies []string + calls int +} + +func (b *scriptedBrain) Name() string { return "scripted" } +func (b *scriptedBrain) Ask(ctx context.Context, system, prompt string) (string, int, error) { + r := "[mock] fallback" + if b.calls < len(b.replies) { + r = b.replies[b.calls] + } + b.calls++ + return r, 10, nil +} + +func newScriptedServer(t *testing.T, replies ...string) (*httptest.Server, *Server) { + t.Helper() + ts, s := newTestNestServer(t) + s.p.Flock.brain = &scriptedBrain{replies: replies} + return ts, s +} + +func nestOf(s *Server, id string) Nest { + n, _ := s.nests.Get(id) + return n +} + func newNest(t *testing.T, ts *httptest.Server, name, crows string) string { t.Helper() code, out := post(t, ts.URL+"/api/nests", `{"name":"`+name+`","crows":[`+crows+`]}`) @@ -99,8 +127,8 @@ func TestPostMessageTriggersOneResponse(t *testing.T) { for _, m := range msgs { if m["author"] == "Hugin" { replies++ - if m["kind"] != "say" && m["kind"] != "claim" && m["kind"] != "deliver" { - t.Fatalf("crow reply has a bad kind: %v", m["kind"]) + if m["kind"] != "claim" && m["kind"] != "deliver" && m["kind"] != "decline" { + t.Fatalf("crow reply must be a responsive kind, got %v", m["kind"]) } if m["refers_to"] != req["msg_id"] { t.Fatalf("crow reply must point at the request, got %v", m["refers_to"]) @@ -124,3 +152,57 @@ func TestNoIdleResponse(t *testing.T) { t.Fatalf("an unaddressed say must not summon a crow, log: %v", got) } } + +func TestParseAddressing(t *testing.T) { + members := []string{"Hugin", "Munin", "owner"} + if got := parseAddressing("Taking it, @Hugin", members); got != "Hugin" { + t.Fatalf("want Hugin, got %q", got) + } + if got := parseAddressing("hello @Nobody", members); got != "" { + t.Fatalf("unknown handle must not resolve, got %q", got) + } + if got := parseAddressing("ask @Munin.", members); got != "Munin" { + t.Fatalf("want trailing period stripped, got %q", got) + } +} + +func TestAddressedCrowMustRespond(t *testing.T) { + ts, s := newScriptedServer(t, "some idle chatter", "again not a reply") + id := newNest(t, ts, "Lab", `"Hugin"`) + s.msgs.Append(Message{NestID: id, Kind: "request", Author: "owner", To: "Hugin", Text: "count the eggs"}) + req := s.msgs.All(id)[0] + s.respondToRequest(nestOf(s, id), req) + msgs := s.msgs.All(id) + if len(msgs) != 2 || msgs[1].Kind != "flag" || msgs[1].Author != "Sentry" || msgs[1].To != "Hugin" { + t.Fatalf("two non-responsive replies must end in a Sentry flag, got %+v", msgs) + } + if msgs[1].RefersTo != req.MsgID() { + t.Fatalf("flag must point at the ignored request, got %q", msgs[1].RefersTo) + } +} + +func TestRespondedRequestNoFlag(t *testing.T) { + ts, s := newScriptedServer(t, "KIND: claim\nOn it, @owner.") + id := newNest(t, ts, "Lab", `"Hugin"`) + s.msgs.Append(Message{NestID: id, Kind: "request", Author: "owner", To: "Hugin", Text: "count the eggs"}) + req := s.msgs.All(id)[0] + s.respondToRequest(nestOf(s, id), req) + msgs := s.msgs.All(id) + if len(msgs) != 2 || msgs[1].Kind != "claim" || msgs[1].Text != "On it, @owner." { + t.Fatalf("a responsive first reply must post the claim, got %+v", msgs) + } +} + +func TestDeliverAddressesRequester(t *testing.T) { + ts, _ := newTestNestServer(t) + id := newNest(t, ts, "Lab", `"Munin"`) + _, req := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"request","text":"count the eggs","to":"Munin"}`) + code, d := post(t, ts.URL+"/api/nests/"+id+"/messages", + `{"author":"Munin","kind":"deliver","text":"three eggs","refers_to":"`+req["msg_id"].(string)+`"}`) + if code != http.StatusCreated { + t.Fatalf("deliver: %d %v", code, d) + } + if d["to"] != "owner" { + t.Fatalf("deliver must go back to the requester, got to=%v", d["to"]) + } +}