(#23) Claim arbitration per mode #30
No files matched your search
@@ -0,0 +1,91 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func postMsg(t *testing.T, ts *httptest.Server, id, body string) (int, map[string]any) {
|
||||||
|
t.Helper()
|
||||||
|
return post(t, ts.URL+"/api/nests/"+id+"/messages", body)
|
||||||
|
}
|
||||||
|
|
||||||
|
func postRequest(t *testing.T, ts *httptest.Server, id, mode, text string) string {
|
||||||
|
t.Helper()
|
||||||
|
code, out := postMsg(t, ts, id, `{"author":"owner","kind":"request","mode":"`+mode+`","text":"`+text+`"}`)
|
||||||
|
if code != http.StatusCreated {
|
||||||
|
t.Fatalf("request: %d %v", code, out)
|
||||||
|
}
|
||||||
|
return out["msg_id"].(string)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSoloSecondClaimRefused(t *testing.T) {
|
||||||
|
ts, s := newTestNestServer(t)
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
|
||||||
|
r := postRequest(t, ts, id, "solo", "count the eggs")
|
||||||
|
|
||||||
|
code1, c1 := postMsg(t, ts, id, `{"author":"Hugin","kind":"claim","refers_to":"`+r+`","text":"on it"}`)
|
||||||
|
if code1 != http.StatusCreated || c1["to"] != "owner" {
|
||||||
|
t.Fatalf("first claim should win and address requester: %d %v", code1, c1)
|
||||||
|
}
|
||||||
|
code2, out := postMsg(t, ts, id, `{"author":"Munin","kind":"claim","refers_to":"`+r+`","text":"mine too"}`)
|
||||||
|
if code2 != http.StatusConflict {
|
||||||
|
t.Fatalf("second solo claim must be refused 409, got %d %v", code2, out)
|
||||||
|
}
|
||||||
|
if msg, _ := out["message"].(string); !strings.Contains(msg, "Hugin") {
|
||||||
|
t.Fatalf("refusal must name the owner Hugin, got %q", msg)
|
||||||
|
}
|
||||||
|
if len(s.msgs.All(id)) != 2 { // request + Hugin's claim, not Munin's
|
||||||
|
t.Fatalf("refused claim must not be stored: %+v", s.msgs.All(id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestConspiracyMultipleClaims(t *testing.T) {
|
||||||
|
ts, s := newTestNestServer(t)
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
|
||||||
|
r := postRequest(t, ts, id, "conspiracy", "is this plan risky")
|
||||||
|
|
||||||
|
for _, c := range []string{"Hugin", "Munin"} {
|
||||||
|
code, out := postMsg(t, ts, id, `{"author":"`+c+`","kind":"claim","refers_to":"`+r+`","text":"lens `+c+`"}`)
|
||||||
|
if code != http.StatusCreated || out["refers_to"] != r {
|
||||||
|
t.Fatalf("conspiracy claim by %s refused: %d %v", c, code, out)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
claims := 0
|
||||||
|
for _, m := range s.msgs.All(id) {
|
||||||
|
if m.Kind == "claim" && m.RefersTo == r {
|
||||||
|
claims++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if claims != 2 {
|
||||||
|
t.Fatalf("want 2 conspiracy claims stored, got %d", claims)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSplitSubtaskFirstClaimWins(t *testing.T) {
|
||||||
|
ts, _ := newTestNestServer(t)
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin","Munin"`)
|
||||||
|
r := postRequest(t, ts, id, "split", "build and test")
|
||||||
|
subOf := func(text string) string {
|
||||||
|
code, out := postMsg(t, ts, id, `{"author":"owner","kind":"request","mode":"solo","text":"`+text+`","refers_to":"`+r+`"}`)
|
||||||
|
if code != http.StatusCreated {
|
||||||
|
t.Fatalf("subtask: %d %v", code, out)
|
||||||
|
}
|
||||||
|
return out["msg_id"].(string)
|
||||||
|
}
|
||||||
|
a := subOf("build")
|
||||||
|
b := subOf("test")
|
||||||
|
// same subtask: second claim refused
|
||||||
|
if code, _ := postMsg(t, ts, id, `{"author":"Hugin","kind":"claim","refers_to":"`+a+`","text":"b1"}`); code != http.StatusCreated {
|
||||||
|
t.Fatalf("first subtask claim: %d", code)
|
||||||
|
}
|
||||||
|
if code, _ := postMsg(t, ts, id, `{"author":"Munin","kind":"claim","refers_to":"`+a+`","text":"b2"}`); code != http.StatusConflict {
|
||||||
|
t.Fatalf("second claim on same subtask must 409, got %d", code)
|
||||||
|
}
|
||||||
|
// different subtask: allowed
|
||||||
|
if code, _ := postMsg(t, ts, id, `{"author":"Munin","kind":"claim","refers_to":"`+b+`","text":"t1"}`); code != http.StatusCreated {
|
||||||
|
t.Fatalf("claim on other subtask: %d", code)
|
||||||
|
}
|
||||||
|
}
|
||||||
+11
@@ -132,6 +132,17 @@ func (s *Server) postNestMessage(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Mode (Epic C, same rule shape as kind): Jev first, header escape, "" when unsure.
|
// Mode (Epic C, same rule shape as kind): Jev first, header escape, "" when unsure.
|
||||||
mode, modeBy := s.tagMode(kind, body.Mode, body.Text)
|
mode, modeBy := s.tagMode(kind, body.Mode, body.Text)
|
||||||
|
|
||||||
|
// Claim arbitration (#5, Epic C): first claim wins unless the referent is a
|
||||||
|
// conspiracy, which expects multiple claims.
|
||||||
|
if kind == "claim" && body.RefersTo != "" {
|
||||||
|
if origin, found := s.msgs.ByMsgID(body.RefersTo); found && origin.Mode != "conspiracy" {
|
||||||
|
if owner, claimed := s.msgs.FirstClaim(origin.NestID, origin.MsgID()); claimed {
|
||||||
|
httpError(w, http.StatusConflict, origin.MsgID()+" is claimed by "+owner+"; review it instead of redoing it.")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
msg := Message{
|
msg := Message{
|
||||||
NestID: nestID,
|
NestID: nestID,
|
||||||
Kind: kind,
|
Kind: kind,
|
||||||
|
|||||||
+13
@@ -119,6 +119,19 @@ func (s *MessageStore) ByMsgID(msgID string) (Message, bool) {
|
|||||||
return Message{}, false
|
return Message{}, false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// FirstClaim returns the author of the first claim against a message, and
|
||||||
|
// whether any claim exists.
|
||||||
|
func (s *MessageStore) FirstClaim(nestID, referentID string) (string, bool) {
|
||||||
|
s.mu.Lock()
|
||||||
|
defer s.mu.Unlock()
|
||||||
|
for _, m := range s.log[nestID] {
|
||||||
|
if m.Kind == "claim" && m.RefersTo == referentID {
|
||||||
|
return m.Author, true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return "", false
|
||||||
|
}
|
||||||
|
|
||||||
func (s *MessageStore) save(nestID string) error {
|
func (s *MessageStore) save(nestID string) error {
|
||||||
if err := os.MkdirAll(s.dir, 0o755); err != nil {
|
if err := os.MkdirAll(s.dir, 0o755); err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
Reference in new issue
Block a user