Publish Claude Code Binaries / Stage and publish binaries (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-arm64 (push) Canceled after 0s
Publish Tutti App Runtime / Build darwin-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build linux-amd64 (push) Canceled after 0s
Publish Tutti App Runtime / Build windows-amd64 (push) Canceled after 0s
Sync Tutti Agent Skills / sync (push) Canceled after 0s
Windows Agent Adapters / Test Windows agent adapters (push) Canceled after 0s
Windows Daemon Adapters / Test Windows daemon adapters (push) Canceled after 0s
Publish Tutti App Runtime / Publish runtime catalog (push) Canceled after 0s
358 lines
14 KiB
Go
358 lines
14 KiB
Go
package agenthost_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
agenthost "github.com/tutti-os/tutti/packages/agent/host"
|
|
storesqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type sqliteCanonicalStore struct {
|
|
*storesqlite.Store
|
|
}
|
|
|
|
func (sqliteCanonicalStore) InitializeRuntimeSession(context.Context, agenthost.RuntimeSessionInitialization) (storesqlite.Session, error) {
|
|
return storesqlite.Session{}, nil
|
|
}
|
|
|
|
func (s sqliteCanonicalStore) ResolveRuntimeSessionRailPlacement(
|
|
ctx context.Context,
|
|
input agenthost.ResolveRuntimeSessionRailPlacementInput,
|
|
) (*agenthost.RailPlacement, error) {
|
|
var explicit *storesqlite.RailSection
|
|
if input.RailPlacement != nil {
|
|
explicit = &storesqlite.RailSection{
|
|
Kind: string(input.RailPlacement.Kind),
|
|
ProjectPath: input.RailPlacement.ProjectPath,
|
|
Key: input.RailPlacement.SectionKey,
|
|
}
|
|
}
|
|
section, err := s.ResolveAgentSessionRailSection(ctx, storesqlite.ResolveAgentSessionRailSectionInput{
|
|
WorkspaceID: input.WorkspaceID,
|
|
AgentSessionID: input.AgentSessionID,
|
|
Cwd: input.Cwd,
|
|
RuntimeContext: input.RuntimeContext,
|
|
ExplicitPlacement: explicit,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &agenthost.RailPlacement{
|
|
Version: 1,
|
|
Kind: agenthost.RailPlacementKind(section.Kind),
|
|
ProjectPath: section.ProjectPath,
|
|
SectionKey: section.Key,
|
|
}, nil
|
|
}
|
|
|
|
func TestSessionInteractionSnapshotReadsOnlyLatestTurnFromSQLite(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
store := storesqlite.New(db, storesqlite.Options{})
|
|
if err := store.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
if _, err := store.ReportSessionState(t.Context(), storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatalf("seed session: %v", err)
|
|
}
|
|
seedTurn := func(turnID string, occurredAt int64) {
|
|
t.Helper()
|
|
if _, accepted, err := store.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: turnID,
|
|
Phase: storesqlite.TurnPhaseRunning, OccurredAtUnixMS: occurredAt,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("seed turn %s: accepted=%v err=%v", turnID, accepted, err)
|
|
}
|
|
}
|
|
seedInteraction := func(turnID, requestID, status string, occurredAt int64) {
|
|
t.Helper()
|
|
if _, result, err := store.UpsertInteraction(t.Context(), storesqlite.InteractionUpsert{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: turnID,
|
|
RequestID: requestID, Kind: storesqlite.InteractionKindQuestion, Status: status,
|
|
OccurredAtUnixMS: occurredAt,
|
|
}); err != nil || result != storesqlite.InteractionTransitionApplied {
|
|
t.Fatalf("seed interaction %s: result=%v err=%v", requestID, result, err)
|
|
}
|
|
}
|
|
|
|
seedTurn("turn-old", 10)
|
|
seedInteraction("turn-old", "old-pending", storesqlite.InteractionStatusPending, 11)
|
|
if _, accepted, err := store.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: "turn-old",
|
|
Phase: storesqlite.TurnPhaseSettled, Outcome: storesqlite.TurnOutcomeCompleted, OccurredAtUnixMS: 12,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("settle old turn: accepted=%v err=%v", accepted, err)
|
|
}
|
|
seedTurn("turn-latest", 20)
|
|
seedInteraction("turn-latest", "latest-pending", storesqlite.InteractionStatusPending, 21)
|
|
seedInteraction("turn-latest", "latest-answered", storesqlite.InteractionStatusPending, 22)
|
|
seedInteraction("turn-latest", "latest-answered", storesqlite.InteractionStatusAnswered, 23)
|
|
|
|
host := agenthost.New(agenthost.Config{CanonicalStore: sqliteCanonicalStore{Store: store}})
|
|
snapshot, err := host.GetSessionInteractionSnapshot(t.Context(), agenthost.SessionRef{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetSessionInteractionSnapshot() error = %v", err)
|
|
}
|
|
if len(snapshot.Interactions) != 2 {
|
|
t.Fatalf("Interactions = %#v, want two from latest turn", snapshot.Interactions)
|
|
}
|
|
for _, interaction := range snapshot.Interactions {
|
|
if interaction.TurnID != "turn-latest" || interaction.RequestID == "old-pending" {
|
|
t.Fatalf("snapshot leaked non-latest interaction: %#v", interaction)
|
|
}
|
|
}
|
|
if len(snapshot.PendingInteractions) != 1 || snapshot.PendingInteractions[0].RequestID != "latest-pending" {
|
|
t.Fatalf("PendingInteractions = %#v, want latest-pending only", snapshot.PendingInteractions)
|
|
}
|
|
}
|
|
|
|
func TestGetInteractionReadsExactTurnAndRequestFromSQLite(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host-interaction.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
store := storesqlite.New(db, storesqlite.Options{})
|
|
if err := store.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
if _, err := store.ReportSessionState(t.Context(), storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
Provider: "codex", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatalf("seed session: %v", err)
|
|
}
|
|
for index, turnID := range []string{"turn-old", "turn-current"} {
|
|
occurredAt := int64(index*10 + 2)
|
|
if _, accepted, err := store.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
TurnID: turnID, Phase: storesqlite.TurnPhaseRunning,
|
|
OccurredAtUnixMS: occurredAt,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("seed turn %s: accepted=%v err=%v", turnID, accepted, err)
|
|
}
|
|
if _, result, err := store.UpsertInteraction(t.Context(), storesqlite.InteractionUpsert{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
TurnID: turnID, RequestID: "shared-request",
|
|
Kind: storesqlite.InteractionKindQuestion,
|
|
Status: storesqlite.InteractionStatusPending,
|
|
OccurredAtUnixMS: occurredAt + 1,
|
|
}); err != nil || result != storesqlite.InteractionTransitionApplied {
|
|
t.Fatalf("seed interaction for %s: result=%v err=%v", turnID, result, err)
|
|
}
|
|
if index == 0 {
|
|
if _, accepted, err := store.RecordTurnTransition(
|
|
t.Context(),
|
|
storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
TurnID: turnID, Phase: storesqlite.TurnPhaseSettled,
|
|
Outcome: storesqlite.TurnOutcomeCompleted,
|
|
OccurredAtUnixMS: occurredAt + 2,
|
|
},
|
|
); err != nil || !accepted {
|
|
t.Fatalf("settle turn %s: accepted=%v err=%v", turnID, accepted, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
host := agenthost.New(agenthost.Config{
|
|
CanonicalStore: sqliteCanonicalStore{Store: store},
|
|
})
|
|
interaction, found, err := host.GetInteraction(
|
|
t.Context(),
|
|
agenthost.SessionRef{
|
|
WorkspaceID: " workspace-1 ", AgentSessionID: " session-1 ",
|
|
},
|
|
" turn-old ",
|
|
" shared-request ",
|
|
)
|
|
if err != nil || !found {
|
|
t.Fatalf("GetInteraction() found=%v error=%v", found, err)
|
|
}
|
|
if interaction.TurnID != "turn-old" ||
|
|
interaction.RequestID != "shared-request" {
|
|
t.Fatalf("GetInteraction() = %#v, want exact old-turn interaction", interaction)
|
|
}
|
|
if _, found, err := host.GetInteraction(
|
|
t.Context(),
|
|
agenthost.SessionRef{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
},
|
|
"turn-old",
|
|
"missing-request",
|
|
); err != nil || found {
|
|
t.Fatalf("GetInteraction(missing) found=%v error=%v", found, err)
|
|
}
|
|
}
|
|
|
|
func TestGetGoalStateDoesNotBootstrapMissingProjection(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host-goal-read.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
store := storesqlite.New(db, storesqlite.Options{})
|
|
if err := store.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
if _, err := store.ReportSessionState(t.Context(), storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-goal-read", Provider: "codex", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatalf("seed session: %v", err)
|
|
}
|
|
if _, found, err := store.GetSessionGoalState(t.Context(), "workspace-1", "session-goal-read"); err != nil || found {
|
|
t.Fatalf("goal before read: found=%v err=%v", found, err)
|
|
}
|
|
|
|
host := agenthost.New(agenthost.Config{
|
|
CanonicalStore: sqliteCanonicalStore{Store: store},
|
|
GoalStore: store,
|
|
})
|
|
result, err := host.GetGoalState(t.Context(), agenthost.SessionRef{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-goal-read",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("GetGoalState() error = %v", err)
|
|
}
|
|
if result.State.Revision != 0 || result.State.WorkspaceID != "" {
|
|
t.Fatalf("GetGoalState() state = %#v, want zero value", result.State)
|
|
}
|
|
if _, found, err := store.GetSessionGoalState(t.Context(), "workspace-1", "session-goal-read"); err != nil || found {
|
|
t.Fatalf("goal after read: found=%v err=%v", found, err)
|
|
}
|
|
}
|
|
|
|
func TestListSessionMessagesReadsOneCanonicalTurnPageFromSQLite(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host-messages.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
store := storesqlite.New(db, storesqlite.Options{})
|
|
if err := store.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
if _, err := store.ReportSessionState(t.Context(), storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatalf("seed session: %v", err)
|
|
}
|
|
if _, accepted, err := store.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: "turn-old",
|
|
Phase: storesqlite.TurnPhaseRunning, OccurredAtUnixMS: 2,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("seed old turn: accepted=%v err=%v", accepted, err)
|
|
}
|
|
if _, accepted, err := store.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: "turn-old",
|
|
Phase: storesqlite.TurnPhaseSettled, Outcome: storesqlite.TurnOutcomeCompleted, OccurredAtUnixMS: 3,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("settle old turn: accepted=%v err=%v", accepted, err)
|
|
}
|
|
if _, accepted, err := store.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: "turn-current",
|
|
Phase: storesqlite.TurnPhaseRunning, OccurredAtUnixMS: 4,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("seed current turn: accepted=%v err=%v", accepted, err)
|
|
}
|
|
for index, turnID := range []string{"turn-old", "turn-current", "turn-current"} {
|
|
messageID := []string{"old", "first", "second"}[index]
|
|
if _, err := store.ReportSessionMessages(t.Context(), storesqlite.SessionMessageReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", Messages: []storesqlite.MessageUpdate{{
|
|
MessageID: messageID, TurnID: turnID, Role: "assistant", Kind: "tool", OccurredAtUnixMS: int64(index + 10),
|
|
}},
|
|
}); err != nil {
|
|
t.Fatalf("seed message %s: %v", messageID, err)
|
|
}
|
|
}
|
|
|
|
host := agenthost.New(agenthost.Config{CanonicalStore: sqliteCanonicalStore{Store: store}})
|
|
page, found, err := host.ListSessionMessages(t.Context(), agenthost.SessionRef{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
}, agenthost.SessionMessageQuery{TurnID: "turn-current", Limit: 1, Order: storesqlite.MessageOrderAsc})
|
|
if err != nil || !found {
|
|
t.Fatalf("ListSessionMessages() found=%v error=%v", found, err)
|
|
}
|
|
if len(page.Messages) != 1 || page.Messages[0].MessageID != "first" || !page.HasMore || page.LatestVersion == 0 {
|
|
t.Fatalf("ListSessionMessages() page = %#v, want first current-turn message with cursor", page)
|
|
}
|
|
}
|
|
|
|
func TestListSessionTurnsReadsStableNewestFirstPagesFromSQLite(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host-turns.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
store := storesqlite.New(db, storesqlite.Options{})
|
|
if err := store.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
if _, err := store.ReportSessionState(t.Context(), storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", Provider: "codex", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatalf("seed session: %v", err)
|
|
}
|
|
for index, turnID := range []string{"turn-1", "turn-2", "turn-3"} {
|
|
startedAt := int64((index + 1) * 10)
|
|
if _, accepted, err := store.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: turnID,
|
|
Phase: storesqlite.TurnPhaseRunning, OccurredAtUnixMS: startedAt,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("seed turn %s: accepted=%v err=%v", turnID, accepted, err)
|
|
}
|
|
if index < 2 {
|
|
if _, accepted, err := store.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: turnID,
|
|
Phase: storesqlite.TurnPhaseSettled, Outcome: storesqlite.TurnOutcomeCompleted,
|
|
OccurredAtUnixMS: startedAt + 1,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("settle turn %s: accepted=%v err=%v", turnID, accepted, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
host := agenthost.New(agenthost.Config{CanonicalStore: sqliteCanonicalStore{Store: store}})
|
|
first, err := host.ListSessionTurns(t.Context(), agenthost.SessionRef{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
}, agenthost.SessionTurnQuery{Limit: 2})
|
|
if err != nil {
|
|
t.Fatalf("ListSessionTurns(first) error = %v", err)
|
|
}
|
|
if len(first.Turns) != 2 || first.Turns[0].TurnID != "turn-3" || first.Turns[1].TurnID != "turn-2" || !first.HasMore {
|
|
t.Fatalf("ListSessionTurns(first) = %#v", first)
|
|
}
|
|
|
|
second, err := host.ListSessionTurns(t.Context(), agenthost.SessionRef{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1",
|
|
}, agenthost.SessionTurnQuery{
|
|
Before: &agenthost.SessionTurnCursor{
|
|
StartedAtUnixMS: first.Turns[1].StartedAtUnixMS,
|
|
TurnID: first.Turns[1].TurnID,
|
|
},
|
|
Limit: 2,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ListSessionTurns(second) error = %v", err)
|
|
}
|
|
if len(second.Turns) != 1 || second.Turns[0].TurnID != "turn-1" || second.HasMore {
|
|
t.Fatalf("ListSessionTurns(second) = %#v", second)
|
|
}
|
|
}
|