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
2308 lines
82 KiB
Go
2308 lines
82 KiB
Go
package storesqlite
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
func TestSessionForkThroughTurnClonesInclusivePrefixAndReplays(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, updated, err := store.UpdateSessionTitle(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
"123",
|
|
); err != nil || !updated {
|
|
t.Fatalf("UpdateSessionTitle() updated=%v error=%v", updated, err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_sessions
|
|
SET session_metadata_json = '{
|
|
"visible": true,
|
|
"imported": true,
|
|
"capabilities": [],
|
|
"usage": {
|
|
"contextWindow": {"usedTokens": 1, "totalTokens": 10},
|
|
"quotas": []
|
|
},
|
|
"goal": {"objective": "ship", "status": "active"}
|
|
}'
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
prepare := SessionForkPrepare{
|
|
OperationID: "fork-op", WorkspaceID: "ws-1", RequestID: "request-1",
|
|
RequestHash: "hash-1", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target", SourceTurnID: "turn-1",
|
|
DriverKind: "codex-app-server", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
TargetCwd: "/target/project",
|
|
TargetRuntimeContext: map[string]any{"sessionRuntimeSnapshot": map[string]any{
|
|
"version": float64(1),
|
|
}},
|
|
TargetSettings: map[string]any{
|
|
"model": "prepared-model", "permissionModeId": "prepared-mode",
|
|
},
|
|
}
|
|
operation, changed, err := prepareSessionForkForTest(t, store, ctx, prepare)
|
|
if err != nil || !changed || operation.Status != SessionForkStatusPrepared ||
|
|
operation.PointKind != SessionForkPointThroughTurn {
|
|
t.Fatalf("PrepareSessionFork() operation=%#v changed=%v error=%v", operation, changed, err)
|
|
}
|
|
replayed, changed, err := prepareSessionForkForTest(t, store, ctx, prepare)
|
|
if err != nil || changed || replayed.OperationID != operation.OperationID {
|
|
t.Fatalf("PrepareSessionFork(replay) operation=%#v changed=%v error=%v", replayed, changed, err)
|
|
}
|
|
conflict := prepare
|
|
conflict.RequestHash = "different"
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, conflict); !errors.Is(err, ErrSessionForkRequestConflict) {
|
|
t.Fatalf("PrepareSessionFork(conflict) error=%v", err)
|
|
}
|
|
|
|
operation, changed, err = store.MarkSessionForkDispatching(ctx, "ws-1", "fork-op", 101)
|
|
if err != nil || !changed || operation.Status != SessionForkStatusDispatching {
|
|
t.Fatalf("MarkSessionForkDispatching() operation=%#v changed=%v error=%v", operation, changed, err)
|
|
}
|
|
operation, changed, err = store.RecordSessionForkProviderResult(ctx, SessionForkProviderResult{
|
|
WorkspaceID: "ws-1", OperationID: "fork-op",
|
|
Status: SessionForkStatusProviderAccepted, TargetProviderSessionID: "provider-target",
|
|
OccurredAtUnixMS: 102,
|
|
})
|
|
if err != nil || !changed || operation.Status != SessionForkStatusProviderAccepted {
|
|
t.Fatalf("RecordSessionForkProviderResult() operation=%#v changed=%v error=%v", operation, changed, err)
|
|
}
|
|
committed, err := store.CommitSessionFork(ctx, "ws-1", "fork-op", 103)
|
|
if err != nil || !committed.Changed || committed.Operation.Status != SessionForkStatusCommitted {
|
|
t.Fatalf("CommitSessionFork() result=%#v error=%v", committed, err)
|
|
}
|
|
retried, err := store.CommitSessionFork(ctx, "ws-1", "fork-op", 104)
|
|
if err != nil || retried.Changed || retried.Operation.Status != SessionForkStatusCommitted {
|
|
t.Fatalf("CommitSessionFork(retry) result=%#v error=%v", retried, err)
|
|
}
|
|
|
|
child, found, err := store.GetSession(ctx, "ws-1", "target")
|
|
if err != nil || !found || child.Kind != SessionKindRoot ||
|
|
child.ProviderSessionID != "provider-target" || child.ActiveTurnID != "" ||
|
|
child.Title != "123 (2)" ||
|
|
child.Cwd != "/target/project" ||
|
|
child.Model != "prepared-model" ||
|
|
child.Settings["permissionModeId"] != "prepared-mode" ||
|
|
child.InternalRuntimeContext["sessionRuntimeSnapshot"] == nil ||
|
|
child.Metadata.Imported || child.Metadata.Usage != nil ||
|
|
child.Metadata.Goal != nil ||
|
|
child.StartedAtUnixMS != 103 || child.LastEventUnixMS != 103 ||
|
|
child.EndedAtUnixMS != 0 {
|
|
t.Fatalf("forked session=%#v found=%v error=%v", child, found, err)
|
|
}
|
|
if retried.Session.StartedAtUnixMS != child.StartedAtUnixMS ||
|
|
retried.Session.LastEventUnixMS != child.LastEventUnixMS ||
|
|
retried.Session.Title != child.Title ||
|
|
retried.Session.Metadata.Imported != child.Metadata.Imported ||
|
|
retried.Session.Metadata.Usage != child.Metadata.Usage ||
|
|
retried.Session.Metadata.Goal != child.Metadata.Goal {
|
|
t.Fatalf(
|
|
"first/replayed committed sessions diverged: first=%#v replay=%#v",
|
|
child,
|
|
retried.Session,
|
|
)
|
|
}
|
|
expectedTurnID := deterministicSessionForkCanonicalID(operation, "turn", "turn-1")
|
|
expectedMessageID := deterministicSessionForkCanonicalID(operation, "message", "message-1")
|
|
if committed.Operation.TargetTurnID != expectedTurnID ||
|
|
committed.Lineage.TargetTurnID != expectedTurnID ||
|
|
retried.Operation.TargetTurnID != expectedTurnID ||
|
|
retried.Lineage.TargetTurnID != expectedTurnID {
|
|
t.Fatalf(
|
|
"fork boundary target identity first=%#v replay=%#v want=%q",
|
|
committed,
|
|
retried,
|
|
expectedTurnID,
|
|
)
|
|
}
|
|
turns, err := store.ListSessionTurns(ctx, "ws-1", "target")
|
|
if err != nil || len(turns) != 1 || turns[0].TurnID != expectedTurnID {
|
|
t.Fatalf("forked turns=%#v error=%v", turns, err)
|
|
}
|
|
page, found, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{
|
|
WorkspaceID: "ws-1", AgentSessionID: "target", Limit: 10,
|
|
})
|
|
if err != nil || !found || len(page.Messages) != 1 {
|
|
t.Fatalf("forked messages=%#v found=%v error=%v", page.Messages, found, err)
|
|
}
|
|
if page.Messages[0].MessageID != expectedMessageID ||
|
|
page.Messages[0].TurnID != expectedTurnID ||
|
|
page.Messages[0].Version != 1 ||
|
|
page.Messages[0].Payload["sourceSessionId"] != "source" {
|
|
t.Fatalf("forked message=%#v", page.Messages[0])
|
|
}
|
|
lineage, found, err := store.GetSessionForkLineage(ctx, "ws-1", "target")
|
|
if err != nil || !found || lineage.SourceAgentSessionID != "source" ||
|
|
lineage.SourceTurnID != "turn-1" ||
|
|
lineage.TargetTurnID != expectedTurnID {
|
|
t.Fatalf("lineage=%#v found=%v error=%v", lineage, found, err)
|
|
}
|
|
recovered, created, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-op-lost-response", WorkspaceID: "ws-1",
|
|
RequestID: "request-lost-response", RequestHash: "hash-lost-response",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-lost-response",
|
|
SourceTurnID: "turn-1", DriverKind: "codex-app-server",
|
|
DriverVersion: "1", OccurredAtUnixMS: 105,
|
|
})
|
|
if err != nil || !created || recovered.OperationID != "fork-op-lost-response" ||
|
|
recovered.TargetAgentSessionID != "target-lost-response" ||
|
|
recovered.ClientObservedAtUnixMS != 0 {
|
|
t.Fatalf(
|
|
"unobserved committed barrier operation=%#v created=%v error=%v",
|
|
recovered,
|
|
created,
|
|
err,
|
|
)
|
|
}
|
|
if _, changed, err := store.FailPreparedSessionFork(
|
|
ctx, "ws-1", recovered.OperationID, "test cleanup", 105,
|
|
); err != nil || !changed {
|
|
t.Fatalf("cleanup parallel fork changed=%v error=%v", changed, err)
|
|
}
|
|
if _, found, changed, err := store.AcknowledgeSessionForkOperation(
|
|
ctx,
|
|
"ws-1",
|
|
"fork-op",
|
|
106,
|
|
); err != nil || !found || !changed {
|
|
t.Fatalf("first acknowledgement found=%v changed=%v error=%v", found, changed, err)
|
|
}
|
|
if acknowledged, found, changed, err := store.AcknowledgeSessionForkOperation(
|
|
ctx,
|
|
"ws-1",
|
|
"fork-op",
|
|
107,
|
|
); err != nil || !found || changed ||
|
|
acknowledged.ClientObservedAtUnixMS != 106 {
|
|
t.Fatalf(
|
|
"replayed acknowledgement operation=%#v found=%v changed=%v error=%v",
|
|
acknowledged,
|
|
found,
|
|
changed,
|
|
err,
|
|
)
|
|
}
|
|
next, created, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-op-next", WorkspaceID: "ws-1",
|
|
RequestID: "request-next", RequestHash: "hash-next",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-next",
|
|
SourceTurnID: "turn-1", DriverKind: "codex-app-server",
|
|
DriverVersion: "1", OccurredAtUnixMS: 108,
|
|
})
|
|
if err != nil || !created || next.OperationID != "fork-op-next" {
|
|
t.Fatalf("post-ack prepare operation=%#v created=%v error=%v", next, created, err)
|
|
}
|
|
if _, changed, err := store.FailPreparedSessionFork(
|
|
ctx,
|
|
"ws-1",
|
|
"fork-op-next",
|
|
"test cleanup",
|
|
109,
|
|
); err != nil || !changed {
|
|
t.Fatalf("cleanup post-ack fork changed=%v error=%v", changed, err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `PRAGMA foreign_keys = OFF`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_sessions
|
|
SET deleted_at_unix_ms = 200
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'target'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
purged, err := store.PurgeDeletedSessions(ctx, PurgeDeletedSessionsInput{
|
|
CutoffUnixMS: 201,
|
|
})
|
|
if err != nil || len(purged.Sessions) != 1 ||
|
|
purged.Sessions[0].AgentSessionID != "target" {
|
|
t.Fatalf("PurgeDeletedSessions() result=%#v error=%v", purged, err)
|
|
}
|
|
if _, found, err := store.GetSessionForkLineage(
|
|
ctx,
|
|
"ws-1",
|
|
"target",
|
|
); err != nil || found {
|
|
t.Fatalf("lineage after hard purge found=%v error=%v", found, err)
|
|
}
|
|
for _, table := range []string{
|
|
"workspace_agent_session_fork_operations",
|
|
"workspace_agent_session_fork_target_reservations",
|
|
"workspace_agent_session_fork_boundary_barriers",
|
|
} {
|
|
var count int
|
|
if err := store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM `+table+` WHERE operation_id='fork-op'`).Scan(&count); err != nil || count != 0 {
|
|
t.Fatalf("%s count after target purge=%d error=%v", table, count, err)
|
|
}
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "target", Kind: SessionKindRoot,
|
|
Provider: "codex", ProviderSessionID: "provider-target-reused",
|
|
OccurredAtUnixMS: 201,
|
|
}); err != nil {
|
|
t.Fatalf("reuse hard-purged fork target id: %v", err)
|
|
}
|
|
reused, found, err := store.GetSession(ctx, "ws-1", "target")
|
|
if err != nil || !found || reused.ProviderSessionID != "provider-target-reused" {
|
|
t.Fatalf("reused target=%#v found=%v error=%v", reused, found, err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkRemapsTurnIdentityAnchorWithinFork(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.db.ExecContext(ctx, `UPDATE workspace_agent_turns
|
|
SET identity_anchor_turn_id='turn-1'
|
|
WHERE workspace_id='ws-1' AND agent_session_id='source' AND turn_id='turn-2'`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := commitFork(t, store, SessionForkPrepare{
|
|
OperationID: "fork-identity-anchor", WorkspaceID: "ws-1",
|
|
RequestID: "request-identity-anchor", RequestHash: "hash-identity-anchor",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-identity-anchor",
|
|
SourceTurnID: "turn-2", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
})
|
|
wantTurnID := deterministicSessionForkCanonicalID(result.Operation, "turn", "turn-2")
|
|
wantAnchorTurnID := deterministicSessionForkCanonicalID(result.Operation, "turn", "turn-1")
|
|
turn, found, err := store.GetTurn(ctx, "ws-1", "target-identity-anchor", wantTurnID)
|
|
if err != nil || !found || turn.IdentityAnchorTurnID != wantAnchorTurnID {
|
|
t.Fatalf("forked identity Turn=%#v found=%v error=%v wantAnchor=%q", turn, found, err, wantAnchorTurnID)
|
|
}
|
|
}
|
|
|
|
func TestHardPurgeForkSourceRemovesSnapshotAndKeepsTargetRestorable(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-source-purge", WorkspaceID: "ws-1",
|
|
RequestID: "request-source-purge", RequestHash: "hash-source-purge",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "surviving-target",
|
|
SourceTurnID: "turn-1", DriverKind: "codex-app-server",
|
|
DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(ctx, "ws-1", "fork-source-purge", 101); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.RecordSessionForkProviderResult(ctx, SessionForkProviderResult{
|
|
WorkspaceID: "ws-1", OperationID: "fork-source-purge",
|
|
Status: SessionForkStatusProviderAccepted, TargetProviderSessionID: "provider-surviving-target",
|
|
OccurredAtUnixMS: 102,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.CommitSessionFork(ctx, "ws-1", "fork-source-purge", 103); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if removed, err := store.DeleteSession(ctx, "ws-1", "source"); err != nil || !removed {
|
|
t.Fatalf("DeleteSession(source) removed=%v error=%v", removed, err)
|
|
}
|
|
purged, err := store.PurgeDeletedSessionTrees(ctx, PurgeDeletedSessionTreesInput{
|
|
WorkspaceID: "ws-1", RootSessionIDs: []string{"source"},
|
|
})
|
|
if err != nil || purged.RemovedSessions != 1 {
|
|
t.Fatalf("PurgeDeletedSessionTrees(source)=%#v error=%v", purged, err)
|
|
}
|
|
for _, table := range []string{
|
|
"workspace_agent_session_fork_operations",
|
|
"workspace_agent_session_fork_target_reservations",
|
|
"workspace_agent_session_fork_boundary_barriers",
|
|
} {
|
|
var count int
|
|
if err := store.db.QueryRowContext(ctx, `SELECT COUNT(*) FROM `+table+` WHERE operation_id='fork-source-purge'`).Scan(&count); err != nil || count != 0 {
|
|
t.Fatalf("%s count after source purge=%d error=%v", table, count, err)
|
|
}
|
|
}
|
|
if _, found, err := store.GetSessionForkLineage(ctx, "ws-1", "surviving-target"); err != nil || found {
|
|
t.Fatalf("surviving target lineage found=%v error=%v", found, err)
|
|
}
|
|
target, found, err := store.GetSession(ctx, "ws-1", "surviving-target")
|
|
if err != nil || !found || target.ProviderSessionID != "provider-surviving-target" {
|
|
t.Fatalf("surviving target=%#v found=%v error=%v", target, found, err)
|
|
}
|
|
page, found, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{
|
|
WorkspaceID: "ws-1", AgentSessionID: "surviving-target", Limit: 10,
|
|
})
|
|
if err != nil || !found || len(page.Messages) != 1 {
|
|
t.Fatalf("surviving target messages=%#v found=%v error=%v", page.Messages, found, err)
|
|
}
|
|
if removed, err := store.DeleteSession(ctx, "ws-1", "surviving-target"); err != nil || !removed {
|
|
t.Fatalf("DeleteSession(surviving target) removed=%v error=%v", removed, err)
|
|
}
|
|
restored, err := store.RestoreDeletedSession(ctx, RestoreDeletedSessionInput{
|
|
WorkspaceID: "ws-1", AgentSessionID: "surviving-target",
|
|
})
|
|
if err != nil || !restored.Restored {
|
|
t.Fatalf("RestoreDeletedSession(surviving target)=%#v error=%v", restored, err)
|
|
}
|
|
target, found, err = store.GetSession(ctx, "ws-1", "surviving-target")
|
|
if err != nil || !found || target.ProviderSessionID != "provider-surviving-target" {
|
|
t.Fatalf("restored surviving target=%#v found=%v error=%v", target, found, err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkRejectsUnsettledSelectedTurn(t *testing.T) {
|
|
for _, phase := range []string{TurnPhaseRunning, TurnPhaseWaiting} {
|
|
phase := phase
|
|
t.Run(phase, func(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_turns
|
|
SET phase = ?, outcome = NULL, settled_at_unix_ms = NULL,
|
|
root_provider_turn_phase = 'running', root_provider_turn_outcome = NULL
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source' AND turn_id = 'turn-1'
|
|
`, phase); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_sessions
|
|
SET active_turn_id = 'turn-1'
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
boundary, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx, "ws-1", "source", "turn-1",
|
|
)
|
|
if err != nil || supported ||
|
|
boundary.RejectionReason != SessionForkBoundaryReasonTurnNotSettled {
|
|
t.Fatalf(
|
|
"CheckSessionForkThroughTurn() boundary=%#v supported=%v error=%v",
|
|
boundary,
|
|
supported,
|
|
err,
|
|
)
|
|
}
|
|
|
|
_, changed, err := store.PrepareSessionFork(ctx, SessionForkPrepare{
|
|
OperationID: "fork-" + phase, WorkspaceID: "ws-1",
|
|
RequestID: "request-" + phase, RequestHash: "hash-" + phase,
|
|
SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-" + phase,
|
|
SourceTurnID: "turn-1",
|
|
PointKind: SessionForkPointThroughTurn,
|
|
DriverKind: "codex",
|
|
DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
})
|
|
var boundaryErr *SessionForkBoundaryError
|
|
if changed || !errors.Is(err, ErrSessionForkTurnState) ||
|
|
!errors.As(err, &boundaryErr) ||
|
|
boundaryErr.Reason != SessionForkBoundaryReasonTurnNotSettled {
|
|
t.Fatalf("PrepareSessionFork() changed=%v error=%v", changed, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionForkAllowsSettledBoundaryWhileNewerTurnIsActive(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.ReportActivityState(ctx, ActivityStateReport{
|
|
Session: SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Kind: SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Status: "active", CurrentPhase: "working", OccurredAtUnixMS: 30,
|
|
},
|
|
Turn: &TurnTransition{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", TurnID: "turn-3",
|
|
Phase: TurnPhaseRunning, OccurredAtUnixMS: 30,
|
|
},
|
|
RootProviderTurn: &RootProviderTurnTransition{
|
|
WorkspaceID: "ws-1", RootAgentSessionID: "source", RootTurnID: "turn-3",
|
|
ProviderTurnID: "provider-turn-3", Phase: RootProviderTurnPhaseRunning,
|
|
OccurredAtUnixMS: 30,
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx, "ws-1", "source", "turn-2",
|
|
); err != nil || !supported {
|
|
t.Fatalf("CheckSessionForkThroughTurn() supported=%v error=%v", supported, err)
|
|
}
|
|
if _, changed, err := store.PrepareSessionFork(ctx, SessionForkPrepare{
|
|
OperationID: "fork-settled", WorkspaceID: "ws-1",
|
|
RequestID: "request-settled", RequestHash: "hash-settled",
|
|
SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-settled",
|
|
SourceTurnID: "turn-2",
|
|
PointKind: SessionForkPointThroughTurn,
|
|
DriverKind: "codex",
|
|
DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
}); err != nil || !changed {
|
|
t.Fatalf("PrepareSessionFork() changed=%v error=%v", changed, err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkProviderOwnedResultRewritesEveryChildProviderTurnBinding(t *testing.T) {
|
|
t.Parallel()
|
|
ctx := context.Background()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
seedForkSession(t, store)
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_turns
|
|
SET provider_turn_binding_json = CASE turn_id
|
|
WHEN 'turn-1' THEN '{"schemaVersion":1,"checkpointMessageId":"claude-source-checkpoint-1"}'
|
|
WHEN 'turn-2' THEN '{"schemaVersion":1,"checkpointMessageId":"claude-source-checkpoint-2"}'
|
|
END
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source'
|
|
AND turn_id IN ('turn-1', 'turn-2')
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, updated, err := store.UpdateSessionTitle(
|
|
ctx, "ws-1", "source", "Claude session",
|
|
); err != nil || !updated {
|
|
t.Fatalf("UpdateSessionTitle() updated=%v error=%v", updated, err)
|
|
}
|
|
input := SessionForkPrepare{
|
|
OperationID: "fork-provider-owned", WorkspaceID: "ws-1",
|
|
RequestID: "request-provider-owned", RequestHash: "hash-provider-owned",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-provider-owned",
|
|
SourceTurnID: "turn-2", DriverKind: "claude-agent-sdk-session-fork",
|
|
DriverVersion: "0.3.201/sidecar-v3", OccurredAtUnixMS: 500,
|
|
}
|
|
operation, _, err := prepareSessionForkForTest(t, store, ctx, input)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if operation.TargetTitle == "" {
|
|
t.Fatal("prepared operation omitted the frozen target title")
|
|
}
|
|
if string(operation.SourceProviderTurnBindingJSON) !=
|
|
`{"schemaVersion":1,"checkpointMessageId":"claude-source-checkpoint-2"}` {
|
|
t.Fatalf("prepared operation=%#v", operation)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(
|
|
ctx, input.WorkspaceID, input.OperationID, 501,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
operation, _, err = store.RecordSessionForkProviderResult(
|
|
ctx,
|
|
SessionForkProviderResult{
|
|
WorkspaceID: input.WorkspaceID, OperationID: input.OperationID,
|
|
Status: SessionForkStatusProviderAccepted,
|
|
TargetProviderSessionID: "claude-child",
|
|
TargetProviderTurnBindings: []SessionForkProviderTurnBinding{
|
|
{
|
|
ProviderTurnID: "claude-child-turn-1",
|
|
ProviderTurnBindingJSON: json.RawMessage(
|
|
`{"checkpointMessageId":"claude-child-checkpoint-1","schemaVersion":1}`,
|
|
),
|
|
},
|
|
{
|
|
ProviderTurnID: "claude-child-turn-2",
|
|
ProviderTurnBindingJSON: json.RawMessage(
|
|
`{"checkpointMessageId":"claude-child-checkpoint-2","schemaVersion":1}`,
|
|
),
|
|
},
|
|
},
|
|
StateBindingMode: "provider_owned",
|
|
StateBindingReceipt: "claude-sdk-fork-v3:receipt",
|
|
OccurredAtUnixMS: 502,
|
|
},
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if operation.StateBindingMode != "provider_owned" ||
|
|
operation.StateBindingReceipt == "" ||
|
|
!reflect.DeepEqual(
|
|
operation.TargetProviderTurnBindings,
|
|
[]SessionForkProviderTurnBinding{
|
|
{
|
|
ProviderTurnID: "claude-child-turn-1",
|
|
ProviderTurnBindingJSON: json.RawMessage(
|
|
`{"checkpointMessageId":"claude-child-checkpoint-1","schemaVersion":1}`,
|
|
),
|
|
},
|
|
{
|
|
ProviderTurnID: "claude-child-turn-2",
|
|
ProviderTurnBindingJSON: json.RawMessage(
|
|
`{"checkpointMessageId":"claude-child-checkpoint-2","schemaVersion":1}`,
|
|
),
|
|
},
|
|
},
|
|
) {
|
|
t.Fatalf("provider acceptance evidence=%#v", operation)
|
|
}
|
|
result, err := store.CommitSessionFork(ctx, input.WorkspaceID, input.OperationID, 503)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for index, sourceTurnID := range []string{"turn-1", "turn-2"} {
|
|
targetTurnID := deterministicSessionForkCanonicalID(
|
|
operation,
|
|
"turn",
|
|
sourceTurnID,
|
|
)
|
|
turn, found, err := store.GetTurn(
|
|
ctx,
|
|
input.WorkspaceID,
|
|
input.TargetAgentSessionID,
|
|
targetTurnID,
|
|
)
|
|
if err != nil || !found {
|
|
t.Fatalf("GetTurn(%s) found=%v error=%v", targetTurnID, found, err)
|
|
}
|
|
wantProviderTurnID := fmt.Sprintf("claude-child-turn-%d", index+1)
|
|
wantBindingJSON := fmt.Sprintf(
|
|
`{"checkpointMessageId":"claude-child-checkpoint-%d","schemaVersion":1}`,
|
|
index+1,
|
|
)
|
|
if turn.RootProviderTurnID != wantProviderTurnID ||
|
|
string(turn.ProviderTurnBindingJSON) != wantBindingJSON {
|
|
t.Fatalf(
|
|
"child provider binding=%#v, want turn=%q binding=%q",
|
|
turn,
|
|
wantProviderTurnID,
|
|
wantBindingJSON,
|
|
)
|
|
}
|
|
if _, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx,
|
|
input.WorkspaceID,
|
|
input.TargetAgentSessionID,
|
|
targetTurnID,
|
|
); err != nil || !supported {
|
|
t.Fatalf(
|
|
"CheckSessionForkThroughTurn(%s) supported=%v error=%v",
|
|
targetTurnID,
|
|
supported,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
if result.Lineage.TargetTurnID != deterministicSessionForkCanonicalID(
|
|
operation,
|
|
"turn",
|
|
"turn-2",
|
|
) {
|
|
t.Fatalf("lineage=%#v", result.Lineage)
|
|
}
|
|
|
|
firstTargetTurnID := deterministicSessionForkCanonicalID(
|
|
operation,
|
|
"turn",
|
|
"turn-1",
|
|
)
|
|
secondInput := SessionForkPrepare{
|
|
OperationID: "fork-provider-owned-again", WorkspaceID: "ws-1",
|
|
RequestID: "request-provider-owned-again", RequestHash: "hash-provider-owned-again",
|
|
SourceAgentSessionID: input.TargetAgentSessionID,
|
|
TargetAgentSessionID: "target-provider-owned-again",
|
|
SourceTurnID: firstTargetTurnID,
|
|
DriverKind: "claude-agent-sdk-session-fork",
|
|
DriverVersion: "0.3.220/sidecar-v8-full-turn-bindings",
|
|
OccurredAtUnixMS: 504,
|
|
}
|
|
secondOperation, _, err := prepareSessionForkForTest(
|
|
t,
|
|
store,
|
|
ctx,
|
|
secondInput,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(
|
|
ctx,
|
|
secondInput.WorkspaceID,
|
|
secondInput.OperationID,
|
|
505,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.RecordSessionForkProviderResult(
|
|
ctx,
|
|
SessionForkProviderResult{
|
|
WorkspaceID: secondInput.WorkspaceID,
|
|
OperationID: secondInput.OperationID,
|
|
Status: SessionForkStatusProviderAccepted,
|
|
TargetProviderSessionID: "claude-grandchild",
|
|
TargetProviderTurnBindings: []SessionForkProviderTurnBinding{{
|
|
ProviderTurnID: "claude-grandchild-turn-1",
|
|
ProviderTurnBindingJSON: json.RawMessage(
|
|
`{"schemaVersion":1,"checkpointMessageId":"claude-grandchild-checkpoint-1"}`,
|
|
),
|
|
}},
|
|
StateBindingMode: "provider_owned",
|
|
StateBindingReceipt: "claude-sdk-fork-v3:grandchild-receipt",
|
|
OccurredAtUnixMS: 506,
|
|
},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secondResult, err := store.CommitSessionFork(
|
|
ctx,
|
|
secondInput.WorkspaceID,
|
|
secondInput.OperationID,
|
|
507,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
secondTargetTurnID := deterministicSessionForkCanonicalID(
|
|
secondOperation,
|
|
"turn",
|
|
firstTargetTurnID,
|
|
)
|
|
if secondResult.Lineage.TargetTurnID != secondTargetTurnID {
|
|
t.Fatalf("second lineage=%#v", secondResult.Lineage)
|
|
}
|
|
if _, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx,
|
|
secondInput.WorkspaceID,
|
|
secondInput.TargetAgentSessionID,
|
|
secondTargetTurnID,
|
|
); err != nil || !supported {
|
|
t.Fatalf(
|
|
"fork-of-fork boundary supported=%v error=%v",
|
|
supported,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkTitlesIncrementAcrossForkFamily(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, updated, err := store.UpdateSessionTitle(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
"123",
|
|
); err != nil || !updated {
|
|
t.Fatalf("UpdateSessionTitle() updated=%v error=%v", updated, err)
|
|
}
|
|
first := commitFork(t, store, SessionForkPrepare{
|
|
OperationID: "fork-title-1", WorkspaceID: "ws-1",
|
|
RequestID: "request-title-1", RequestHash: "hash-title-1",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-title-1",
|
|
SourceTurnID: "turn-1", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
})
|
|
if first.Session.Title != "123 (2)" {
|
|
t.Fatalf("first Fork title=%q, want %q", first.Session.Title, "123 (2)")
|
|
}
|
|
second := commitFork(t, store, SessionForkPrepare{
|
|
OperationID: "fork-title-2", WorkspaceID: "ws-1",
|
|
RequestID: "request-title-2", RequestHash: "hash-title-2",
|
|
SourceAgentSessionID: "target-title-1",
|
|
TargetAgentSessionID: "target-title-2",
|
|
SourceTurnID: first.Operation.TargetTurnID,
|
|
DriverKind: "codex",
|
|
DriverVersion: "1",
|
|
OccurredAtUnixMS: 200,
|
|
})
|
|
if second.Session.Title != "123 (3)" {
|
|
t.Fatalf("second Fork title=%q, want %q", second.Session.Title, "123 (3)")
|
|
}
|
|
third := commitFork(t, store, SessionForkPrepare{
|
|
OperationID: "fork-title-3", WorkspaceID: "ws-1",
|
|
RequestID: "request-title-3", RequestHash: "hash-title-3",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-title-3",
|
|
SourceTurnID: "turn-2", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 300,
|
|
})
|
|
if third.Session.Title != "123 (4)" {
|
|
t.Fatalf("third Fork title=%q, want %q", third.Session.Title, "123 (4)")
|
|
}
|
|
}
|
|
|
|
func TestSessionForkV1SnapshotMaterializesSourceRuntimeContext(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := t.Context()
|
|
seedForkSession(t, store)
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_sessions
|
|
SET cwd = '/legacy/project',
|
|
settings_json = '{"model":"legacy-model","permissionModeId":"legacy-mode"}',
|
|
internal_runtime_context_json = '{"legacyRuntime":true}'
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
operation, _, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "legacy-fork", WorkspaceID: "ws-1", RequestID: "legacy-request",
|
|
RequestHash: "legacy-hash", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "legacy-target", SourceTurnID: "turn-1",
|
|
DriverKind: "codex-app-server", DriverVersion: "1", OccurredAtUnixMS: 300,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var snapshotJSON string
|
|
if err := store.db.QueryRowContext(ctx, `
|
|
SELECT snapshot_json
|
|
FROM workspace_agent_session_fork_operations
|
|
WHERE operation_id = 'legacy-fork'
|
|
`).Scan(&snapshotJSON); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var snapshot sessionForkSnapshot
|
|
if err := json.Unmarshal([]byte(snapshotJSON), &snapshot); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
type legacySessionForkSnapshot struct {
|
|
Version int `json:"version"`
|
|
BoundaryMessageID int64 `json:"boundaryMessageId"`
|
|
Session Session `json:"session"`
|
|
Turns []sessionForkTurnSnapshot `json:"turns"`
|
|
Messages []Message `json:"messages"`
|
|
Interactions []Interaction `json:"interactions"`
|
|
}
|
|
legacySnapshot := legacySessionForkSnapshot{
|
|
Version: 1,
|
|
BoundaryMessageID: snapshot.BoundaryMessageID,
|
|
Session: snapshot.Session,
|
|
Turns: snapshot.Turns,
|
|
Messages: snapshot.Messages,
|
|
Interactions: snapshot.Interactions,
|
|
}
|
|
legacyJSON, err := json.Marshal(legacySnapshot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_session_fork_operations
|
|
SET snapshot_json = ?, snapshot_hash = ?
|
|
WHERE operation_id = 'legacy-fork'
|
|
`, string(legacyJSON), hashSessionForkBytes(legacyJSON)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(
|
|
ctx, "ws-1", operation.OperationID, 301,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.RecordSessionForkProviderResult(
|
|
ctx,
|
|
SessionForkProviderResult{
|
|
WorkspaceID: "ws-1", OperationID: operation.OperationID,
|
|
Status: SessionForkStatusProviderAccepted, TargetProviderSessionID: "legacy-child",
|
|
OccurredAtUnixMS: 302,
|
|
},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
committed, err := store.CommitSessionFork(ctx, "ws-1", operation.OperationID, 303)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if committed.Session.Cwd != "/legacy/project" ||
|
|
committed.Session.Settings["permissionModeId"] != "legacy-mode" ||
|
|
committed.Session.InternalRuntimeContext["legacyRuntime"] != true {
|
|
t.Fatalf("materialized legacy child=%#v", committed.Session)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkCanonicalIdentityRemapIsDeterministicAndReferenceSafe(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Origin: "runtime",
|
|
Messages: []MessageUpdate{{
|
|
MessageID: "message-1", TurnID: "turn-1",
|
|
Role: "assistant", Kind: "text", Status: "completed",
|
|
Payload: map[string]any{
|
|
"kind": "agent_system_notice",
|
|
"noticeKind": "plan_implementation_completed",
|
|
"planTurnId": "turn-1",
|
|
"confirmedTurnId": "turn-1",
|
|
"sourceSessionId": "source",
|
|
"providerTurnId": "provider-turn-1",
|
|
"content": "source turn-1 message-1 request-1",
|
|
"structuredOutput": map[string]any{
|
|
"agentSessionId": "source",
|
|
"turnId": "turn-1",
|
|
"messageId": "message-1",
|
|
"requestId": "request-1",
|
|
},
|
|
},
|
|
OccurredAtUnixMS: 13,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_turns
|
|
SET completed_command_json = ?
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source' AND turn_id = 'turn-1'
|
|
`, encodeCompletedCommandJSON("", "", finalAssistantWatermark{
|
|
MessageID: "message-1",
|
|
Resolved: true,
|
|
})); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
INSERT INTO workspace_agent_interactions (
|
|
workspace_id, agent_session_id, request_id, turn_id, kind, status,
|
|
tool_name, input_json, output_json, metadata_json,
|
|
created_at_unix_ms, updated_at_unix_ms
|
|
) VALUES ('ws-1', 'source', 'request-1', 'turn-1', 'approval', 'pending', '',
|
|
'{"requestId":"request-1","toolCall":{"input":{"turnId":"turn-1","requestId":"request-1","messageId":"message-1"}},"content":"turn-1 request-1"}',
|
|
'{"requestId":"request-1","payload":{"turnId":"turn-1","requestId":"request-1","messageId":"message-1"}}',
|
|
'{"providerTurnId":"provider-turn-1","structured":{"agentSessionId":"source","turnId":"turn-1","requestId":"request-1"}}',
|
|
12, 13)
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
input := SessionForkPrepare{
|
|
OperationID: "fork-remap", WorkspaceID: "ws-1", RequestID: "request-remap",
|
|
RequestHash: "hash-remap", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-remap", SourceTurnID: "turn-1",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
}
|
|
result := commitFork(t, store, input)
|
|
expectedTurnID := deterministicSessionForkCanonicalID(result.Operation, "turn", "turn-1")
|
|
expectedMessageID := deterministicSessionForkCanonicalID(result.Operation, "message", "message-1")
|
|
expectedRequestID := deterministicSessionForkCanonicalID(
|
|
result.Operation,
|
|
"interaction",
|
|
"turn-1\x00request-1",
|
|
)
|
|
if expectedTurnID == "turn-1" || expectedMessageID == "message-1" ||
|
|
expectedRequestID == "request-1" {
|
|
t.Fatal("fork canonical identities were not isolated from the source")
|
|
}
|
|
|
|
turns, err := store.ListSessionTurns(ctx, "ws-1", "target-remap")
|
|
if err != nil || len(turns) != 1 ||
|
|
turns[0].TurnID != expectedTurnID ||
|
|
turns[0].FinalAssistantMessageID != expectedMessageID {
|
|
t.Fatalf("forked turns=%#v error=%v", turns, err)
|
|
}
|
|
page, found, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{
|
|
WorkspaceID: "ws-1", AgentSessionID: "target-remap", Limit: 10,
|
|
})
|
|
if err != nil || !found || len(page.Messages) != 1 {
|
|
t.Fatalf("forked messages=%#v found=%v error=%v", page.Messages, found, err)
|
|
}
|
|
message := page.Messages[0]
|
|
if message.MessageID != expectedMessageID || message.TurnID != expectedTurnID {
|
|
t.Fatalf("forked message identity=%#v", message)
|
|
}
|
|
if message.Payload["planTurnId"] != expectedTurnID ||
|
|
message.Payload["confirmedTurnId"] != expectedTurnID ||
|
|
message.Payload["kind"] != "agent_system_notice" {
|
|
t.Fatalf("forked message canonical refs=%#v", message.Payload)
|
|
}
|
|
structuredOutput, ok := message.Payload["structuredOutput"].(map[string]any)
|
|
if !ok ||
|
|
structuredOutput["agentSessionId"] != "source" ||
|
|
structuredOutput["turnId"] != "turn-1" ||
|
|
structuredOutput["messageId"] != "message-1" ||
|
|
structuredOutput["requestId"] != "request-1" {
|
|
t.Fatalf("structured output was rewritten=%#v", message.Payload["structuredOutput"])
|
|
}
|
|
if message.Payload["sourceSessionId"] != "source" ||
|
|
message.Payload["providerTurnId"] != "provider-turn-1" ||
|
|
message.Payload["content"] != "source turn-1 message-1 request-1" {
|
|
t.Fatalf("non-canonical payload text was rewritten: %#v", message.Payload)
|
|
}
|
|
|
|
interactions, err := store.ListSessionInteractions(ctx, ListSessionInteractionsInput{
|
|
WorkspaceID: "ws-1", AgentSessionID: "target-remap",
|
|
})
|
|
if err != nil || len(interactions) != 1 {
|
|
t.Fatalf("forked interactions=%#v error=%v", interactions, err)
|
|
}
|
|
interaction := interactions[0]
|
|
if interaction.TurnID != expectedTurnID ||
|
|
interaction.RequestID != expectedRequestID ||
|
|
interaction.Status != InteractionStatusSuperseded ||
|
|
interaction.Input["requestId"] != expectedRequestID ||
|
|
interaction.Input["content"] != "turn-1 request-1" ||
|
|
interaction.Output["requestId"] != expectedRequestID ||
|
|
interaction.Metadata["providerTurnId"] != "provider-turn-1" {
|
|
t.Fatalf("forked interaction=%#v", interaction)
|
|
}
|
|
toolCall := interaction.Input["toolCall"].(map[string]any)
|
|
toolInput := toolCall["input"].(map[string]any)
|
|
answerPayload := interaction.Output["payload"].(map[string]any)
|
|
metadataStructured := interaction.Metadata["structured"].(map[string]any)
|
|
if toolInput["turnId"] != "turn-1" ||
|
|
toolInput["requestId"] != "request-1" ||
|
|
toolInput["messageId"] != "message-1" ||
|
|
answerPayload["turnId"] != "turn-1" ||
|
|
answerPayload["requestId"] != "request-1" ||
|
|
answerPayload["messageId"] != "message-1" ||
|
|
metadataStructured["agentSessionId"] != "source" ||
|
|
metadataStructured["turnId"] != "turn-1" ||
|
|
metadataStructured["requestId"] != "request-1" {
|
|
t.Fatalf("interaction structured payload was rewritten=%#v", interaction)
|
|
}
|
|
sourceTurns, err := store.ListSessionTurns(ctx, "ws-1", "source")
|
|
if err != nil || len(sourceTurns) != 2 ||
|
|
sourceTurns[0].TurnID != "turn-1" ||
|
|
sourceTurns[0].FinalAssistantMessageID != "message-1" {
|
|
t.Fatalf("source turns changed=%#v error=%v", sourceTurns, err)
|
|
}
|
|
sourceInteractions, err := store.ListSessionInteractions(ctx, ListSessionInteractionsInput{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source",
|
|
})
|
|
if err != nil || len(sourceInteractions) != 1 ||
|
|
sourceInteractions[0].TurnID != "turn-1" ||
|
|
sourceInteractions[0].RequestID != "request-1" ||
|
|
sourceInteractions[0].Status != InteractionStatusPending {
|
|
t.Fatalf("source interactions changed=%#v error=%v", sourceInteractions, err)
|
|
}
|
|
|
|
replayed, err := store.CommitSessionFork(ctx, "ws-1", "fork-remap", 104)
|
|
if err != nil || replayed.Changed {
|
|
t.Fatalf("CommitSessionFork(replay) result=%#v error=%v", replayed, err)
|
|
}
|
|
replayedTurns, err := store.ListSessionTurns(ctx, "ws-1", "target-remap")
|
|
if err != nil || len(replayedTurns) != 1 || replayedTurns[0].TurnID != expectedTurnID {
|
|
t.Fatalf("replayed fork identities changed=%#v error=%v", replayedTurns, err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkCanonicalIdentityMapFailsClosed(t *testing.T) {
|
|
t.Parallel()
|
|
operation := SessionForkOperation{
|
|
OperationID: "fork-map", WorkspaceID: "ws-1",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target",
|
|
}
|
|
duplicateTurns := sessionForkSnapshot{
|
|
Turns: []sessionForkTurnSnapshot{
|
|
{Turn: Turn{TurnID: "turn-1"}},
|
|
{Turn: Turn{TurnID: "turn-1"}},
|
|
},
|
|
}
|
|
if _, err := buildSessionForkCanonicalIdentityMap(operation, duplicateTurns); err == nil {
|
|
t.Fatal("duplicate source turn identities unexpectedly produced a mapping")
|
|
}
|
|
danglingMessageTurn := sessionForkSnapshot{
|
|
Turns: []sessionForkTurnSnapshot{{Turn: Turn{TurnID: "turn-1"}}},
|
|
Messages: []Message{{
|
|
MessageID: "message-1",
|
|
TurnID: "turn-outside-prefix",
|
|
}},
|
|
}
|
|
if _, err := buildSessionForkCanonicalIdentityMap(operation, danglingMessageTurn); err == nil {
|
|
t.Fatal("message with an unmapped canonical turn unexpectedly produced a mapping")
|
|
}
|
|
first := deterministicSessionForkCanonicalID(operation, "turn", "turn-1")
|
|
if again := deterministicSessionForkCanonicalID(operation, "turn", "turn-1"); again != first {
|
|
t.Fatalf("canonical identity is not deterministic: first=%q again=%q", first, again)
|
|
}
|
|
otherTarget := operation
|
|
otherTarget.TargetAgentSessionID = "other-target"
|
|
if deterministicSessionForkCanonicalID(otherTarget, "turn", "turn-1") == first {
|
|
t.Fatal("canonical identity was not isolated by target session")
|
|
}
|
|
otherSource := operation
|
|
otherSource.SourceAgentSessionID = "other-source"
|
|
if deterministicSessionForkCanonicalID(otherSource, "turn", "turn-1") == first {
|
|
t.Fatal("canonical identity was not isolated by source session")
|
|
}
|
|
}
|
|
|
|
func TestSessionForkTypedRemapLeavesStructuredToolOutputUnchanged(t *testing.T) {
|
|
t.Parallel()
|
|
operation := SessionForkOperation{
|
|
OperationID: "fork-typed-output", WorkspaceID: "ws-1",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target",
|
|
}
|
|
message := Message{
|
|
MessageID: "message-1",
|
|
TurnID: "turn-1",
|
|
Kind: "tool_call",
|
|
Payload: map[string]any{
|
|
"callType": "interactive",
|
|
"requestId": "request-1",
|
|
"input": map[string]any{
|
|
"turnId": "turn-1",
|
|
"messageId": "message-1",
|
|
"requestId": "request-1",
|
|
},
|
|
"output": map[string]any{
|
|
"requestId": "request-1",
|
|
"payload": map[string]any{
|
|
"turnId": "turn-1",
|
|
"messageId": "message-1",
|
|
"requestId": "request-1",
|
|
"agentSessionId": "source",
|
|
},
|
|
},
|
|
},
|
|
}
|
|
snapshot := sessionForkSnapshot{
|
|
Turns: []sessionForkTurnSnapshot{{Turn: Turn{TurnID: "turn-1"}}},
|
|
Messages: []Message{message},
|
|
Interactions: []Interaction{{
|
|
TurnID: "turn-1", RequestID: "request-1", Kind: InteractionKindQuestion,
|
|
}},
|
|
}
|
|
identityMap, err := buildSessionForkCanonicalIdentityMap(operation, snapshot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
remapped, err := remapSessionForkMessage(message, identityMap)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
expectedRequestID := identityMap.InteractionIDs[sessionForkInteractionRef{
|
|
TurnID: "turn-1", RequestID: "request-1",
|
|
}]
|
|
if remapped.Payload["requestId"] != expectedRequestID {
|
|
t.Fatalf("root requestId=%#v want=%q", remapped.Payload["requestId"], expectedRequestID)
|
|
}
|
|
output := remapped.Payload["output"].(map[string]any)
|
|
if output["requestId"] != expectedRequestID {
|
|
t.Fatalf("output requestId=%#v want=%q", output["requestId"], expectedRequestID)
|
|
}
|
|
input := remapped.Payload["input"].(map[string]any)
|
|
structured := output["payload"].(map[string]any)
|
|
if input["turnId"] != "turn-1" ||
|
|
input["messageId"] != "message-1" ||
|
|
input["requestId"] != "request-1" ||
|
|
structured["turnId"] != "turn-1" ||
|
|
structured["messageId"] != "message-1" ||
|
|
structured["requestId"] != "request-1" ||
|
|
structured["agentSessionId"] != "source" {
|
|
t.Fatalf("structured tool data was rewritten: input=%#v output=%#v", input, structured)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkTypedRemapRejectsDanglingCanonicalReference(t *testing.T) {
|
|
t.Parallel()
|
|
operation := SessionForkOperation{
|
|
OperationID: "fork-dangling-ref", WorkspaceID: "ws-1",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target",
|
|
}
|
|
message := Message{
|
|
MessageID: "message-1",
|
|
TurnID: "turn-1",
|
|
Kind: "text",
|
|
Payload: map[string]any{
|
|
"kind": "agent_system_notice",
|
|
"noticeKind": "plan_implementation_completed",
|
|
"confirmedTurnId": "turn-outside-prefix",
|
|
},
|
|
}
|
|
snapshot := sessionForkSnapshot{
|
|
Turns: []sessionForkTurnSnapshot{{Turn: Turn{TurnID: "turn-1"}}},
|
|
Messages: []Message{message},
|
|
}
|
|
identityMap, err := buildSessionForkCanonicalIdentityMap(operation, snapshot)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := remapSessionForkMessage(message, identityMap); err == nil ||
|
|
!strings.Contains(err.Error(), `"confirmedTurnId"="turn-outside-prefix"`) {
|
|
t.Fatalf("dangling typed canonical reference error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestCheckSessionForkThroughTurnReturnsSelectedProviderTurn(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
boundary, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
"turn-2",
|
|
)
|
|
if err != nil || !supported {
|
|
t.Fatalf("CheckSessionForkThroughTurn() supported=%v error=%v", supported, err)
|
|
}
|
|
want := []string{"provider-turn-2"}
|
|
if len(boundary.RootProviderTurnIDs) != len(want) {
|
|
t.Fatalf("provider turn prefix=%v want=%v", boundary.RootProviderTurnIDs, want)
|
|
}
|
|
for index := range want {
|
|
if boundary.RootProviderTurnIDs[index] != want[index] {
|
|
t.Fatalf("provider turn prefix=%v want=%v", boundary.RootProviderTurnIDs, want)
|
|
}
|
|
}
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_turns
|
|
SET root_provider_turn_id = 'provider-turn-1'
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source' AND turn_id = 'turn-2'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if duplicateBoundary, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
"turn-2",
|
|
); err != nil || !supported || len(duplicateBoundary.RootProviderTurnIDs) != 1 ||
|
|
duplicateBoundary.RootProviderTurnIDs[0] != "provider-turn-1" {
|
|
t.Fatalf(
|
|
"duplicate provider turn prefix boundary=%#v supported=%v error=%v",
|
|
duplicateBoundary,
|
|
supported,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkRejectsLegacyCanonicalProviderTurnBinding(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_turns
|
|
SET root_provider_turn_id = turn_id
|
|
WHERE workspace_id = 'ws-1'
|
|
AND agent_session_id = 'source'
|
|
AND turn_id = 'turn-2'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
boundary, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
"turn-2",
|
|
)
|
|
if err != nil || supported ||
|
|
boundary.RejectionReason != SessionForkBoundaryReasonProviderTurnMissing {
|
|
t.Fatalf(
|
|
"CheckSessionForkThroughTurn() boundary=%#v supported=%v error=%v",
|
|
boundary,
|
|
supported,
|
|
err,
|
|
)
|
|
}
|
|
|
|
_, changed, err := store.PrepareSessionFork(ctx, SessionForkPrepare{
|
|
OperationID: "fork-legacy-binding", WorkspaceID: "ws-1",
|
|
RequestID: "request-legacy-binding", RequestHash: "hash-legacy-binding",
|
|
SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-legacy-binding",
|
|
SourceTurnID: "turn-2",
|
|
PointKind: SessionForkPointThroughTurn,
|
|
DriverKind: "claude-agent-sdk-session-fork",
|
|
DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
})
|
|
var boundaryErr *SessionForkBoundaryError
|
|
if changed || !errors.Is(err, ErrSessionForkTurnState) ||
|
|
!errors.As(err, &boundaryErr) ||
|
|
boundaryErr.Reason != SessionForkBoundaryReasonProviderTurnMissing {
|
|
t.Fatalf("PrepareSessionFork() changed=%v error=%v", changed, err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkRejectsUnverifiedHistoricalProviderTurnBinding(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_turns
|
|
SET root_provider_turn_id = 'synthetic-legacy',
|
|
provider_turn_binding_json = '{}'
|
|
WHERE workspace_id = 'ws-1'
|
|
AND agent_session_id = 'source'
|
|
AND turn_id = 'turn-2'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
boundary, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
"turn-2",
|
|
)
|
|
if err != nil || supported ||
|
|
boundary.RejectionReason != SessionForkBoundaryReasonProviderTurnMissing {
|
|
t.Fatalf(
|
|
"CheckSessionForkThroughTurn() boundary=%#v supported=%v error=%v",
|
|
boundary,
|
|
supported,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestListSessionForkTurnIdentitiesReturnsCanonicalSequence(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
|
|
identities, err := store.ListSessionForkTurnIdentities(ctx, "ws-1", "source")
|
|
if err != nil {
|
|
t.Fatalf("ListSessionForkTurnIdentities() error=%v", err)
|
|
}
|
|
want := []SessionForkTurnIdentity{
|
|
{
|
|
TurnID: "turn-1",
|
|
ProviderTurnID: "provider-turn-1",
|
|
Phase: TurnPhaseSettled,
|
|
},
|
|
{
|
|
TurnID: "turn-2",
|
|
ProviderTurnID: "provider-turn-2",
|
|
Phase: TurnPhaseSettled,
|
|
},
|
|
}
|
|
if len(identities) != len(want) {
|
|
t.Fatalf("ListSessionForkTurnIdentities()=%#v want=%#v", identities, want)
|
|
}
|
|
for index := range want {
|
|
if identities[index] != want[index] {
|
|
t.Fatalf(
|
|
"ListSessionForkTurnIdentities()[%d]=%#v want=%#v",
|
|
index,
|
|
identities[index],
|
|
want[index],
|
|
)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionForkThroughTurnIgnoresHistoricalProvenance(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
seedForkSession(t, store)
|
|
ctx := context.Background()
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_turn_sequences
|
|
SET provenance = 'legacy_unverified'
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source' AND turn_id = 'turn-1'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, supported, err := store.CheckSessionForkThroughTurn(ctx, "ws-1", "source", "turn-1"); err != nil || !supported {
|
|
t.Fatalf("CheckSessionForkThroughTurn() supported=%v error=%v", supported, err)
|
|
}
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-op", WorkspaceID: "ws-1", RequestID: "request-1",
|
|
RequestHash: "hash-1", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target", SourceTurnID: "turn-1",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
}); err != nil {
|
|
t.Fatalf("PrepareSessionFork() error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkThroughTurnStagesLocalAttachmentReferences(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Origin: "runtime",
|
|
Messages: []MessageUpdate{{
|
|
MessageID: "message-1", TurnID: "turn-1", Role: "user",
|
|
Kind: "text", Status: "completed",
|
|
Payload: map[string]any{"content": []any{map[string]any{
|
|
"type": "image", "mimeType": "image/png",
|
|
"attachmentId": "attachment-before-boundary",
|
|
}}},
|
|
OccurredAtUnixMS: 13,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx, "ws-1", "source", "turn-1",
|
|
); err != nil || !supported {
|
|
t.Fatalf("CheckSessionForkThroughTurn() supported=%v error=%v", supported, err)
|
|
}
|
|
operation, _, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-attachment", WorkspaceID: "ws-1",
|
|
RequestID: "request-attachment", RequestHash: "hash-attachment",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-attachment",
|
|
SourceTurnID: "turn-1", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PrepareSessionFork() error=%v", err)
|
|
}
|
|
bindings, err := store.ListSessionForkAttachmentBindings(ctx, "ws-1", operation.OperationID)
|
|
if err != nil || len(bindings) != 1 ||
|
|
bindings[0].SourceAttachmentID != "attachment-before-boundary" ||
|
|
bindings[0].TargetAttachmentID == bindings[0].SourceAttachmentID {
|
|
t.Fatalf("attachment bindings=%#v error=%v", bindings, err)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(
|
|
ctx, "ws-1", operation.OperationID, 101,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.RecordSessionForkProviderResult(
|
|
ctx,
|
|
SessionForkProviderResult{
|
|
WorkspaceID: "ws-1", OperationID: operation.OperationID,
|
|
Status: SessionForkStatusProviderAccepted,
|
|
TargetProviderSessionID: "provider-target-attachment",
|
|
OccurredAtUnixMS: 102,
|
|
},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.CommitSessionFork(ctx, "ws-1", operation.OperationID, 103); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
page, found, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{
|
|
WorkspaceID: "ws-1", AgentSessionID: "target-attachment", Limit: 10,
|
|
})
|
|
if err != nil || !found || len(page.Messages) != 1 {
|
|
t.Fatalf("target attachment messages=%#v found=%v error=%v", page.Messages, found, err)
|
|
}
|
|
content := page.Messages[0].Payload["content"].([]any)
|
|
image := content[0].(map[string]any)
|
|
if image["attachmentId"] != bindings[0].TargetAttachmentID {
|
|
t.Fatalf("target attachment payload=%#v binding=%#v", image, bindings[0])
|
|
}
|
|
}
|
|
|
|
func TestSessionForkThroughTurnIgnoresAttachmentReferencesAfterBoundary(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Origin: "runtime",
|
|
Messages: []MessageUpdate{{
|
|
MessageID: "message-2", TurnID: "turn-2", Role: "user",
|
|
Kind: "text", Status: "completed",
|
|
Payload: map[string]any{"content": []any{map[string]any{
|
|
"type": "image", "mimeType": "image/png",
|
|
"attachmentId": "attachment-after-boundary",
|
|
}}},
|
|
OccurredAtUnixMS: 23,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx, "ws-1", "source", "turn-1",
|
|
); err != nil || !supported {
|
|
t.Fatalf("CheckSessionForkThroughTurn() supported=%v error=%v", supported, err)
|
|
}
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-before-attachment", WorkspaceID: "ws-1",
|
|
RequestID: "request-before-attachment",
|
|
RequestHash: "hash-before-attachment",
|
|
SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-before-attachment",
|
|
SourceTurnID: "turn-1", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
}); err != nil {
|
|
t.Fatalf("PrepareSessionFork() error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkPrepareFreezesCurrentSnapshotDespiteSourceDrift(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, _, err := store.UpdateSessionTitle(
|
|
ctx, "ws-1", "source", "changed before prepare",
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, changed, err := store.PrepareSessionFork(ctx, SessionForkPrepare{
|
|
OperationID: "fork-stale-source", WorkspaceID: "ws-1",
|
|
RequestID: "request-stale-source", RequestHash: "hash-stale-source",
|
|
SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-stale-source",
|
|
SourceTurnID: "turn-1", PointKind: SessionForkPointThroughTurn,
|
|
DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
}); err != nil || !changed {
|
|
t.Fatalf("PrepareSessionFork() changed=%v error=%v", changed, err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkSnapshotUsesPresentationOrderAndIncludesSessionAudit(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Origin: "runtime",
|
|
Messages: []MessageUpdate{{
|
|
MessageID: "audit-1", Role: "system", Kind: "session_audit",
|
|
Status: "completed", OccurredAtUnixMS: 31,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
appendSettledForkTurn(t, store, "turn-3", 40)
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
UPDATE workspace_agent_messages
|
|
SET version = CASE message_id
|
|
WHEN 'message-1' THEN 99
|
|
WHEN 'message-2' THEN 1
|
|
ELSE version
|
|
END
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commitFork(t, store, SessionForkPrepare{
|
|
OperationID: "fork-order", WorkspaceID: "ws-1", RequestID: "request-order",
|
|
RequestHash: "hash-order", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-order", SourceTurnID: "turn-3",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
})
|
|
page, found, err := store.ListSessionMessages(ctx, ListSessionMessagesInput{
|
|
WorkspaceID: "ws-1", AgentSessionID: "target-order", Limit: 10,
|
|
})
|
|
if err != nil || !found {
|
|
t.Fatalf("ListSessionMessages() found=%v error=%v", found, err)
|
|
}
|
|
got := make([]string, 0, len(page.Messages))
|
|
for _, message := range page.Messages {
|
|
got = append(got, message.MessageID)
|
|
}
|
|
operation := SessionForkOperation{
|
|
OperationID: "fork-order", WorkspaceID: "ws-1",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-order",
|
|
}
|
|
wantSources := []string{"message-1", "message-2", "audit-1", "message-3"}
|
|
want := make([]string, len(wantSources))
|
|
for index, sourceMessageID := range wantSources {
|
|
want[index] = deterministicSessionForkCanonicalID(
|
|
operation,
|
|
"message",
|
|
sourceMessageID,
|
|
)
|
|
}
|
|
if len(got) != len(want) {
|
|
t.Fatalf("forked message order=%v want=%v", got, want)
|
|
}
|
|
for index := range want {
|
|
if got[index] != want[index] || page.Messages[index].Version != uint64(index+1) {
|
|
t.Fatalf("forked message order=%v want=%v", got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestSessionForkKeepsSourceWritesAndLifecycleAvailable(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
prepare := SessionForkPrepare{
|
|
OperationID: "fork-fence", WorkspaceID: "ws-1", RequestID: "request-fence",
|
|
RequestHash: "hash-fence", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-fence", SourceTurnID: "turn-1",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
}
|
|
if _, changed, err := prepareSessionForkForTest(t, store, ctx, prepare); err != nil || !changed {
|
|
t.Fatalf("PrepareSessionFork() changed=%v error=%v", changed, err)
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Provider: "codex",
|
|
OccurredAtUnixMS: 101,
|
|
}); err != nil {
|
|
t.Fatalf("ReportSessionState() error=%v", err)
|
|
}
|
|
if _, _, err := store.PrepareRuntimeOperation(ctx, RuntimeOperationPrepare{
|
|
OperationID: "runtime-after-fork", WorkspaceID: "ws-1",
|
|
AgentSessionID: "source", TurnID: "turn-2",
|
|
Kind: RuntimeOperationKindCancelTurn,
|
|
Payload: map[string]any{
|
|
"rootAgentSessionId": "source",
|
|
"targets": []any{map[string]any{
|
|
"agentSessionId": "source", "turnId": "turn-2",
|
|
}},
|
|
},
|
|
OccurredAtMS: 101,
|
|
}); err != nil {
|
|
t.Fatalf("PrepareRuntimeOperation() error=%v", err)
|
|
}
|
|
if _, err := store.PutGoalReconcileInbox(ctx, GoalReconcileInboxItem{
|
|
RequestID: "inbox-after-fork", WorkspaceID: "ws-1",
|
|
AgentSessionID: "source",
|
|
Payload: map[string]any{"phase": goalReconcilePhasePending},
|
|
CreatedAtUnixMS: 101,
|
|
}); err != nil {
|
|
t.Fatalf("PutGoalReconcileInbox() error=%v", err)
|
|
}
|
|
if _, _, err := store.UpdateSessionSettings(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
"gpt-5",
|
|
map[string]any{"reasoningEffort": "high"},
|
|
); err != nil {
|
|
t.Fatalf("UpdateSessionSettings() error=%v", err)
|
|
}
|
|
if _, updated, err := store.UpdateSessionTitle(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
"title may change while fork is live",
|
|
); err != nil || !updated {
|
|
t.Fatalf("UpdateSessionTitle() updated=%v error=%v", updated, err)
|
|
}
|
|
if _, updated, err := store.UpdateSessionPinned(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
true,
|
|
); err != nil || !updated {
|
|
t.Fatalf("UpdateSessionPinned() updated=%v error=%v", updated, err)
|
|
}
|
|
if _, _, _, err := store.PrepareGoalControlOperation(ctx, GoalControlOperationPrepare{
|
|
OperationID: "goal-after-fork", WorkspaceID: "ws-1",
|
|
AgentSessionID: "source", Action: "set", Objective: "ship",
|
|
OccurredAtUnixMS: 101,
|
|
}); err != nil {
|
|
t.Fatalf("PrepareGoalControlOperation() error=%v", err)
|
|
}
|
|
if _, _, err := store.PrepareSubmitClaim(ctx, SubmitClaimPrepare{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source",
|
|
ClientSubmitID: "submit-after-fork", CanonicalTurnID: "turn-after-fork",
|
|
NowUnixMS: 101,
|
|
}); err != nil {
|
|
t.Fatalf("PrepareSubmitClaim() error=%v", err)
|
|
}
|
|
_, created, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-other", WorkspaceID: "ws-1", RequestID: "request-other",
|
|
RequestHash: "hash-other", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-other", SourceTurnID: "turn-1",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 101,
|
|
})
|
|
if !errors.Is(err, ErrSessionForkInProgress) || created {
|
|
t.Fatalf(
|
|
"second PrepareSessionFork() created=%v error=%v",
|
|
created,
|
|
err,
|
|
)
|
|
}
|
|
if _, err := store.PlanDeleteSessions(ctx, DeleteSessionsBatchInput{
|
|
WorkspaceID: "ws-1", SessionIDs: []string{"source"},
|
|
}); !errors.Is(err, ErrSessionForkInProgress) {
|
|
t.Fatalf("PlanDeleteSessions(source) error=%v", err)
|
|
}
|
|
if _, err := store.PlanDeleteSessions(ctx, DeleteSessionsBatchInput{
|
|
WorkspaceID: "ws-1", SessionIDs: []string{"target-fence"},
|
|
}); !errors.Is(err, ErrSessionForkTargetReserved) {
|
|
t.Fatalf("PlanDeleteSessions(target) error=%v", err)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(ctx, "ws-1", "fork-fence", 102); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.RecordSessionForkProviderResult(ctx, SessionForkProviderResult{
|
|
WorkspaceID: "ws-1", OperationID: "fork-fence",
|
|
Status: SessionForkStatusProviderAccepted, TargetProviderSessionID: "provider-target-fence",
|
|
OccurredAtUnixMS: 103,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Provider: "codex",
|
|
OccurredAtUnixMS: 104,
|
|
}); err != nil {
|
|
t.Fatalf("ReportSessionState(provider accepted) error=%v", err)
|
|
}
|
|
if _, err := store.CommitSessionFork(ctx, "ws-1", "fork-fence", 105); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Provider: "codex",
|
|
ProviderSessionID: "provider-source", OccurredAtUnixMS: 106,
|
|
}); err != nil {
|
|
t.Fatalf("ReportSessionState(after commit) error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkAllowsRootTurnPatchFromChildReport(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Kind: SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Status: "ready", CurrentPhase: "idle", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
appendSettledForkTurn(t, store, "turn-1", 10)
|
|
if _, err := store.ReportActivityState(ctx, ActivityStateReport{
|
|
Session: SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Kind: SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Status: "active", CurrentPhase: "working", OccurredAtUnixMS: 20,
|
|
},
|
|
Turn: &TurnTransition{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", TurnID: "turn-2",
|
|
Phase: TurnPhaseRunning, OccurredAtUnixMS: 20,
|
|
},
|
|
RootProviderTurn: &RootProviderTurnTransition{
|
|
WorkspaceID: "ws-1", RootAgentSessionID: "source", RootTurnID: "turn-2",
|
|
ProviderTurnID: "provider-turn-2", Phase: RootProviderTurnPhaseRunning,
|
|
OccurredAtUnixMS: 20,
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1",
|
|
AgentSessionID: "child",
|
|
Kind: SessionKindChild,
|
|
RootAgentSessionID: "source",
|
|
RootTurnID: "turn-2",
|
|
ParentAgentSessionID: "source",
|
|
ParentTurnID: "turn-2",
|
|
ParentToolCallID: "tool-child",
|
|
Origin: "runtime",
|
|
Provider: "codex",
|
|
ProviderSessionID: "provider-child",
|
|
OccurredAtUnixMS: 50,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Origin: "runtime",
|
|
Messages: []MessageUpdate{{
|
|
MessageID: "message-2", TurnID: "turn-2", Role: "assistant",
|
|
Kind: "text", Status: "completed",
|
|
Payload: map[string]any{"sourceSessionId": "source"},
|
|
OccurredAtUnixMS: 21,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportRootProviderTurn(
|
|
t, store, "source", "turn-2", "provider-turn-2",
|
|
RootProviderTurnPhaseCompleted, 22,
|
|
)
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-root-patch", WorkspaceID: "ws-1",
|
|
RequestID: "request-root-patch", RequestHash: "hash-root-patch",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-root-patch",
|
|
SourceTurnID: "turn-1", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
tx, err := store.db.BeginTx(ctx, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer func() {
|
|
_ = tx.Rollback()
|
|
}()
|
|
if _, _, _, err := store.applyRootProviderTurnTransitionTx(
|
|
ctx,
|
|
tx,
|
|
RootProviderTurnTransition{
|
|
WorkspaceID: "ws-1", RootAgentSessionID: "source",
|
|
RootTurnID: "turn-1", ProviderTurnID: "provider-turn-1",
|
|
Phase: RootProviderTurnPhaseCompleted, Outcome: TurnOutcomeFailed,
|
|
OccurredAtUnixMS: 101,
|
|
},
|
|
101,
|
|
); err != nil {
|
|
t.Fatalf("child root provider patch error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkAllowsDescendantLaneInsideBoundary(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.ReportActivityState(ctx, ActivityStateReport{
|
|
Session: SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Kind: SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Status: "active", CurrentPhase: "working", OccurredAtUnixMS: 40,
|
|
},
|
|
Turn: &TurnTransition{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", TurnID: "turn-3",
|
|
Phase: TurnPhaseRunning, OccurredAtUnixMS: 40,
|
|
},
|
|
RootProviderTurn: &RootProviderTurnTransition{
|
|
WorkspaceID: "ws-1", RootAgentSessionID: "source", RootTurnID: "turn-3",
|
|
ProviderTurnID: "provider-turn-3", Phase: RootProviderTurnPhaseRunning,
|
|
OccurredAtUnixMS: 40,
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1",
|
|
AgentSessionID: "child",
|
|
Kind: SessionKindChild,
|
|
RootAgentSessionID: "source",
|
|
RootTurnID: "turn-3",
|
|
ParentAgentSessionID: "source",
|
|
ParentTurnID: "turn-3",
|
|
ParentToolCallID: "tool-child",
|
|
Origin: "runtime",
|
|
Provider: "codex",
|
|
ProviderSessionID: "provider-child",
|
|
OccurredAtUnixMS: 50,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Origin: "runtime",
|
|
Messages: []MessageUpdate{{
|
|
MessageID: "message-3", TurnID: "turn-3", Role: "assistant",
|
|
Kind: "text", Status: "completed",
|
|
Payload: map[string]any{"sourceSessionId": "source"},
|
|
OccurredAtUnixMS: 41,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportRootProviderTurn(
|
|
t, store, "source", "turn-3", "provider-turn-3",
|
|
RootProviderTurnPhaseCompleted, 42,
|
|
)
|
|
if _, supported, err := store.CheckSessionForkThroughTurn(
|
|
ctx, "ws-1", "source", "turn-3",
|
|
); err != nil || !supported {
|
|
t.Fatalf("CheckSessionForkThroughTurn() supported=%v error=%v", supported, err)
|
|
}
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-descendant", WorkspaceID: "ws-1",
|
|
RequestID: "request-descendant", RequestHash: "hash-descendant",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-descendant",
|
|
SourceTurnID: "turn-3", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
}); err != nil {
|
|
t.Fatalf("PrepareSessionFork() error=%v", err)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkFailedAndUnknownReleaseSourceFence(t *testing.T) {
|
|
t.Parallel()
|
|
for _, terminalStatus := range []string{
|
|
SessionForkStatusFailed,
|
|
SessionForkStatusUnknown,
|
|
} {
|
|
terminalStatus := terminalStatus
|
|
t.Run(terminalStatus, func(t *testing.T) {
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
input := SessionForkPrepare{
|
|
OperationID: "fork-" + terminalStatus, WorkspaceID: "ws-1",
|
|
RequestID: "request-" + terminalStatus, RequestHash: "hash-" + terminalStatus,
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-" + terminalStatus,
|
|
SourceTurnID: "turn-1", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 100,
|
|
}
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(ctx, "ws-1", input.OperationID, 101); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.RecordSessionForkProviderResult(ctx, SessionForkProviderResult{
|
|
WorkspaceID: "ws-1", OperationID: input.OperationID,
|
|
Status: terminalStatus, LastError: "terminal", OccurredAtUnixMS: 102,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
unresolved, found, err := store.GetUnknownSessionForkOperation(
|
|
ctx,
|
|
"ws-1",
|
|
"source",
|
|
SessionForkPointThroughTurn,
|
|
"turn-1",
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if terminalStatus == SessionForkStatusUnknown {
|
|
if !found || unresolved.OperationID != input.OperationID {
|
|
t.Fatalf("unknown lookup=%#v found=%v", unresolved, found)
|
|
}
|
|
} else if found {
|
|
t.Fatalf("failed operation appeared unresolved: %#v", unresolved)
|
|
}
|
|
if terminalStatus == SessionForkStatusUnknown {
|
|
if _, found, changed, err := store.AcknowledgeSessionForkOperation(
|
|
ctx,
|
|
"ws-1",
|
|
input.OperationID,
|
|
103,
|
|
); !errors.Is(err, ErrSessionForkTransition) || !found || changed {
|
|
t.Fatalf(
|
|
"acknowledge unknown found=%v changed=%v error=%v",
|
|
found,
|
|
changed,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Provider: "codex",
|
|
ProviderSessionID: "provider-source", OccurredAtUnixMS: 103,
|
|
}); err != nil {
|
|
t.Fatalf("ReportSessionState(after %s) error=%v", terminalStatus, err)
|
|
}
|
|
next, created, err := prepareSessionForkForTest(t, store, ctx, SessionForkPrepare{
|
|
OperationID: "fork-next-" + terminalStatus, WorkspaceID: "ws-1",
|
|
RequestID: "request-next-" + terminalStatus, RequestHash: "hash-next-" + terminalStatus,
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-next-" + terminalStatus,
|
|
SourceTurnID: "turn-1", DriverKind: "codex", DriverVersion: "1",
|
|
OccurredAtUnixMS: 104,
|
|
})
|
|
if err != nil {
|
|
t.Fatalf("PrepareSessionFork(after %s) error=%v", terminalStatus, err)
|
|
}
|
|
if !created || next.OperationID == input.OperationID {
|
|
t.Fatalf("terminal operation did not release boundary: %#v", next)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRetryUnknownSessionForkReopensDurableDispatchMarker(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
input := SessionForkPrepare{
|
|
OperationID: "fork-retry", WorkspaceID: "ws-1",
|
|
RequestID: "request-retry", RequestHash: "hash-retry",
|
|
SourceAgentSessionID: "source", TargetAgentSessionID: "target-retry",
|
|
SourceTurnID: "turn-1", DriverKind: "claude",
|
|
DriverVersion: "deterministic-v1", OccurredAtUnixMS: 100,
|
|
}
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(
|
|
ctx, input.WorkspaceID, input.OperationID, 101,
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.RecordSessionForkProviderResult(
|
|
ctx,
|
|
SessionForkProviderResult{
|
|
WorkspaceID: input.WorkspaceID, OperationID: input.OperationID,
|
|
Status: SessionForkStatusUnknown, LastError: "response lost",
|
|
OccurredAtUnixMS: 102,
|
|
},
|
|
); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
operation, changed, err := store.RetryUnknownSessionFork(
|
|
ctx, input.WorkspaceID, input.OperationID, 103,
|
|
)
|
|
if err != nil || !changed ||
|
|
operation.Status != SessionForkStatusDispatching ||
|
|
operation.LastError != "" ||
|
|
operation.CompletedAtUnixMS != 0 ||
|
|
operation.DispatchedAtUnixMS != 103 {
|
|
t.Fatalf(
|
|
"RetryUnknownSessionFork() operation=%#v changed=%v error=%v",
|
|
operation,
|
|
changed,
|
|
err,
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestSessionForkPrepareDoesNotRequireGoalOrRuntimeQuiescence(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
busy func(*testing.T, *Store)
|
|
}{
|
|
{
|
|
name: "goal control",
|
|
busy: func(t *testing.T, store *Store) {
|
|
_, _, _, err := store.PrepareGoalControlOperation(context.Background(), GoalControlOperationPrepare{
|
|
OperationID: "goal-op", WorkspaceID: "ws-1", AgentSessionID: "source",
|
|
Action: "set", Objective: "ship", OccurredAtUnixMS: 50,
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "runtime operation",
|
|
busy: func(t *testing.T, store *Store) {
|
|
if _, err := store.db.ExecContext(context.Background(), `
|
|
INSERT INTO workspace_agent_runtime_operations (
|
|
operation_id, workspace_id, agent_session_id, kind, status, turn_id,
|
|
payload_json, next_attempt_at_unix_ms, created_at_unix_ms, updated_at_unix_ms
|
|
) VALUES ('runtime-op', 'ws-1', 'source', 'cancel_turn', 'prepared', 'turn-2',
|
|
'{}', 50, 50, 50)
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "reporter barrier",
|
|
busy: func(t *testing.T, store *Store) {
|
|
if _, err := store.PutGoalReconcileInbox(context.Background(), GoalReconcileInboxItem{
|
|
RequestID: "inbox-1", WorkspaceID: "ws-1", AgentSessionID: "source",
|
|
Payload: map[string]any{"phase": goalReconcilePhasePending}, CreatedAtUnixMS: 50,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "submit claim",
|
|
busy: func(t *testing.T, store *Store) {
|
|
if _, _, err := store.PrepareSubmitClaim(context.Background(), SubmitClaimPrepare{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source",
|
|
ClientSubmitID: "submit-1", CanonicalTurnID: "turn-2",
|
|
NowUnixMS: 50,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "legacy submit claim without canonical turn",
|
|
busy: func(t *testing.T, store *Store) {
|
|
if _, err := store.db.ExecContext(context.Background(), `
|
|
INSERT INTO workspace_agent_submit_claims (
|
|
workspace_id, agent_session_id, client_submit_id, status, turn_id,
|
|
created_at_unix_ms, updated_at_unix_ms, canonical_turn_id
|
|
) VALUES ('ws-1', 'source', 'legacy-submit', 'prepared', NULL, 50, 50, NULL)
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
seedForkSession(t, store)
|
|
test.busy(t, store)
|
|
if _, _, err := prepareSessionForkForTest(t, store, context.Background(), SessionForkPrepare{
|
|
OperationID: "fork-busy", WorkspaceID: "ws-1", RequestID: "request-busy",
|
|
RequestHash: "hash-busy", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-busy", SourceTurnID: "turn-2",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
}); err != nil {
|
|
t.Fatalf("PrepareSessionFork() error=%v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionForkCommitUsesFrozenPrefixAndReprovesProviderIdentity(t *testing.T) {
|
|
t.Parallel()
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*testing.T, *Store)
|
|
wantErr error
|
|
}{
|
|
{
|
|
name: "prefix",
|
|
mutate: func(t *testing.T, store *Store) {
|
|
if _, err := store.db.ExecContext(context.Background(), `
|
|
UPDATE workspace_agent_messages
|
|
SET payload_json = '{"changed":true}'
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source' AND message_id = 'message-1'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
},
|
|
{
|
|
name: "provider identity",
|
|
mutate: func(t *testing.T, store *Store) {
|
|
if _, err := store.db.ExecContext(context.Background(), `
|
|
UPDATE workspace_agent_sessions
|
|
SET provider_session_id = 'provider-source-changed'
|
|
WHERE workspace_id = 'ws-1' AND agent_session_id = 'source'
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
},
|
|
wantErr: ErrSessionForkSourceState,
|
|
},
|
|
}
|
|
for _, test := range tests {
|
|
test := test
|
|
t.Run(test.name, func(t *testing.T) {
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
seedForkSession(t, store)
|
|
prepareAcceptedFork(t, store, SessionForkPrepare{
|
|
OperationID: "fork-proof", WorkspaceID: "ws-1", RequestID: "request-proof",
|
|
RequestHash: "hash-proof", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-proof", SourceTurnID: "turn-1",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
})
|
|
test.mutate(t, store)
|
|
_, err := store.CommitSessionFork(context.Background(), "ws-1", "fork-proof", 103)
|
|
if test.wantErr != nil {
|
|
if !errors.Is(err, test.wantErr) {
|
|
t.Fatalf("CommitSessionFork() error=%v", err)
|
|
}
|
|
} else if err != nil {
|
|
t.Fatalf("CommitSessionFork() error=%v", err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestSessionForkCommitDeltaIncludesClonedInteraction(t *testing.T) {
|
|
t.Parallel()
|
|
participant := &testTransactionParticipant{}
|
|
store := openParticipantTestStore(t, participant)
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
if _, err := store.db.ExecContext(ctx, `
|
|
INSERT INTO workspace_agent_interactions (
|
|
workspace_id, agent_session_id, request_id, turn_id, kind, status,
|
|
tool_name, input_json, output_json, metadata_json,
|
|
created_at_unix_ms, updated_at_unix_ms
|
|
) VALUES ('ws-1', 'source', 'request-1', 'turn-1', 'question', 'answered',
|
|
'', '{}', '{}', '{}', 12, 13)
|
|
`); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
result := commitFork(t, store, SessionForkPrepare{
|
|
OperationID: "fork-delta", WorkspaceID: "ws-1", RequestID: "request-delta",
|
|
RequestHash: "hash-delta", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "target-delta", SourceTurnID: "turn-1",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
})
|
|
assertParticipantMutationKinds(t, result.CommitDelta,
|
|
MutationEntitySession, MutationEntityTurn, MutationEntityMessage,
|
|
MutationEntityInteraction, MutationEntityInteractionTree, MutationEntitySessionForkOperation)
|
|
assertParticipantMutationEntityID(t, result.CommitDelta,
|
|
MutationEntityInteraction,
|
|
deterministicSessionForkCanonicalID(result.Operation, "turn", "turn-1")+
|
|
"\x00"+
|
|
deterministicSessionForkCanonicalID(
|
|
result.Operation,
|
|
"interaction",
|
|
"turn-1\x00request-1",
|
|
),
|
|
)
|
|
}
|
|
|
|
func TestListRecoverableSessionForkOperationsPage(t *testing.T) {
|
|
t.Parallel()
|
|
store := openTestStore(t, testOptions(&staticProjectPaths{}))
|
|
ctx := context.Background()
|
|
seedForkSession(t, store)
|
|
baseFork := commitFork(t, store, SessionForkPrepare{
|
|
OperationID: "fork-base", WorkspaceID: "ws-1", RequestID: "request-base",
|
|
RequestHash: "hash-base", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "forked-source", SourceTurnID: "turn-1",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 100,
|
|
})
|
|
if _, found, _, err := store.AcknowledgeSessionForkOperation(
|
|
ctx,
|
|
"ws-1",
|
|
baseFork.Operation.OperationID,
|
|
150,
|
|
); err != nil || !found {
|
|
t.Fatalf("acknowledge base fork found=%v error=%v", found, err)
|
|
}
|
|
forkedSourceTurnID := deterministicSessionForkCanonicalID(
|
|
baseFork.Operation,
|
|
"turn",
|
|
"turn-1",
|
|
)
|
|
for _, input := range []SessionForkPrepare{
|
|
{
|
|
OperationID: "recover-a", WorkspaceID: "ws-1", RequestID: "recover-request-a",
|
|
RequestHash: "recover-hash-a", SourceAgentSessionID: "source",
|
|
TargetAgentSessionID: "recover-target-a", SourceTurnID: "turn-1",
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 200,
|
|
},
|
|
{
|
|
OperationID: "recover-b", WorkspaceID: "ws-1", RequestID: "recover-request-b",
|
|
RequestHash: "recover-hash-b", SourceAgentSessionID: "forked-source",
|
|
TargetAgentSessionID: "recover-target-b", SourceTurnID: forkedSourceTurnID,
|
|
DriverKind: "codex", DriverVersion: "1", OccurredAtUnixMS: 200,
|
|
},
|
|
} {
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
first, err := store.ListRecoverableSessionForkOperationsPage(ctx, SessionForkRecoveryCursor{}, 1)
|
|
if err != nil || len(first) != 1 || first[0].OperationID != "recover-a" {
|
|
t.Fatalf("first recovery page=%#v error=%v", first, err)
|
|
}
|
|
second, err := store.ListRecoverableSessionForkOperationsPage(ctx, SessionForkRecoveryCursor{
|
|
CreatedAtUnixMS: first[0].CreatedAtUnixMS, OperationID: first[0].OperationID,
|
|
}, 1)
|
|
if err != nil || len(second) != 1 || second[0].OperationID != "recover-b" {
|
|
t.Fatalf("second recovery page=%#v error=%v", second, err)
|
|
}
|
|
}
|
|
|
|
func prepareSessionForkForTest(
|
|
t *testing.T,
|
|
store *Store,
|
|
ctx context.Context,
|
|
input SessionForkPrepare,
|
|
) (SessionForkOperation, bool, error) {
|
|
t.Helper()
|
|
if input.PointKind == "" {
|
|
input.PointKind = SessionForkPointThroughTurn
|
|
}
|
|
return store.PrepareSessionFork(ctx, input)
|
|
}
|
|
|
|
func seedForkSession(t *testing.T, store *Store) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
if _, err := store.ReportSessionState(ctx, SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Kind: SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Status: "ready", CurrentPhase: "idle", OccurredAtUnixMS: 1,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for index, turnID := range []string{"turn-1", "turn-2"} {
|
|
started := int64(10 + index*10)
|
|
providerTurnID := "provider-" + turnID
|
|
if _, err := store.ReportActivityState(ctx, ActivityStateReport{
|
|
Session: SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Kind: SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Status: "active", CurrentPhase: "working", OccurredAtUnixMS: started,
|
|
},
|
|
Turn: &TurnTransition{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", TurnID: turnID,
|
|
Phase: TurnPhaseRunning, OccurredAtUnixMS: started,
|
|
},
|
|
RootProviderTurn: &RootProviderTurnTransition{
|
|
WorkspaceID: "ws-1", RootAgentSessionID: "source", RootTurnID: turnID,
|
|
ProviderTurnID: providerTurnID, Phase: RootProviderTurnPhaseRunning,
|
|
OccurredAtUnixMS: started,
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Origin: "runtime",
|
|
Messages: []MessageUpdate{{
|
|
MessageID: "message-" + string(rune('1'+index)), TurnID: turnID,
|
|
Role: "assistant", Kind: "text", Status: "completed",
|
|
Payload: map[string]any{"sourceSessionId": "source"},
|
|
OccurredAtUnixMS: started + 1,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportRootProviderTurn(t, store, "source", turnID, providerTurnID,
|
|
RootProviderTurnPhaseCompleted, started+2)
|
|
}
|
|
}
|
|
|
|
func appendSettledForkTurn(t *testing.T, store *Store, turnID string, started int64) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
providerTurnID := "provider-" + turnID
|
|
if _, err := store.ReportActivityState(ctx, ActivityStateReport{
|
|
Session: SessionStateReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Kind: SessionKindRoot,
|
|
Origin: "runtime", Provider: "codex", ProviderSessionID: "provider-source",
|
|
Status: "active", CurrentPhase: "working", OccurredAtUnixMS: started,
|
|
},
|
|
Turn: &TurnTransition{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", TurnID: turnID,
|
|
Phase: TurnPhaseRunning, OccurredAtUnixMS: started,
|
|
},
|
|
RootProviderTurn: &RootProviderTurnTransition{
|
|
WorkspaceID: "ws-1", RootAgentSessionID: "source", RootTurnID: turnID,
|
|
ProviderTurnID: providerTurnID, Phase: RootProviderTurnPhaseRunning,
|
|
OccurredAtUnixMS: started,
|
|
},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := store.ReportSessionMessages(ctx, SessionMessageReport{
|
|
WorkspaceID: "ws-1", AgentSessionID: "source", Origin: "runtime",
|
|
Messages: []MessageUpdate{{
|
|
MessageID: "message-" + turnID[len(turnID)-1:], TurnID: turnID,
|
|
Role: "assistant", Kind: "text", Status: "completed",
|
|
Payload: map[string]any{"sourceSessionId": "source"}, OccurredAtUnixMS: started + 1,
|
|
}},
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
reportRootProviderTurn(t, store, "source", turnID, providerTurnID,
|
|
RootProviderTurnPhaseCompleted, started+2)
|
|
}
|
|
|
|
func prepareAcceptedFork(t *testing.T, store *Store, input SessionForkPrepare) {
|
|
t.Helper()
|
|
ctx := context.Background()
|
|
if _, _, err := prepareSessionForkForTest(t, store, ctx, input); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.MarkSessionForkDispatching(ctx, input.WorkspaceID, input.OperationID, input.OccurredAtUnixMS+1); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, _, err := store.RecordSessionForkProviderResult(ctx, SessionForkProviderResult{
|
|
WorkspaceID: input.WorkspaceID, OperationID: input.OperationID,
|
|
Status: SessionForkStatusProviderAccepted,
|
|
TargetProviderSessionID: "provider-" + input.TargetAgentSessionID,
|
|
OccurredAtUnixMS: input.OccurredAtUnixMS + 2,
|
|
}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func commitFork(t *testing.T, store *Store, input SessionForkPrepare) SessionForkCommitResult {
|
|
t.Helper()
|
|
prepareAcceptedFork(t, store, input)
|
|
result, err := store.CommitSessionFork(
|
|
context.Background(), input.WorkspaceID, input.OperationID, input.OccurredAtUnixMS+3,
|
|
)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return result
|
|
}
|