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
685 lines
20 KiB
Go
685 lines
20 KiB
Go
package agentruntime
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestCodexAppServerForkCapabilitiesRequireExactSupportedRuntime(t *testing.T) {
|
|
t.Run("supported codex", func(t *testing.T) {
|
|
adapter, source, transport := startForkCapableCodexAdapter(t)
|
|
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
|
|
if err != nil {
|
|
t.Fatalf("ForkCapabilities: %v", err)
|
|
}
|
|
if capabilities.FullSession || !capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities = %#v, want through-turn only", capabilities)
|
|
}
|
|
if spawned, live := transport.snapshot(); spawned != 1 || len(live) != 1 {
|
|
t.Fatalf(
|
|
"capability probe processes = spawned %d/live %d, want existing 1/1",
|
|
spawned,
|
|
len(live),
|
|
)
|
|
}
|
|
})
|
|
|
|
t.Run("older codex", func(t *testing.T) {
|
|
transport := &multiProcAppServerTransport{}
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.143.9"
|
|
})
|
|
adapter := NewCodexAppServerAdapter(transport)
|
|
source := testAppServerSession()
|
|
if _, err := adapter.Start(context.Background(), source); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
|
|
if err != nil {
|
|
t.Fatalf("ForkCapabilities: %v", err)
|
|
}
|
|
if capabilities.FullSession || capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities = %#v, want unsupported", capabilities)
|
|
}
|
|
})
|
|
|
|
t.Run("supported tutti agent", func(t *testing.T) {
|
|
transport := &multiProcAppServerTransport{}
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = tuttiAgentForkUserAgent()
|
|
})
|
|
adapter := NewTuttiAgentAppServerAdapterWithHostMetadata(
|
|
transport,
|
|
LegacyHostMetadata(),
|
|
)
|
|
source := testAppServerSession()
|
|
source.Provider = ProviderTuttiAgent
|
|
if _, err := adapter.Start(context.Background(), source); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
|
|
if err != nil {
|
|
t.Fatalf("ForkCapabilities: %v", err)
|
|
}
|
|
if capabilities.FullSession || !capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities = %#v, want through-turn only", capabilities)
|
|
}
|
|
})
|
|
|
|
t.Run("older tutti agent", func(t *testing.T) {
|
|
transport := &multiProcAppServerTransport{}
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "tutti_agent/0.0.9"
|
|
})
|
|
adapter := NewTuttiAgentAppServerAdapterWithHostMetadata(
|
|
transport,
|
|
LegacyHostMetadata(),
|
|
)
|
|
source := testAppServerSession()
|
|
source.Provider = ProviderTuttiAgent
|
|
if _, err := adapter.Start(context.Background(), source); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
source.ProviderSessionID = "tutti-thread-1"
|
|
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
|
|
if err != nil {
|
|
t.Fatalf("ForkCapabilities: %v", err)
|
|
}
|
|
if capabilities.FullSession || capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities = %#v, want unsupported", capabilities)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCodexAppServerForkCapabilitiesUsePersistedRuntimeAttestation(t *testing.T) {
|
|
transport := &multiProcAppServerTransport{}
|
|
adapter := NewCodexAppServerAdapter(transport)
|
|
source := testAppServerSession()
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
source.RuntimeContext = map[string]any{
|
|
"agent": map[string]any{"userAgent": "codex/0.144.1"},
|
|
}
|
|
|
|
capabilities, err := adapter.ForkCapabilities(context.Background(), source)
|
|
if err != nil {
|
|
t.Fatalf("ForkCapabilities: %v", err)
|
|
}
|
|
if capabilities.FullSession || !capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities = %#v, want through-turn only", capabilities)
|
|
}
|
|
if spawned, live := transport.snapshot(); spawned != 0 || len(live) != 0 {
|
|
t.Fatalf(
|
|
"capability projection processes = spawned %d/live %d, want 0/0",
|
|
spawned,
|
|
len(live),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestTuttiAgentAppServerForkCapabilitiesUsePersistedRuntimeAttestation(
|
|
t *testing.T,
|
|
) {
|
|
transport := &multiProcAppServerTransport{}
|
|
adapter := NewTuttiAgentAppServerAdapterWithHostMetadata(
|
|
transport,
|
|
LegacyHostMetadata(),
|
|
)
|
|
source := testAppServerSession()
|
|
source.Provider = ProviderTuttiAgent
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
source.RuntimeContext = map[string]any{
|
|
"agent": map[string]any{"userAgent": tuttiAgentForkUserAgent()},
|
|
}
|
|
|
|
capabilities, err := adapter.ForkCapabilities(t.Context(), source)
|
|
if err != nil {
|
|
t.Fatalf("ForkCapabilities: %v", err)
|
|
}
|
|
if capabilities.FullSession || !capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities = %#v, want through-turn only", capabilities)
|
|
}
|
|
if spawned, live := transport.snapshot(); spawned != 0 || len(live) != 0 {
|
|
t.Fatalf(
|
|
"capability projection processes = spawned %d/live %d, want 0/0",
|
|
spawned,
|
|
len(live),
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestCodexAppServerForkCapabilitiesRequirePersistedAttestation(t *testing.T) {
|
|
t.Run("missing attestation", func(t *testing.T) {
|
|
transport := &multiProcAppServerTransport{}
|
|
adapter := NewCodexAppServerAdapter(transport)
|
|
source := testAppServerSession()
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
|
|
capabilities, err := adapter.ForkCapabilities(t.Context(), source)
|
|
if err != nil || capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities=%#v error=%v, want unsupported", capabilities, err)
|
|
}
|
|
if spawned, _ := transport.snapshot(); spawned != 0 {
|
|
t.Fatalf("capability projection spawned=%d, want 0", spawned)
|
|
}
|
|
})
|
|
|
|
t.Run("older persisted runtime", func(t *testing.T) {
|
|
transport := &multiProcAppServerTransport{}
|
|
adapter := NewCodexAppServerAdapter(transport)
|
|
source := testAppServerSession()
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
source.RuntimeContext = map[string]any{
|
|
"agent": map[string]any{"userAgent": "codex/0.143.9"},
|
|
}
|
|
|
|
capabilities, err := adapter.ForkCapabilities(t.Context(), source)
|
|
if err != nil || capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities=%#v error=%v, want unsupported", capabilities, err)
|
|
}
|
|
if spawned, _ := transport.snapshot(); spawned != 0 {
|
|
t.Fatalf("capability projection spawned=%d, want 0", spawned)
|
|
}
|
|
})
|
|
}
|
|
|
|
func TestCodexAppServerForkThroughProviderTurn(t *testing.T) {
|
|
adapter, source, transport := startForkCapableCodexAdapter(t)
|
|
|
|
result, err := adapter.Fork(context.Background(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Fork: %v", err)
|
|
}
|
|
if result.ProviderSessionID != "codex-thread-fork" ||
|
|
result.ForkedFromProviderSessionID != source.ProviderSessionID ||
|
|
result.ThroughProviderTurnID != "provider-turn-2" {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
request := appServerRequestParams(
|
|
t,
|
|
transport.conn(1),
|
|
appServerMethodThreadFork,
|
|
)
|
|
if got := asString(request["threadId"]); got != source.ProviderSessionID {
|
|
t.Fatalf("threadId = %q, want %q", got, source.ProviderSessionID)
|
|
}
|
|
if got := asString(request["lastTurnId"]); got != "provider-turn-2" {
|
|
t.Fatalf("lastTurnId = %q, want provider-turn-2", got)
|
|
}
|
|
for _, forbidden := range []string{"path", "cwd", "model", "config"} {
|
|
if _, ok := request[forbidden]; ok {
|
|
t.Fatalf("fork request unexpectedly contains %q: %#v", forbidden, request)
|
|
}
|
|
}
|
|
spawned, live := transport.snapshot()
|
|
if spawned != 2 || len(live) != 1 {
|
|
t.Fatalf("processes = spawned %d/live %d, want 2/1", spawned, len(live))
|
|
}
|
|
if !adapter.HasLiveSession(source) {
|
|
t.Fatal("source live session was closed by fork")
|
|
}
|
|
}
|
|
|
|
func TestTuttiAgentAppServerForkThroughProviderTurn(t *testing.T) {
|
|
transport := &multiProcAppServerTransport{}
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = tuttiAgentForkUserAgent()
|
|
server.threadReadTurnIDs = []string{
|
|
"provider-turn-1",
|
|
"provider-turn-2",
|
|
}
|
|
})
|
|
adapter := NewTuttiAgentAppServerAdapterWithHostMetadata(
|
|
transport,
|
|
LegacyHostMetadata(),
|
|
)
|
|
source := testAppServerSession()
|
|
source.Provider = ProviderTuttiAgent
|
|
if _, err := adapter.Start(t.Context(), source); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
|
|
result, err := controller.Fork(t.Context(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
ProviderTurnBindingJSON: json.RawMessage(`{"schemaVersion":1}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Fork: %v", err)
|
|
}
|
|
if result.ProviderSessionID != "codex-thread-fork" ||
|
|
result.ForkedFromProviderSessionID != source.ProviderSessionID ||
|
|
result.ThroughProviderTurnID != "provider-turn-2" {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
request := appServerRequestParams(
|
|
t,
|
|
transport.conn(1),
|
|
appServerMethodThreadFork,
|
|
)
|
|
if got := asString(request["lastTurnId"]); got != "provider-turn-2" {
|
|
t.Fatalf("lastTurnId = %q, want provider-turn-2", got)
|
|
}
|
|
}
|
|
|
|
func TestCodexAppServerForkedChildCanResumeAndStartTurn(t *testing.T) {
|
|
adapter, source, transport := startForkCapableCodexAdapter(t)
|
|
result, err := adapter.Fork(t.Context(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Fork: %v", err)
|
|
}
|
|
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
server.holdTurn = true
|
|
})
|
|
target := source
|
|
target.AgentSessionID = "agent-session-fork"
|
|
target.ProviderSessionID = result.ProviderSessionID
|
|
if err := adapter.Resume(t.Context(), target); err != nil {
|
|
t.Fatalf("Resume forked child: %v", err)
|
|
}
|
|
if !adapter.HasLiveSession(target) {
|
|
t.Fatal("forked child did not retain a live resumed session")
|
|
}
|
|
|
|
if err := adapter.ExecAsync(
|
|
t.Context(),
|
|
target,
|
|
[]PromptContentBlock{{Type: "text", Text: "continue from fork"}},
|
|
"",
|
|
"canonical-fork-turn-1",
|
|
nil,
|
|
nil,
|
|
); err != nil {
|
|
t.Fatalf("ExecAsync forked child: %v", err)
|
|
}
|
|
targetConn := transport.conn(2)
|
|
waitForCondition(t, func() bool {
|
|
return len(appServerRequestParamsList(
|
|
t,
|
|
targetConn,
|
|
appServerMethodTurnStart,
|
|
)) == 1
|
|
})
|
|
turnStart := appServerRequestParams(
|
|
t,
|
|
targetConn,
|
|
appServerMethodTurnStart,
|
|
)
|
|
if got := asString(turnStart["threadId"]); got != result.ProviderSessionID {
|
|
t.Fatalf(
|
|
"forked child turn/start threadId = %q, want %q",
|
|
got,
|
|
result.ProviderSessionID,
|
|
)
|
|
}
|
|
if !adapter.HasLiveSession(source) {
|
|
t.Fatal("resuming forked child replaced the source live session")
|
|
}
|
|
if err := adapter.Close(t.Context(), target); err != nil {
|
|
t.Fatalf("Close forked child: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestCodexAppServerForkRejectsUnavailableBoundaryBeforeProviderMutation(
|
|
t *testing.T,
|
|
) {
|
|
adapter, source, transport := startForkCapableCodexAdapter(t)
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
server.threadReadTurnIDs = []string{"provider-turn-1"}
|
|
})
|
|
|
|
result, err := adapter.Fork(t.Context(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
})
|
|
if err == nil ||
|
|
result.DeliveryDisposition != SessionForkDeliveryNotStarted {
|
|
t.Fatalf("Fork() result=%#v error=%v, want not-started", result, err)
|
|
}
|
|
if requests := appServerRequestParamsList(
|
|
t,
|
|
transport.conn(1),
|
|
appServerMethodThreadFork,
|
|
); len(requests) != 0 {
|
|
t.Fatalf("thread/fork requests=%d, want zero", len(requests))
|
|
}
|
|
}
|
|
|
|
func TestCodexAppServerForkRejectsUnverifiedChild(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
configure func(*fakeCodexAppServer)
|
|
}{
|
|
{
|
|
name: "lineage missing",
|
|
configure: func(server *fakeCodexAppServer) {
|
|
server.omitForkedFromThreadID = true
|
|
},
|
|
},
|
|
{
|
|
name: "lineage empty",
|
|
configure: func(server *fakeCodexAppServer) {
|
|
server.emptyForkedFromThreadID = true
|
|
},
|
|
},
|
|
{
|
|
name: "lineage mismatch",
|
|
configure: func(server *fakeCodexAppServer) {
|
|
server.forkedFromThreadID = "different-source"
|
|
},
|
|
},
|
|
{
|
|
name: "boundary mismatch",
|
|
configure: func(server *fakeCodexAppServer) {
|
|
server.forkResponseLastTurnID = "different-turn"
|
|
},
|
|
},
|
|
{
|
|
name: "source returned as child",
|
|
configure: func(server *fakeCodexAppServer) {
|
|
server.forkChildThreadID = "codex-thread-1"
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
adapter, source, transport := startForkCapableCodexAdapter(t)
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
test.configure(server)
|
|
})
|
|
result, err := adapter.Fork(context.Background(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Fork succeeded, want verification error")
|
|
}
|
|
if result.DeliveryDisposition != SessionForkDeliveryUnknown {
|
|
t.Fatalf(
|
|
"delivery disposition = %q, want %q",
|
|
result.DeliveryDisposition,
|
|
SessionForkDeliveryUnknown,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCodexAppServerForkClassifiesExplicitRPCRejection(t *testing.T) {
|
|
adapter, source, transport := startForkCapableCodexAdapter(t)
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
server.forkRPCError = true
|
|
})
|
|
result, err := adapter.Fork(context.Background(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
})
|
|
if err == nil {
|
|
t.Fatal("Fork succeeded, want explicit provider rejection")
|
|
}
|
|
if result.DeliveryDisposition != SessionForkDeliveryRejected {
|
|
t.Fatalf(
|
|
"delivery disposition = %q, want %q",
|
|
result.DeliveryDisposition,
|
|
SessionForkDeliveryRejected,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestCodexAppServerForkVerifiesSelectedProviderTurnBinding(t *testing.T) {
|
|
adapter, source, transport := startForkCapableCodexAdapter(t)
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
server.forkResponseTurnIDs = []string{"provider-turn-1", "provider-turn-2"}
|
|
})
|
|
result, err := adapter.Fork(context.Background(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Fork: %v", err)
|
|
}
|
|
if result.DeliveryDisposition != SessionForkDeliveryAccepted {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
server.forkResponseTurnIDs = []string{"provider-turn-2"}
|
|
})
|
|
selectedOnly, err := adapter.Fork(context.Background(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
})
|
|
if err != nil || selectedOnly.DeliveryDisposition != SessionForkDeliveryAccepted {
|
|
t.Fatalf("selected-only Fork result=%#v error=%v", selectedOnly, err)
|
|
}
|
|
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
server.forkResponseTurnIDs = []string{"provider-turn-1"}
|
|
})
|
|
rejected, err := adapter.Fork(context.Background(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
})
|
|
if err == nil || rejected.DeliveryDisposition != SessionForkDeliveryUnknown {
|
|
t.Fatalf("missing selected binding result=%#v error=%v", rejected, err)
|
|
}
|
|
}
|
|
|
|
func TestControllerForkUsesOptionalSessionForkAdapter(t *testing.T) {
|
|
transport := &multiProcAppServerTransport{}
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
})
|
|
adapter := NewCodexAppServerAdapter(transport)
|
|
source := testAppServerSession()
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
source.RuntimeContext = map[string]any{
|
|
"agent": map[string]any{"userAgent": "codex/0.144.1"},
|
|
}
|
|
controller := NewController([]Adapter{adapter}, nil)
|
|
|
|
capabilities, err := controller.ForkCapabilities(context.Background(), source)
|
|
if err != nil {
|
|
t.Fatalf("ForkCapabilities: %v", err)
|
|
}
|
|
if !capabilities.ThroughTurn {
|
|
t.Fatalf("capabilities = %#v", capabilities)
|
|
}
|
|
result, err := controller.Fork(context.Background(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
ProviderTurnBindingJSON: []byte(`{"schemaVersion":1}`),
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("Fork: %v", err)
|
|
}
|
|
if result.ProviderSessionID != "codex-thread-fork" {
|
|
t.Fatalf("result = %#v", result)
|
|
}
|
|
spawned, live := transport.snapshot()
|
|
if spawned != 1 || len(live) != 0 {
|
|
t.Fatalf("processes = spawned %d/live %d, want 1/0", spawned, len(live))
|
|
}
|
|
if adapter.HasLiveSession(source) {
|
|
t.Fatal("historical fork registered a live source session")
|
|
}
|
|
|
|
controller.mu.Lock()
|
|
controller.turns[sessionKey(source.RoomID, source.AgentSessionID)] = activeTurn{
|
|
turnID: "canonical-turn-running",
|
|
}
|
|
controller.mu.Unlock()
|
|
activeResult, err := controller.Fork(context.Background(), SessionForkInput{
|
|
Source: source,
|
|
ProviderTurnID: "provider-turn-2",
|
|
ProviderTurnBindingJSON: []byte(`{"schemaVersion":1}`),
|
|
})
|
|
if err != nil || activeResult.DeliveryDisposition != SessionForkDeliveryAccepted {
|
|
t.Fatalf("Fork active turn result=%#v error=%v", activeResult, err)
|
|
}
|
|
}
|
|
|
|
func TestControllerForkClassifiesSourcePreparationFailureAsNotStarted(t *testing.T) {
|
|
source := testAppServerSession()
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
|
|
tests := []struct {
|
|
name string
|
|
controller *Controller
|
|
mutate func(*Session)
|
|
}{
|
|
{
|
|
name: "invalid source",
|
|
controller: NewController(nil, nil),
|
|
mutate: func(source *Session) {
|
|
source.Provider = ""
|
|
},
|
|
},
|
|
{
|
|
name: "adapter resolution failure",
|
|
controller: NewControllerWithAdapterResolver(
|
|
nil,
|
|
nil,
|
|
sessionForkErrorAdapterResolver{},
|
|
),
|
|
mutate: func(*Session) {},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
testSource := source
|
|
test.mutate(&testSource)
|
|
result, err := test.controller.Fork(
|
|
context.Background(),
|
|
SessionForkInput{
|
|
Source: testSource,
|
|
ProviderTurnID: "provider-turn-2",
|
|
},
|
|
)
|
|
if err == nil {
|
|
t.Fatal("Fork succeeded, want source preparation error")
|
|
}
|
|
if result.DeliveryDisposition != SessionForkDeliveryNotStarted {
|
|
t.Fatalf(
|
|
"delivery disposition = %q, want %q",
|
|
result.DeliveryDisposition,
|
|
SessionForkDeliveryNotStarted,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
type sessionForkErrorAdapterResolver struct{}
|
|
|
|
func (sessionForkErrorAdapterResolver) ResolveAdapter(
|
|
context.Context,
|
|
AdapterResolveInput,
|
|
) (Adapter, error) {
|
|
return nil, errors.New("session fork adapter resolution failed")
|
|
}
|
|
|
|
func TestCodexAppServerUserAgentVersionGate(t *testing.T) {
|
|
strategy := appServerForkStrategyForTest(t, ProviderCodex)
|
|
tests := []struct {
|
|
userAgent string
|
|
want bool
|
|
}{
|
|
{userAgent: "codex/0.144.0", want: true},
|
|
{userAgent: "codex-cli 0.145.2", want: true},
|
|
{userAgent: "codex/0.143.99", want: false},
|
|
{userAgent: "codex/0.146.0-alpha.3.1", want: true},
|
|
{userAgent: "tutti-agent/9.0.0", want: false},
|
|
{userAgent: "", want: false},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.userAgent, func(t *testing.T) {
|
|
version, ok := appServerForkVersion(
|
|
strategy,
|
|
map[string]any{"userAgent": test.userAgent},
|
|
)
|
|
got := ok && versionAtLeast(
|
|
version,
|
|
strategy.throughTurnMinimumVersion,
|
|
)
|
|
if got != test.want {
|
|
t.Fatalf("gate(%q) = %v, want %v", test.userAgent, got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTuttiAgentAppServerUserAgentVersionGate(t *testing.T) {
|
|
strategy := appServerForkStrategyForTest(t, ProviderTuttiAgent)
|
|
tests := []struct {
|
|
userAgent string
|
|
want bool
|
|
}{
|
|
{userAgent: tuttiAgentForkUserAgent(), want: true},
|
|
{
|
|
userAgent: tuttiAgentForkUserAgent() + " (Mac OS 26.5.0; arm64) dumb",
|
|
want: true,
|
|
},
|
|
{userAgent: "tutti-agent/0.0.9", want: false},
|
|
{userAgent: "codex/0.144.0", want: false},
|
|
{userAgent: "", want: false},
|
|
}
|
|
for _, test := range tests {
|
|
t.Run(test.userAgent, func(t *testing.T) {
|
|
version, ok := appServerForkVersion(
|
|
strategy,
|
|
map[string]any{"userAgent": test.userAgent},
|
|
)
|
|
got := ok && versionAtLeast(
|
|
version,
|
|
strategy.throughTurnMinimumVersion,
|
|
)
|
|
if got != test.want {
|
|
t.Fatalf(
|
|
"gate(%q) = %v, want %v",
|
|
test.userAgent,
|
|
got,
|
|
test.want,
|
|
)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func startForkCapableCodexAdapter(
|
|
t *testing.T,
|
|
) (*CodexAppServerAdapter, Session, *multiProcAppServerTransport) {
|
|
t.Helper()
|
|
transport := &multiProcAppServerTransport{}
|
|
transport.setConfigure(func(server *fakeCodexAppServer) {
|
|
server.userAgent = "codex/0.144.1"
|
|
server.threadReadTurnIDs = []string{"provider-turn-1", "provider-turn-2"}
|
|
})
|
|
adapter := NewCodexAppServerAdapter(transport)
|
|
source := testAppServerSession()
|
|
if _, err := adapter.Start(context.Background(), source); err != nil {
|
|
t.Fatalf("Start: %v", err)
|
|
}
|
|
source.ProviderSessionID = "codex-thread-1"
|
|
return adapter, source, transport
|
|
}
|