Skip to content

Commit 9696471

Browse files
committed
test(runtime): raise checkpoint resume patch coverage above 90
1 parent 24feb15 commit 9696471

2 files changed

Lines changed: 151 additions & 9 deletions

File tree

internal/runtime/checkpoint_flow_test.go

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package runtime
22

33
import (
44
"context"
5+
"errors"
56
"os"
67
"path/filepath"
78
"strings"
@@ -17,6 +18,7 @@ import (
1718

1819
type 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

5759
func (s *checkpointStoreSpy) SetResumeCheckpoint(_ context.Context, rc agentsession.ResumeCheckpoint) error {
5860
s.lastResume = rc
59-
return nil
61+
return s.setResumeErr
6062
}
6163

6264
func (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+
345373
func 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+
578724
func TestApplyResumeCheckpointSkipsWorkspaceMismatch(t *testing.T) {
579725
fixture := newRuntimeCheckpointFixture(t)
580726
state := newRunState("run-resume-workspace-mismatch", fixture.session)

internal/runtime/checkpoint_resume.go

Lines changed: 4 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import (
1515
const (
1616
resumeStrategyReplayPlan = "replay_plan"
1717
resumeStrategyVerifyClosureFirst = "resume_verify_closure"
18-
resumeTranscriptRevisionInvalid = int64(-1)
1918
)
2019

2120
// updateResumeCheckpoint 在 phase 转换时写入或更新 ResumeCheckpoint。
@@ -126,14 +125,11 @@ func sessionTranscriptRevision(session agentsession.Session) int64 {
126125
PlanContextDirty: session.PlanContextDirty,
127126
PlanRestorePendingAlign: session.PlanRestorePendingAlign,
128127
}
129-
raw, err := json.Marshal(snapshot)
130-
if err != nil {
131-
return resumeTranscriptRevisionInvalid
132-
}
128+
// snapshot 字段仅由基础可序列化类型组成,Marshal 对该结构是稳定可达的。
129+
raw, _ := json.Marshal(snapshot)
133130
hasher := fnv.New64a()
134-
if _, err := hasher.Write(raw); err != nil {
135-
return resumeTranscriptRevisionInvalid
136-
}
131+
// fnv.Hash Write 对内存写入不会返回错误,这里忽略 error 以保持实现简洁。
132+
_, _ = hasher.Write(raw)
137133
const positiveInt64Mask = uint64(1<<63 - 1)
138134
return int64(hasher.Sum64() & positiveInt64Mask)
139135
}

0 commit comments

Comments
 (0)