(#13) Live models deliver in the nest #20
No files matched your search
@@ -4,6 +4,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -55,3 +56,23 @@ func (l *FlightLog) Write(e Event) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *FlightLog) Close() error { return l.f.Close() }
|
func (l *FlightLog) Close() error { return l.f.Close() }
|
||||||
|
|
||||||
|
// ReadEvents parses a flight log file back into events, in the order written.
|
||||||
|
func ReadEvents(path string) ([]Event, error) {
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var out []Event
|
||||||
|
for _, line := range strings.Split(string(data), "\n") {
|
||||||
|
if strings.TrimSpace(line) == "" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var e Event
|
||||||
|
if err := json.Unmarshal([]byte(line), &e); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, e)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,97 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
// newLiveNestServer builds the server over the real brain from env (NewBrain)
|
||||||
|
// instead of the mock. Requires an API key; callers must skip first.
|
||||||
|
func newLiveNestServer(t *testing.T) (*Server, string) {
|
||||||
|
t.Helper()
|
||||||
|
dir := t.TempDir()
|
||||||
|
ledger, _ := LoadLedger(filepath.Join(dir, "ledger.json"))
|
||||||
|
flog, err := OpenFlightLog(filepath.Join(dir, "flight.jsonl"))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Cleanup(func() { flog.Close() })
|
||||||
|
nests, _ := LoadNests(filepath.Join(dir, "nests.json"))
|
||||||
|
msgs, _ := LoadMessageStore(filepath.Join(dir, "messages"))
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
approver := NewWebApprover(time.Minute)
|
||||||
|
p := &Pipeline{Flock: NewFlock(NewBrain(), DefaultGuardrails(), flog), Ledger: ledger, Log: flog, G: DefaultGuardrails(), Approver: approver}
|
||||||
|
s := NewServer(ctx, p, approver, nests).WithMessages(msgs)
|
||||||
|
ts := httptest.NewServer(s.Handler())
|
||||||
|
t.Cleanup(func() { cancel(); ts.Close() })
|
||||||
|
return s, filepath.Join(dir, "flight.jsonl")
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLiveCrowDeliversInNest(t *testing.T) {
|
||||||
|
if os.Getenv("OPENROUTER_API_KEY") == "" && os.Getenv("ANTHROPIC_API_KEY") == "" {
|
||||||
|
t.Skip("no model key")
|
||||||
|
}
|
||||||
|
s, logPath := newLiveNestServer(t)
|
||||||
|
nest, err := s.nests.Create("Live Lab", []string{"Hugin"})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
req, err := s.msgs.Append(Message{NestID: nest.ID, Kind: "request", Author: "owner", To: "Hugin", Text: "What is the plural of crow?", Timestamp: time.Now().UTC().Format(time.RFC3339)})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
// Synchronous call, like the endpoint's goroutine would.
|
||||||
|
s.respondToRequest(nest, req)
|
||||||
|
|
||||||
|
msgs := s.msgs.All(nest.ID)
|
||||||
|
var reply *Message
|
||||||
|
for i := range msgs {
|
||||||
|
if msgs[i].Author == "Hugin" {
|
||||||
|
reply = &msgs[i]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
events, err := ReadEvents(logPath)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if reply == nil {
|
||||||
|
// No crow message at all: either a flight failure (429/timeout: the
|
||||||
|
// known free-model instability -> skip) or nothing was even attempted.
|
||||||
|
flightFailed := false
|
||||||
|
for _, e := range events {
|
||||||
|
if e.Kind == "flight" && e.Error != "" {
|
||||||
|
flightFailed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flightFailed {
|
||||||
|
t.Skip("upstream model failed (known free-model instability); the flag path is covered by #11")
|
||||||
|
}
|
||||||
|
t.Fatalf("no Hugin reply after a request, log: %+v", msgs)
|
||||||
|
}
|
||||||
|
kind := reply.Kind
|
||||||
|
if kind != "deliver" && kind != "claim" && kind != "decline" {
|
||||||
|
t.Fatalf("live crow reply must be responsive, got %q (%s)", kind, reply.Text)
|
||||||
|
}
|
||||||
|
if reply.To != "owner" {
|
||||||
|
t.Fatalf("deliver must answer the requester, got to=%q", reply.To)
|
||||||
|
}
|
||||||
|
if reply.RefersTo != req.MsgID() {
|
||||||
|
t.Fatalf("reply must point at the request, got %q", reply.RefersTo)
|
||||||
|
}
|
||||||
|
// The call went through Flock.ask, so the flight log has Hugin's flight.
|
||||||
|
flights := 0
|
||||||
|
for _, e := range events {
|
||||||
|
if e.Crow == "Hugin" && e.Model != "" && !strings.Contains(strings.ToLower(e.Model), "mock") {
|
||||||
|
flights++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if flights == 0 {
|
||||||
|
t.Fatalf("want a logged non-mock Hugin flight, events: %+v", events)
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in new issue
Block a user