test(mode): red for #22 — modeQuestions, ClassifyMode guardrail wrapper, record fields
This commit is contained in:
1 parent
7b042b77cc
commit
5b3701d6e2
2 files changed
+139
No files matched your search
@@ -58,6 +58,9 @@ Every nest event carries `nest_id`, `ts` (RFC 3339) and one message record. The
|
|||||||
| `to` | string | The @-addressed member, or `""`. `deliver`/`claim` default to the requester they answer. |
|
| `to` | string | The @-addressed member, or `""`. `deliver`/`claim` default to the requester they answer. |
|
||||||
| `refers_to` | string | The `msg_id` this responds to (claim/deliver/nudge point at their request/expectation), or `""`. |
|
| `refers_to` | string | The `msg_id` this responds to (claim/deliver/nudge point at their request/expectation), or `""`. |
|
||||||
| `tagged_by` | string | How `kind` was decided: `"jev"`, `"header"`, `"disagreement"` (Jev overrode the crow's header) or `"default"` (nothing fit, became `say`). |
|
| `tagged_by` | string | How `kind` was decided: `"jev"`, `"header"`, `"disagreement"` (Jev overrode the crow's header) or `"default"` (nothing fit, became `say`). |
|
||||||
|
| `mode` | string | On a `request` only: how it should be taken up — `solo`, `conspiracy` (several crows, one task, distinct readings), `split`, or `""` (not yet classified). Tagged Jev-first with a `mode` header escape (`jev_test.go`, `Flock.ClassifyMode`), same rule shape as `kind`. |
|
||||||
|
| `mode_tagged_by` | string | How `mode` was decided, same four sources as `tagged_by`. |
|
||||||
|
| `invited` | int | Conspiracy only: how many crows the request expects to take it up (default from the `MurderSize` cap). |
|
||||||
|
|
||||||
### SSE event
|
### SSE event
|
||||||
|
|
||||||
|
|||||||
+136
@@ -0,0 +1,136 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func fakeModeJev(t *testing.T, choice string, conf float64) *JevClient {
|
||||||
|
t.Helper()
|
||||||
|
return fakeJevFor(t, map[string]struct {
|
||||||
|
choice string
|
||||||
|
conf float64
|
||||||
|
}{"mode": {choice, conf}})
|
||||||
|
}
|
||||||
|
|
||||||
|
// fakeJevFor answers only the named question keys; any other question in the
|
||||||
|
// same request gets an empty answer (so callers fall back to headers).
|
||||||
|
func fakeJevFor(t *testing.T, answers map[string]struct {
|
||||||
|
choice string
|
||||||
|
conf float64
|
||||||
|
}) *JevClient {
|
||||||
|
t.Helper()
|
||||||
|
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var body struct {
|
||||||
|
Questions map[string]any `json:"questions"`
|
||||||
|
}
|
||||||
|
json.NewDecoder(r.Body).Decode(&body)
|
||||||
|
out := map[string]any{}
|
||||||
|
for q := range body.Questions {
|
||||||
|
if a, ok := answers[q]; ok {
|
||||||
|
out[q] = map[string]any{"type": "choice", "choice": a.choice, "confidence": a.conf,
|
||||||
|
"probabilities": map[string]float64{a.choice: a.conf}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
json.NewEncoder(w).Encode(map[string]any{"model": "jev-1.14.0", "answers": out,
|
||||||
|
"usage": map[string]int{"input_tokens": 60, "output_tokens": 0}})
|
||||||
|
}))
|
||||||
|
t.Cleanup(srv.Close)
|
||||||
|
return &JevClient{url: srv.URL, key: "tk", model: "jev-latest", threshold: 0.7, client: srv.Client()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModeQuestionsExist(t *testing.T) {
|
||||||
|
m := modeQuestions["mode"].(map[string]any)
|
||||||
|
if m["type"] != "choice" {
|
||||||
|
t.Fatalf("mode must be a choice question, got %v", m["type"])
|
||||||
|
}
|
||||||
|
crit := m["criteria"].(map[string]string)
|
||||||
|
for _, k := range []string{"solo", "conspiracy", "split", "other"} {
|
||||||
|
if _, ok := crit[k]; !ok {
|
||||||
|
t.Errorf("mode criteria missing %q escape/class: %v", k, crit)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassifyMode(t *testing.T) {
|
||||||
|
dir := t.TempDir()
|
||||||
|
flog, err := OpenFlightLog(filepath.Join(dir, "flight.jsonl"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
defer flog.Close()
|
||||||
|
f := NewFlock(MockBrain{}, DefaultGuardrails(), flog)
|
||||||
|
f.SetClassifier(fakeModeJev(t, "conspiracy", 0.9))
|
||||||
|
got, ok := f.ClassifyMode(context.Background(), "Should we launch a CLI or a web app first?")
|
||||||
|
if !ok || got != "conspiracy" {
|
||||||
|
t.Fatalf("want conspiracy ok, got %q ok=%v", got, ok)
|
||||||
|
}
|
||||||
|
evs, err := ReadEvents(filepath.Join(dir, "flight.jsonl"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
modes := 0
|
||||||
|
for _, e := range evs {
|
||||||
|
if e.Kind == "mode" && e.Mode == "conspiracy" && e.Prob != nil {
|
||||||
|
modes++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if modes != 1 {
|
||||||
|
t.Fatalf("want one logged mode verdict with probs, events: %+v", evs)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestClassifyModeUnsureNotOk(t *testing.T) {
|
||||||
|
f := NewFlock(MockBrain{}, DefaultGuardrails(), nil)
|
||||||
|
f.SetClassifier(fakeModeJev(t, "split", 0.4))
|
||||||
|
if got, ok := f.ClassifyMode(context.Background(), "hi"); ok || got != "" {
|
||||||
|
t.Fatalf("below-band confidence must not be ok, got %q ok=%v", got, ok)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestMessageModeFields(t *testing.T) {
|
||||||
|
m := Message{NestID: "lab", Kind: "request", Mode: "conspiracy", Invited: 3}
|
||||||
|
if m.Mode != "conspiracy" || m.Invited != 3 {
|
||||||
|
t.Fatalf("Message must carry mode + invited, got %+v", m)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRequestJevMode(t *testing.T) {
|
||||||
|
ts, s := newTestNestServer(t)
|
||||||
|
s.p.Flock.SetClassifier(fakeModeJev(t, "conspiracy", 0.9))
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin"`)
|
||||||
|
code, req := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"request","text":"should we launch a CLI or web app"}`)
|
||||||
|
if code != http.StatusCreated || req["mode"] != "conspiracy" || req["mode_tagged_by"] != "jev" {
|
||||||
|
t.Fatalf("want conspiracy/jev, got %d %v", code, req)
|
||||||
|
}
|
||||||
|
stored := s.msgs.All(id)[0]
|
||||||
|
if stored.Mode != "conspiracy" || stored.ModeTagged != "jev" {
|
||||||
|
t.Fatalf("record not persisted with mode: %+v", stored)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestPostRequestModeHeaderFallback(t *testing.T) {
|
||||||
|
ts, _ := newTestNestServer(t) // no Jev set
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin"`)
|
||||||
|
code, req := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"request","mode":"split","text":"do both parts"}`)
|
||||||
|
if code != http.StatusCreated || req["mode"] != "split" || req["mode_tagged_by"] != "header" {
|
||||||
|
t.Fatalf("want split/header, got %d %v", code, req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestSayHasNoMode(t *testing.T) {
|
||||||
|
ts, s := newTestNestServer(t)
|
||||||
|
s.p.Flock.SetClassifier(fakeModeJev(t, "conspiracy", 0.9))
|
||||||
|
id := newNest(t, ts, "Lab", `"Hugin"`)
|
||||||
|
code, say := post(t, ts.URL+"/api/nests/"+id+"/messages", `{"author":"owner","kind":"say","text":"nice weather"}`)
|
||||||
|
if code != http.StatusCreated {
|
||||||
|
t.Fatalf("say: %d", code)
|
||||||
|
}
|
||||||
|
if _, ok := say["mode"]; ok {
|
||||||
|
t.Fatalf("a say must not carry a mode: %v", say)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user