157 lines
5.6 KiB
Go
157 lines
5.6 KiB
Go
package headless
|
|
|
|
// Tests for headless session persistence and resume (session id in stream-json
|
|
// + --resume for headless runs). openHeadlessSession/persist are exercised
|
|
// directly against an isolated PIGO_HOME so a headless run's session round-trips
|
|
// without spawning a provider.
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"github.com/smallnest/pigo/internal/agentcore"
|
|
)
|
|
|
|
func textUser(s string) agentcore.UserMessage {
|
|
return agentcore.UserMessage{RoleField: agentcore.RoleUser, Content: agentcore.ContentList{agentcore.NewTextContent(s)}}
|
|
}
|
|
|
|
func textAssistant(s string) agentcore.AssistantMessage {
|
|
return agentcore.AssistantMessage{RoleField: agentcore.RoleAssistant, Content: agentcore.ContentList{agentcore.NewTextContent(s)}}
|
|
}
|
|
|
|
// TestOpenHeadlessSessionFresh verifies a fresh headless session gets a new id
|
|
// and empty prior messages, and that persist writes the run's messages so they
|
|
// can be resumed.
|
|
func TestOpenHeadlessSessionFresh(t *testing.T) {
|
|
t.Setenv("PIGO_HOME", t.TempDir())
|
|
|
|
prior, hs, err := openHeadlessSession("", "faux-model", "faux", "sys prompt")
|
|
if err != nil {
|
|
t.Fatalf("openHeadlessSession fresh: %v", err)
|
|
}
|
|
if len(prior) != 0 {
|
|
t.Errorf("fresh session must have no prior messages, got %d", len(prior))
|
|
}
|
|
if hs.header.ID == "" {
|
|
t.Fatal("fresh session must have a non-empty id")
|
|
}
|
|
if hs.header.SystemPrompt != "sys prompt" {
|
|
t.Errorf("header SystemPrompt = %q, want the passed prompt", hs.header.SystemPrompt)
|
|
}
|
|
|
|
// Simulate a completed run: prompt + assistant reply.
|
|
agentCtx := &agentcore.AgentContext{Messages: agentcore.MessageList{textUser("1+1=?"), textAssistant("2")}}
|
|
if err := hs.persist(agentCtx); err != nil {
|
|
t.Fatalf("persist: %v", err)
|
|
}
|
|
|
|
// The session must now be loadable with both messages.
|
|
_, msgs, err := hs.store.Load(hs.header.ID)
|
|
if err != nil {
|
|
t.Fatalf("Load persisted session: %v", err)
|
|
}
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("persisted session has %d messages, want 2", len(msgs))
|
|
}
|
|
}
|
|
|
|
// TestOpenHeadlessSessionResume verifies that resuming seeds the prior messages
|
|
// and that a subsequent run appends only the new tail as a branch, so the
|
|
// session grows rather than being rewritten.
|
|
func TestOpenHeadlessSessionResume(t *testing.T) {
|
|
t.Setenv("PIGO_HOME", t.TempDir())
|
|
|
|
// First run: create and persist a session.
|
|
_, hs1, err := openHeadlessSession("", "faux-model", "faux", "sys")
|
|
if err != nil {
|
|
t.Fatalf("first openHeadlessSession: %v", err)
|
|
}
|
|
ctx1 := &agentcore.AgentContext{Messages: agentcore.MessageList{textUser("first"), textAssistant("reply1")}}
|
|
if err := hs1.persist(ctx1); err != nil {
|
|
t.Fatalf("first persist: %v", err)
|
|
}
|
|
sessID := hs1.header.ID
|
|
|
|
// Second run: resume the session id.
|
|
prior, hs2, err := openHeadlessSession(sessID, "faux-model", "faux", "sys")
|
|
if err != nil {
|
|
t.Fatalf("resume openHeadlessSession: %v", err)
|
|
}
|
|
if len(prior) != 2 {
|
|
t.Fatalf("resume must seed %d prior messages, got %d", 2, len(prior))
|
|
}
|
|
if hs2.header.ID != sessID {
|
|
t.Errorf("resumed session id = %q, want %q", hs2.header.ID, sessID)
|
|
}
|
|
if hs2.persisted != 2 {
|
|
t.Errorf("resumed persisted cursor = %d, want 2", hs2.persisted)
|
|
}
|
|
|
|
// A second turn appends its new tail.
|
|
ctx2 := &agentcore.AgentContext{Messages: append(prior, textUser("second"), textAssistant("reply2"))}
|
|
if err := hs2.persist(ctx2); err != nil {
|
|
t.Fatalf("second persist: %v", err)
|
|
}
|
|
_, msgs, err := hs2.store.Load(sessID)
|
|
if err != nil {
|
|
t.Fatalf("Load after second turn: %v", err)
|
|
}
|
|
if len(msgs) != 4 {
|
|
t.Fatalf("session after two turns has %d messages, want 4", len(msgs))
|
|
}
|
|
}
|
|
|
|
// TestHeadlessPersistNoop verifies persist is a no-op (no error, no growth) when
|
|
// the run produced nothing new past what was already persisted.
|
|
func TestHeadlessPersistNoop(t *testing.T) {
|
|
t.Setenv("PIGO_HOME", t.TempDir())
|
|
_, hs, err := openHeadlessSession("", "m", "p", "s")
|
|
if err != nil {
|
|
t.Fatalf("openHeadlessSession: %v", err)
|
|
}
|
|
ctx := &agentcore.AgentContext{Messages: agentcore.MessageList{textUser("x"), textAssistant("y")}}
|
|
if err := hs.persist(ctx); err != nil {
|
|
t.Fatalf("first persist: %v", err)
|
|
}
|
|
// Persisting again with no new messages must not error and must not duplicate.
|
|
if err := hs.persist(ctx); err != nil {
|
|
t.Fatalf("noop persist: %v", err)
|
|
}
|
|
_, msgs, err := hs.store.Load(hs.header.ID)
|
|
if err != nil {
|
|
t.Fatalf("Load: %v", err)
|
|
}
|
|
if len(msgs) != 2 {
|
|
t.Fatalf("noop persist changed message count to %d, want 2", len(msgs))
|
|
}
|
|
}
|
|
|
|
// TestHeadlessPersistCompactionShrink verifies persist tolerates the context
|
|
// being rebuilt to fewer messages than were on disk before the run (mid-run
|
|
// compaction replaces agentCtx.Messages). The persisted cursor is clamped so
|
|
// the tail slice stays in bounds rather than panicking.
|
|
func TestHeadlessPersistCompactionShrink(t *testing.T) {
|
|
t.Setenv("PIGO_HOME", t.TempDir())
|
|
_, hs, err := openHeadlessSession("", "m", "p", "s")
|
|
if err != nil {
|
|
t.Fatalf("openHeadlessSession: %v", err)
|
|
}
|
|
// Persist four messages, advancing the cursor to 4.
|
|
ctx := &agentcore.AgentContext{Messages: agentcore.MessageList{textUser("a"), textAssistant("b"), textUser("c"), textAssistant("d")}}
|
|
if err := hs.persist(ctx); err != nil {
|
|
t.Fatalf("first persist: %v", err)
|
|
}
|
|
if hs.persisted != 4 {
|
|
t.Fatalf("cursor = %d, want 4", hs.persisted)
|
|
}
|
|
// Simulate compaction: the context is rebuilt to fewer messages than the
|
|
// cursor. persist must not panic on the out-of-range slice.
|
|
ctx.Messages = agentcore.MessageList{textAssistant("summary"), textUser("e")}
|
|
if err := hs.persist(ctx); err != nil {
|
|
t.Fatalf("persist after compaction shrink: %v", err)
|
|
}
|
|
if hs.persisted != 2 {
|
|
t.Errorf("cursor after clamp = %d, want 2", hs.persisted)
|
|
}
|
|
}
|