@@ -2,6 +2,7 @@ package runtime
22
33import (
44 "context"
5+ "errors"
56 "os"
67 "path/filepath"
78 "strings"
@@ -17,6 +18,7 @@ import (
1718
1819type checkpointStoreSpy struct {
1920 lastResume agentsession.ResumeCheckpoint
21+ setResumeErr error
2022 latestResume * agentsession.ResumeCheckpoint
2123 latestResumeErr error
2224 listRecords []agentsession.CheckpointRecord
@@ -56,7 +58,7 @@ func (s *checkpointStoreSpy) RestoreCheckpoint(context.Context, checkpoint.Resto
5658
5759func (s * checkpointStoreSpy ) SetResumeCheckpoint (_ context.Context , rc agentsession.ResumeCheckpoint ) error {
5860 s .lastResume = rc
59- return nil
61+ return s . setResumeErr
6062}
6163
6264func (s * checkpointStoreSpy ) PruneExpiredCheckpoints (context.Context , string , int ) (int , error ) {
@@ -342,6 +344,32 @@ func TestUpdateResumeCheckpoint(t *testing.T) {
342344 }
343345}
344346
347+ func TestUpdateResumeCheckpointSkipsWhenStoreUnavailable (t * testing.T ) {
348+ t .Parallel ()
349+
350+ fixture := newRuntimeCheckpointFixture (t )
351+ state := newRunState ("run-no-store" , fixture .session )
352+ service := & Service {}
353+ service .updateResumeCheckpoint (context .Background (), & state , "plan" , "" )
354+ }
355+
356+ func TestUpdateResumeCheckpointSwallowsStoreErrorAndUsesEffectiveWorkdir (t * testing.T ) {
357+ t .Parallel ()
358+
359+ fixture := newRuntimeCheckpointFixture (t )
360+ state := newRunState ("run-resume-workdir-fallback" , fixture .session )
361+ state .session .Workdir = " "
362+ state .effectiveWorkdir = fixture .workdir
363+ spy := & checkpointStoreSpy {setResumeErr : errors .New ("write failed" )}
364+ service := & Service {checkpointStore : spy }
365+
366+ service .updateResumeCheckpoint (context .Background (), & state , "verify" , "running" )
367+
368+ if spy .lastResume .WorkspaceKey != agentsession .WorkspacePathKey (fixture .workdir ) {
369+ t .Fatalf ("WorkspaceKey = %q, want %q" , spy .lastResume .WorkspaceKey , agentsession .WorkspacePathKey (fixture .workdir ))
370+ }
371+ }
372+
345373func TestApplyResumeCheckpointReplayPlanStrategy (t * testing.T ) {
346374 fixture := newRuntimeCheckpointFixture (t )
347375 state := newRunState ("run-resume-plan" , fixture .session )
@@ -575,6 +603,124 @@ func TestResumeCheckpointMatchesStateSupportsLegacyMessageCountFallback(t *testi
575603 }
576604}
577605
606+ func TestResolveResumeWorkspaceKey (t * testing.T ) {
607+ t .Parallel ()
608+
609+ sessionWorkdir := t .TempDir ()
610+ effectiveWorkdir := t .TempDir ()
611+ got := resolveResumeWorkspaceKey (sessionWorkdir , effectiveWorkdir )
612+ if got != agentsession .WorkspacePathKey (sessionWorkdir ) {
613+ t .Fatalf ("resolveResumeWorkspaceKey(session, effective) = %q, want %q" , got , agentsession .WorkspacePathKey (sessionWorkdir ))
614+ }
615+
616+ gotFallback := resolveResumeWorkspaceKey (" " , effectiveWorkdir )
617+ if gotFallback != agentsession .WorkspacePathKey (effectiveWorkdir ) {
618+ t .Fatalf ("resolveResumeWorkspaceKey(empty, effective) = %q, want %q" , gotFallback , agentsession .WorkspacePathKey (effectiveWorkdir ))
619+ }
620+ }
621+
622+ func TestResumeCheckpointMatchesStateBranches (t * testing.T ) {
623+ t .Parallel ()
624+
625+ workspace := agentsession .WorkspacePathKey (t .TempDir ())
626+ if resumeCheckpointMatchesState (agentsession.ResumeCheckpoint {}, workspace , 1 , 1 ) {
627+ t .Fatal ("expected empty checkpoint workspace to fail" )
628+ }
629+ if resumeCheckpointMatchesState (agentsession.ResumeCheckpoint {WorkspaceKey : workspace }, "" , 1 , 1 ) {
630+ t .Fatal ("expected empty current workspace to fail" )
631+ }
632+ if resumeCheckpointMatchesState (agentsession.ResumeCheckpoint {WorkspaceKey : workspace + "-other" , TranscriptRevision : 1 }, workspace , 1 , 1 ) {
633+ t .Fatal ("expected workspace mismatch to fail" )
634+ }
635+ if resumeCheckpointMatchesState (agentsession.ResumeCheckpoint {WorkspaceKey : workspace , TranscriptRevision : - 1 }, workspace , 1 , 1 ) {
636+ t .Fatal ("expected negative checkpoint revision to fail" )
637+ }
638+ if resumeCheckpointMatchesState (agentsession.ResumeCheckpoint {WorkspaceKey : workspace , TranscriptRevision : 1 }, workspace , - 1 , 1 ) {
639+ t .Fatal ("expected negative current revision to fail" )
640+ }
641+ if ! resumeCheckpointMatchesState (agentsession.ResumeCheckpoint {WorkspaceKey : workspace , TranscriptRevision : 9 }, workspace , 9 , 3 ) {
642+ t .Fatal ("expected exact current revision match to pass" )
643+ }
644+ if resumeCheckpointMatchesState (agentsession.ResumeCheckpoint {WorkspaceKey : workspace , TranscriptRevision : 8 }, workspace , 9 , - 1 ) {
645+ t .Fatal ("expected legacy fallback disabled on negative legacy revision" )
646+ }
647+ if resumeCheckpointMatchesState (agentsession.ResumeCheckpoint {WorkspaceKey : workspace , TranscriptRevision : 8 }, workspace , 9 , 7 ) {
648+ t .Fatal ("expected mismatch across current/legacy revisions to fail" )
649+ }
650+ }
651+
652+ func TestSessionTranscriptRevisionIncludesTodoAndPlanState (t * testing.T ) {
653+ t .Parallel ()
654+
655+ required := true
656+ base := agentsession .NewWithWorkdir ("resume-rich" , t .TempDir ())
657+ base .Messages = []providertypes.Message {
658+ {
659+ Role : providertypes .RoleUser ,
660+ Parts : []providertypes.ContentPart {
661+ providertypes .NewTextPart ("same-count" ),
662+ },
663+ },
664+ }
665+ base .TodoVersion = 2
666+ base .Todos = []agentsession.TodoItem {
667+ {
668+ ID : "todo-1" ,
669+ Content : "prepare report" ,
670+ Status : agentsession .TodoStatusInProgress ,
671+ Required : & required ,
672+ OwnerType : agentsession .TodoOwnerTypeAgent ,
673+ OwnerID : "agent-1" ,
674+ FailureReason : "" ,
675+ BlockedReason : agentsession .TodoBlockedReasonPermissionWait ,
676+ Revision : 3 ,
677+ UpdatedAt : time .Unix (1700020000 , 0 ).UTC (),
678+ },
679+ }
680+ base .TaskState = agentsession.TaskState {
681+ VerificationProfile : agentsession .VerificationProfileFixBug ,
682+ Goal : "fix bug" ,
683+ Progress : []string {" inspect logs " , "write test" },
684+ OpenItems : []string {" verify patch" },
685+ NextStep : " run tests " ,
686+ Blockers : []string {" permission " },
687+ KeyArtifacts : []string {" report.md " },
688+ Decisions : []string {" keep legacy fallback " },
689+ UserConstraints : []string {" no destructive ops " },
690+ LastUpdatedAt : time .Unix (1700020001 , 0 ).UTC (),
691+ }
692+ base .CurrentPlan = & agentsession.PlanArtifact {
693+ ID : "plan-1" ,
694+ Revision : 2 ,
695+ }
696+ base .LastFullPlanRevision = 2
697+ base .PlanApprovalPendingFullAlign = true
698+
699+ cloned := base
700+ cloned .TaskState .Progress = append ([]string (nil ), base .TaskState .Progress ... )
701+ cloned .TaskState .OpenItems = append ([]string (nil ), base .TaskState .OpenItems ... )
702+ cloned .TaskState .Blockers = append ([]string (nil ), base .TaskState .Blockers ... )
703+ cloned .TaskState .KeyArtifacts = append ([]string (nil ), base .TaskState .KeyArtifacts ... )
704+ cloned .TaskState .Decisions = append ([]string (nil ), base .TaskState .Decisions ... )
705+ cloned .TaskState .UserConstraints = append ([]string (nil ), base .TaskState .UserConstraints ... )
706+ cloned .Todos = append ([]agentsession.TodoItem (nil ), base .Todos ... )
707+ if len (cloned .Todos ) > 0 {
708+ requiredCopy := * cloned .Todos [0 ].Required
709+ cloned .Todos [0 ].Required = & requiredCopy
710+ }
711+
712+ before := sessionTranscriptRevision (base )
713+ after := sessionTranscriptRevision (cloned )
714+ if before != after {
715+ t .Fatalf ("expected equal revisions for equivalent rich state, got %d vs %d" , before , after )
716+ }
717+
718+ cloned .Todos [0 ].Status = agentsession .TodoStatusCompleted
719+ if sessionTranscriptRevision (base ) == sessionTranscriptRevision (cloned ) {
720+ t .Fatal ("expected todo status change to affect transcript revision" )
721+ }
722+ }
723+
578724func TestApplyResumeCheckpointSkipsWorkspaceMismatch (t * testing.T ) {
579725 fixture := newRuntimeCheckpointFixture (t )
580726 state := newRunState ("run-resume-workspace-mismatch" , fixture .session )
0 commit comments