first commit
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user