first commit

This commit is contained in:
2026-08-14 23:41:57 +08:00
commit 086803a8dd
471 changed files with 91938 additions and 0 deletions
+156
View File
@@ -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)
}
}