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
360 lines
14 KiB
Go
360 lines
14 KiB
Go
package agenthost_test
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"errors"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
agenthost "github.com/tutti-os/tutti/packages/agent/host"
|
|
storesqlite "github.com/tutti-os/tutti/packages/agent/store-sqlite"
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
type workspaceStoreClock struct{ value time.Time }
|
|
|
|
func (c workspaceStoreClock) Now() time.Time { return c.value }
|
|
|
|
type workspaceStoreProjectPaths []string
|
|
|
|
func (paths workspaceStoreProjectPaths) ProjectPaths(context.Context, storesqlite.Querier) ([]string, error) {
|
|
return append([]string(nil), paths...), nil
|
|
}
|
|
|
|
type workspaceStoreObserver struct{ deltas []agenthost.CommittedDelta }
|
|
|
|
func (o *workspaceStoreObserver) ObserveCommitted(_ context.Context, delta agenthost.CommittedDelta) error {
|
|
o.deltas = append(o.deltas, delta)
|
|
return nil
|
|
}
|
|
|
|
type runtimeSessionInitializationObserver struct {
|
|
runtime agenthost.ProviderRuntimeSession
|
|
persisted storesqlite.Session
|
|
}
|
|
|
|
type runtimeSessionInitializationPolicy struct{ calls int }
|
|
|
|
type workspaceStoreForkRuntime struct{}
|
|
|
|
func (workspaceStoreForkRuntime) ResolveSessionFork(
|
|
_ context.Context,
|
|
_ agenthost.ProviderRuntimeSession,
|
|
) (agenthost.SessionForkDriverDescriptor, error) {
|
|
return agenthost.SessionForkDriverDescriptor{
|
|
Kind: "codex",
|
|
Version: "1",
|
|
ThroughTurn: true,
|
|
StateBindingMode: agenthost.SessionForkStateBindingProviderOwned,
|
|
}, nil
|
|
}
|
|
|
|
func (workspaceStoreForkRuntime) CanForkProviderTurn(
|
|
context.Context,
|
|
agenthost.RuntimeProviderTurnForkabilityInput,
|
|
) (bool, error) {
|
|
return true, nil
|
|
}
|
|
|
|
func (workspaceStoreForkRuntime) ForkSession(
|
|
_ context.Context,
|
|
_ agenthost.RuntimeSessionForkInput,
|
|
) (agenthost.RuntimeSessionForkResult, error) {
|
|
return agenthost.RuntimeSessionForkResult{}, errors.New("unexpected fork dispatch")
|
|
}
|
|
|
|
func (p *runtimeSessionInitializationPolicy) NormalizeRuntimeSessionInitialization(
|
|
_ context.Context,
|
|
session agenthost.ProviderRuntimeSession,
|
|
) (agenthost.ProviderRuntimeSession, error) {
|
|
p.calls++
|
|
session.AgentTargetID = "canonical-target"
|
|
return session, nil
|
|
}
|
|
|
|
func (o *runtimeSessionInitializationObserver) ObserveRuntimeSessionInitialized(
|
|
_ context.Context,
|
|
runtime agenthost.ProviderRuntimeSession,
|
|
persisted storesqlite.Session,
|
|
) {
|
|
o.runtime = runtime
|
|
o.persisted = persisted
|
|
}
|
|
|
|
func TestSQLiteWorkspaceStoreInitializesCanonicalRuntimeSession(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host-store.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
canonical := storesqlite.New(db, storesqlite.Options{
|
|
ProjectPaths: workspaceStoreProjectPaths{"/workspace/app"},
|
|
})
|
|
if err := canonical.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
observer := &workspaceStoreObserver{}
|
|
initializationObserver := &runtimeSessionInitializationObserver{}
|
|
initializationPolicy := &runtimeSessionInitializationPolicy{}
|
|
store := &agenthost.SQLiteWorkspaceStore{
|
|
StoreForWorkspace: func(workspaceID string) *storesqlite.Store {
|
|
if workspaceID != "workspace-1" {
|
|
return nil
|
|
}
|
|
return canonical
|
|
},
|
|
CurrentUserID: func() string { return " user-1 " },
|
|
Clock: workspaceStoreClock{value: time.UnixMilli(1234)},
|
|
Observer: observer,
|
|
InitializationPolicy: initializationPolicy,
|
|
InitializationObserver: initializationObserver,
|
|
}
|
|
|
|
persisted, err := store.InitializeRuntimeSession(t.Context(), agenthost.RuntimeSessionInitialization{
|
|
Session: agenthost.ProviderRuntimeSession{
|
|
ID: "session-1", WorkspaceID: "workspace-1", AgentTargetID: "target-1", Provider: "codex",
|
|
Visible: true, Provisional: true, RuntimeContext: map[string]any{"source": "create"},
|
|
Settings: &agenthost.ComposerSettings{Model: "gpt-5.6", ReasoningEffort: "ultra", Speed: "standard"},
|
|
},
|
|
RailPlacement: &agenthost.RailPlacement{
|
|
Version: 1,
|
|
Kind: agenthost.RailPlacementKindProject,
|
|
ProjectPath: "/workspace/app",
|
|
SectionKey: "project:/workspace/app",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("InitializeRuntimeSession() error = %v", err)
|
|
}
|
|
if persisted.ID != "session-1" || persisted.UserID != "user-1" || persisted.Provider != "codex" || persisted.AgentTargetID != "canonical-target" {
|
|
t.Fatalf("persisted session = %#v", persisted)
|
|
}
|
|
if persisted.LastEventUnixMS != 1234 || persisted.Settings["reasoningEffort"] != "ultra" || persisted.Settings["speed"] != "standard" {
|
|
t.Fatalf("persisted canonical fields = %#v", persisted)
|
|
}
|
|
wantRailKey := storesqlite.RailSectionKeyForProject("/workspace/app")
|
|
if persisted.RailSectionKey != wantRailKey {
|
|
t.Fatalf("persisted rail section key = %q, want %q", persisted.RailSectionKey, wantRailKey)
|
|
}
|
|
if persisted.Metadata.Visible {
|
|
t.Fatalf("provisional session visibility = true, want false")
|
|
}
|
|
if len(observer.deltas) != 1 || len(observer.deltas[0].ProjectionDirty) == 0 || len(observer.deltas[0].ViewsInvalidated) != 1 {
|
|
t.Fatalf("commit deltas = %#v", observer.deltas)
|
|
}
|
|
if initializationObserver.runtime.ID != "session-1" || initializationObserver.persisted.ID != "session-1" {
|
|
t.Fatalf("initialization projection = %#v", initializationObserver)
|
|
}
|
|
if initializationPolicy.calls != 1 {
|
|
t.Fatalf("initialization policy calls = %d, want 1", initializationPolicy.calls)
|
|
}
|
|
|
|
_, err = store.InitializeRuntimeSession(t.Context(), agenthost.RuntimeSessionInitialization{
|
|
Session: agenthost.ProviderRuntimeSession{
|
|
ID: "session-1", WorkspaceID: "workspace-1", AgentTargetID: "target-1", Provider: "codex",
|
|
},
|
|
RailPlacement: &agenthost.RailPlacement{
|
|
Version: 1, Kind: agenthost.RailPlacementKindConversations, SectionKey: "conversations",
|
|
},
|
|
})
|
|
if !errors.Is(err, agenthost.ErrRailPlacementConflict) {
|
|
t.Fatalf("conflicting rail placement error = %v", err)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteWorkspaceStoreResolvesRuntimeRailPlacementFromPreparedCWD(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host-rail-resolution.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
canonical := storesqlite.New(db, storesqlite.Options{
|
|
ProjectPaths: workspaceStoreProjectPaths{"/workspace/app"},
|
|
})
|
|
if err := canonical.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
store := &agenthost.SQLiteWorkspaceStore{
|
|
StoreForWorkspace: func(workspaceID string) *storesqlite.Store {
|
|
if workspaceID != "workspace-1" {
|
|
return nil
|
|
}
|
|
return canonical
|
|
},
|
|
}
|
|
|
|
placement, err := store.ResolveRuntimeSessionRailPlacement(t.Context(), agenthost.ResolveRuntimeSessionRailPlacementInput{
|
|
WorkspaceID: "workspace-1",
|
|
AgentSessionID: "session-1",
|
|
Cwd: "/workspace/app/pkg",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ResolveRuntimeSessionRailPlacement() error = %v", err)
|
|
}
|
|
wantKey := storesqlite.RailSectionKeyForProject("/workspace/app")
|
|
if placement.Version != 1 || placement.Kind != agenthost.RailPlacementKindProject ||
|
|
placement.ProjectPath != storesqlite.NormalizeProjectPath("/workspace/app") ||
|
|
placement.SectionKey != wantKey {
|
|
t.Fatalf("resolved placement = %#v, want project %q", placement, wantKey)
|
|
}
|
|
|
|
_, err = store.ResolveRuntimeSessionRailPlacement(t.Context(), agenthost.ResolveRuntimeSessionRailPlacementInput{
|
|
WorkspaceID: "workspace-1",
|
|
AgentSessionID: "session-stale-project",
|
|
Cwd: "/workspace/deleted-project",
|
|
RailPlacement: &agenthost.RailPlacement{
|
|
Version: 1, Kind: agenthost.RailPlacementKindProject,
|
|
ProjectPath: "/workspace/deleted-project",
|
|
},
|
|
})
|
|
if !errors.Is(err, agenthost.ErrRailPlacementConflict) {
|
|
t.Fatalf("stale explicit project error = %v, want %v", err, agenthost.ErrRailPlacementConflict)
|
|
}
|
|
|
|
authoritative, err := store.ResolveRuntimeSessionRailPlacement(t.Context(), agenthost.ResolveRuntimeSessionRailPlacementInput{
|
|
WorkspaceID: "workspace-1",
|
|
AgentSessionID: "session-authoritative-project",
|
|
Cwd: "/workspace/caller-project",
|
|
RailPlacementAuthoritative: true,
|
|
RailPlacement: &agenthost.RailPlacement{
|
|
Version: 1, Kind: agenthost.RailPlacementKindProject,
|
|
ProjectPath: "/workspace/caller-project",
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("ResolveRuntimeSessionRailPlacement(authoritative) error = %v", err)
|
|
}
|
|
wantAuthoritativeKey := storesqlite.RailSectionKeyForProject("/workspace/caller-project")
|
|
if authoritative.Kind != agenthost.RailPlacementKindProject ||
|
|
authoritative.ProjectPath != storesqlite.NormalizeProjectPath("/workspace/caller-project") ||
|
|
authoritative.SectionKey != wantAuthoritativeKey {
|
|
t.Fatalf("authoritative placement = %#v, want project %q", authoritative, wantAuthoritativeKey)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteWorkspaceStoreProjectsForkTurnIdentitiesThroughProductionPort(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host-fork-store.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
canonical := storesqlite.New(db, storesqlite.Options{})
|
|
if err := canonical.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
if _, err := canonical.ReportSessionState(t.Context(), storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "source", Kind: storesqlite.SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Cwd: "/workspace", Status: "ready", CurrentPhase: "idle", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatalf("seed source session: %v", err)
|
|
}
|
|
if _, err := canonical.ReportActivityState(t.Context(), storesqlite.ActivityStateReport{
|
|
Session: storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "source", Kind: storesqlite.SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Cwd: "/workspace", Status: "active", CurrentPhase: "working", OccurredAtUnixMS: 10,
|
|
},
|
|
Turn: &storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "source", TurnID: "turn-1",
|
|
Phase: storesqlite.TurnPhaseRunning, OccurredAtUnixMS: 10,
|
|
},
|
|
RootProviderTurn: &storesqlite.RootProviderTurnTransition{
|
|
WorkspaceID: "workspace-1", RootAgentSessionID: "source", RootTurnID: "turn-1",
|
|
ProviderTurnID: "provider-turn-1", Phase: storesqlite.RootProviderTurnPhaseRunning,
|
|
OccurredAtUnixMS: 10,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("seed running source turn: %v", err)
|
|
}
|
|
if _, err := canonical.ReportActivityState(t.Context(), storesqlite.ActivityStateReport{
|
|
Session: storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "source", OccurredAtUnixMS: 12,
|
|
},
|
|
RootProviderTurn: &storesqlite.RootProviderTurnTransition{
|
|
WorkspaceID: "workspace-1", RootAgentSessionID: "source", RootTurnID: "turn-1",
|
|
ProviderTurnID: "provider-turn-1", Phase: storesqlite.RootProviderTurnPhaseCompleted,
|
|
Outcome: storesqlite.TurnOutcomeCompleted, OccurredAtUnixMS: 12,
|
|
},
|
|
}); err != nil {
|
|
t.Fatalf("settle source turn: %v", err)
|
|
}
|
|
store := &agenthost.SQLiteWorkspaceStore{
|
|
StoreForWorkspace: func(workspaceID string) *storesqlite.Store {
|
|
if workspaceID == "workspace-1" {
|
|
return canonical
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
host := agenthost.New(agenthost.Config{
|
|
SessionForks: store,
|
|
SessionForkRuntime: workspaceStoreForkRuntime{},
|
|
})
|
|
capabilities, err := host.GetSessionForkCapabilities(
|
|
t.Context(),
|
|
agenthost.SessionForkCapabilityInput{
|
|
WorkspaceID: "workspace-1",
|
|
SourceAgentSessionID: "source",
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatalf("GetSessionForkCapabilities() error = %v", err)
|
|
}
|
|
if !capabilities.ThroughTurn {
|
|
t.Fatalf("GetSessionForkCapabilities() = %#v, want through-turn enabled", capabilities)
|
|
}
|
|
}
|
|
|
|
func TestSQLiteWorkspaceStoreRejectsUnknownWorkspace(t *testing.T) {
|
|
store := &agenthost.SQLiteWorkspaceStore{StoreForWorkspace: func(string) *storesqlite.Store { return nil }}
|
|
if _, _, err := store.GetSession(t.Context(), "workspace-1", "session-1"); err == nil {
|
|
t.Fatal("GetSession succeeded without a workspace store")
|
|
}
|
|
}
|
|
|
|
func TestSQLiteWorkspaceStoreReadsSessionAndTurnFromOneSnapshot(t *testing.T) {
|
|
db, err := sql.Open("sqlite", filepath.Join(t.TempDir(), "agent-host-snapshot-store.db"))
|
|
if err != nil {
|
|
t.Fatalf("open SQLite: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
db.SetMaxOpenConns(1)
|
|
canonical := storesqlite.New(db, storesqlite.Options{})
|
|
if err := canonical.Migrate(t.Context()); err != nil {
|
|
t.Fatalf("migrate SQLite: %v", err)
|
|
}
|
|
if _, err := canonical.ReportSessionState(t.Context(), storesqlite.SessionStateReport{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", Kind: storesqlite.SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", CurrentPhase: "idle", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatalf("seed session: %v", err)
|
|
}
|
|
if _, accepted, err := canonical.RecordTurnTransition(t.Context(), storesqlite.TurnTransition{
|
|
WorkspaceID: "workspace-1", AgentSessionID: "session-1", TurnID: "turn-1",
|
|
Phase: storesqlite.TurnPhaseRunning, OccurredAtUnixMS: 2,
|
|
}); err != nil || !accepted {
|
|
t.Fatalf("seed turn: accepted=%v err=%v", accepted, err)
|
|
}
|
|
store := &agenthost.SQLiteWorkspaceStore{
|
|
StoreForWorkspace: func(workspaceID string) *storesqlite.Store {
|
|
if workspaceID == "workspace-1" {
|
|
return canonical
|
|
}
|
|
return nil
|
|
},
|
|
}
|
|
session, turn, found, err := store.GetSessionAndTurn(t.Context(), "workspace-1", "session-1", "turn-1")
|
|
if err != nil || !found {
|
|
t.Fatalf("GetSessionAndTurn() found=%v err=%v", found, err)
|
|
}
|
|
if session.ActiveTurnID != turn.TurnID || turn.Phase != storesqlite.TurnPhaseRunning {
|
|
t.Fatalf("snapshot = session active %q, turn %#v; want active turn-1", session.ActiveTurnID, turn)
|
|
}
|
|
}
|