Skip to content

Commit 4c20ebb

Browse files
authored
Merge pull request #65 from Cai-Tang-www/fork-pr-666-1779179913
test(runtime): raise resume verify coverage
2 parents 30cb051 + d680c60 commit 4c20ebb

3 files changed

Lines changed: 232 additions & 0 deletions

File tree

internal/runtime/checkpoint_flow_test.go

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import (
1212
providertypes "neo-code/internal/provider/types"
1313
"neo-code/internal/runtime/controlplane"
1414
agentsession "neo-code/internal/session"
15+
"neo-code/internal/tools"
1516
)
1617

1718
type checkpointStoreSpy struct {
@@ -202,6 +203,15 @@ func countPerEditCheckpointMetaFiles(t *testing.T, root string) int {
202203
return count
203204
}
204205

206+
func hasEventType(events []RuntimeEvent, want EventType) bool {
207+
for _, event := range events {
208+
if event.Type == want {
209+
return true
210+
}
211+
}
212+
return false
213+
}
214+
205215
func TestCreateStartOfTurnCheckpoint_PendingWrite(t *testing.T) {
206216
fixture := newRuntimeCheckpointFixture(t)
207217
fixture.captureFile(t, "main.go", []byte("package main\nconst v = 1\n"))
@@ -389,6 +399,133 @@ func TestApplyResumeCheckpointVerifyClosureStrategy(t *testing.T) {
389399
assertEventSequence(t, events, []EventType{EventResumeApplied, EventRuntimeSnapshotUpdated})
390400
}
391401

402+
func TestApplyResumeCheckpointSkipsUnsupportedInputs(t *testing.T) {
403+
t.Parallel()
404+
405+
fixture := newRuntimeCheckpointFixture(t)
406+
service := &Service{
407+
checkpointStore: &checkpointStoreSpy{},
408+
events: make(chan RuntimeEvent, 16),
409+
runtimeSnapshots: make(map[string]RuntimeSnapshot),
410+
}
411+
412+
service.applyResumeCheckpoint(context.Background(), nil)
413+
414+
emptyState := newRunState("run-empty-session", fixture.session)
415+
emptyState.session.ID = " "
416+
service.applyResumeCheckpoint(context.Background(), &emptyState)
417+
418+
unsupportedState := newRunState("run-unsupported-resume", fixture.session)
419+
service.checkpointStore = &checkpointStoreSpy{
420+
latestResume: &agentsession.ResumeCheckpoint{
421+
RunID: "run-unknown",
422+
SessionID: fixture.session.ID,
423+
Turn: 1,
424+
Phase: "stopped",
425+
CompletionState: "completed",
426+
},
427+
}
428+
service.applyResumeCheckpoint(context.Background(), &unsupportedState)
429+
430+
if unsupportedState.resumeNextBaseLifecycle != "" {
431+
t.Fatalf("resumeNextBaseLifecycle = %q, want empty", unsupportedState.resumeNextBaseLifecycle)
432+
}
433+
if strings.TrimSpace(unsupportedState.pendingSystemReminder) != "" {
434+
t.Fatalf("pendingSystemReminder = %q, want empty", unsupportedState.pendingSystemReminder)
435+
}
436+
if len(collectRuntimeEvents(service.Events())) != 0 {
437+
t.Fatal("expected no runtime events for skipped resume checkpoint cases")
438+
}
439+
}
440+
441+
func TestDeriveResumeBaseLifecycle(t *testing.T) {
442+
t.Parallel()
443+
444+
tests := []struct {
445+
name string
446+
phase string
447+
completionState string
448+
want controlplane.RunState
449+
}{
450+
{name: "verify completed resumes verify", phase: " verify ", completionState: " completed ", want: controlplane.RunStateVerify},
451+
{name: "verify incomplete falls back to plan", phase: "verify", completionState: "running", want: controlplane.RunStatePlan},
452+
{name: "plan resumes plan", phase: "plan", completionState: "", want: controlplane.RunStatePlan},
453+
{name: "execute resumes plan", phase: "execute", completionState: "", want: controlplane.RunStatePlan},
454+
{name: "unknown phase ignored", phase: "stopped", completionState: "completed", want: ""},
455+
}
456+
457+
for _, tt := range tests {
458+
tt := tt
459+
t.Run(tt.name, func(t *testing.T) {
460+
t.Parallel()
461+
462+
if got := deriveResumeBaseLifecycle(tt.phase, tt.completionState); got != tt.want {
463+
t.Fatalf("deriveResumeBaseLifecycle(%q, %q) = %q, want %q", tt.phase, tt.completionState, got, tt.want)
464+
}
465+
})
466+
}
467+
}
468+
469+
func TestServiceRunResumeVerifyClosureBootstrapsFirstTurn(t *testing.T) {
470+
t.Parallel()
471+
472+
manager := newRuntimeConfigManager(t)
473+
store := newMemoryStore()
474+
providerImpl := &scriptedProvider{
475+
responses: []scriptedResponse{
476+
{
477+
Message: providertypes.Message{
478+
Role: providertypes.RoleAssistant,
479+
Parts: []providertypes.ContentPart{
480+
providertypes.NewTextPart("verification summary"),
481+
},
482+
},
483+
FinishReason: "stop",
484+
},
485+
},
486+
}
487+
resumeStore := &checkpointStoreSpy{
488+
latestResume: &agentsession.ResumeCheckpoint{
489+
RunID: "run-old-verify",
490+
SessionID: "ignored-by-spy",
491+
Turn: 2,
492+
Phase: "verify",
493+
CompletionState: "completed",
494+
},
495+
}
496+
service := NewWithFactory(
497+
manager,
498+
tools.NewRegistry(),
499+
store,
500+
&scriptedProviderFactory{provider: providerImpl},
501+
&stubContextBuilder{},
502+
)
503+
service.checkpointStore = resumeStore
504+
service.events = make(chan RuntimeEvent, 32)
505+
service.runtimeSnapshots = make(map[string]RuntimeSnapshot)
506+
507+
if err := service.Run(context.Background(), UserInput{
508+
RunID: "run-resume-verify-first-turn",
509+
Parts: []providertypes.ContentPart{providertypes.NewTextPart("continue")},
510+
}); err != nil {
511+
t.Fatalf("Run() error = %v", err)
512+
}
513+
514+
events := collectRuntimeEvents(service.Events())
515+
if !hasEventType(events, EventResumeApplied) {
516+
t.Fatalf("expected %s event, got %+v", EventResumeApplied, events)
517+
}
518+
if !hasPhaseTransition(events, "", "plan") {
519+
t.Fatalf("missing bootstrap transition '' -> plan, events=%+v", events)
520+
}
521+
if !hasPhaseTransition(events, "plan", "verify") {
522+
t.Fatalf("missing resume transition plan -> verify, events=%+v", events)
523+
}
524+
if !hasEventType(events, EventVerificationStarted) {
525+
t.Fatalf("expected %s event, got %+v", EventVerificationStarted, events)
526+
}
527+
}
528+
392529
func TestRuntimeCheckpointFacadeMethods(t *testing.T) {
393530
t.Run("list checkpoints delegates to store", func(t *testing.T) {
394531
spy := &checkpointStoreSpy{

internal/runtime/run_lifecycle_test.go

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,54 @@ func TestApplyTurnBaseRunStateBootstrapsVerifyFromEmpty(t *testing.T) {
159159
})
160160
}
161161

162+
func TestApplyTurnBaseRunStateNonBootstrapPaths(t *testing.T) {
163+
t.Parallel()
164+
165+
service := &Service{events: make(chan RuntimeEvent, 16)}
166+
if err := service.applyTurnBaseRunState(context.Background(), nil, controlplane.RunStateVerify); err != nil {
167+
t.Fatalf("apply turn base run state nil state: %v", err)
168+
}
169+
170+
state := newRunState("run-bootstrap-plan", newRuntimeSession("session-bootstrap-plan"))
171+
if err := service.applyTurnBaseRunState(context.Background(), &state, controlplane.RunStatePlan); err != nil {
172+
t.Fatalf("apply turn base run state plan: %v", err)
173+
}
174+
if state.lifecycle != controlplane.RunStatePlan {
175+
t.Fatalf("lifecycle = %q, want plan", state.lifecycle)
176+
}
177+
178+
state.lifecycle = controlplane.RunStatePlan
179+
if err := service.applyTurnBaseRunState(context.Background(), &state, controlplane.RunStateVerify); err != nil {
180+
t.Fatalf("apply turn base run state verify from plan: %v", err)
181+
}
182+
if state.lifecycle != controlplane.RunStateVerify {
183+
t.Fatalf("lifecycle = %q, want verify", state.lifecycle)
184+
}
185+
186+
events := collectRuntimeEvents(service.Events())
187+
assertPhaseTransitions(t, events, [][2]string{
188+
{"", "plan"},
189+
{"plan", "verify"},
190+
})
191+
}
192+
193+
func TestRunLifecycleRejectsInvalidStates(t *testing.T) {
194+
t.Parallel()
195+
196+
service := &Service{events: make(chan RuntimeEvent, 16)}
197+
state := newRunState("run-invalid-lifecycle", newRuntimeSession("session-invalid-lifecycle"))
198+
199+
if err := service.setBaseRunState(context.Background(), &state, controlplane.RunStateWaitingPermission); err == nil {
200+
t.Fatal("expected invalid base lifecycle state error")
201+
}
202+
if err := service.enterTemporaryRunState(context.Background(), &state, controlplane.RunStatePlan); err == nil {
203+
t.Fatal("expected unsupported temporary lifecycle state error")
204+
}
205+
if err := service.leaveTemporaryRunState(context.Background(), &state, controlplane.RunStatePlan); err == nil {
206+
t.Fatal("expected unsupported temporary lifecycle state error")
207+
}
208+
}
209+
162210
func assertPhaseTransitions(t *testing.T, events []RuntimeEvent, expected [][2]string) {
163211
t.Helper()
164212

internal/runtime/verification_events_test.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,3 +71,50 @@ func TestEmitVerificationLifecycleEvents(t *testing.T) {
7171
t.Fatalf("ErrorClass = %q, want unknown", finished.ErrorClass)
7272
}
7373
}
74+
75+
func TestEmitVerificationLifecycleEventsAcceptedAndNilGuards(t *testing.T) {
76+
t.Parallel()
77+
78+
service := &Service{events: make(chan RuntimeEvent, 16)}
79+
state := newRunState("run-verification-accepted", agentsession.New("verification-accepted"))
80+
service.emitVerificationLifecycleEvents(context.Background(), &state, controlplane.CompletionState{}, acceptgate.Report{
81+
Outcome: acceptgate.OutcomeAccepted,
82+
Results: []acceptgate.CheckResult{
83+
{Name: " permission-check ", Passed: false, Reason: "permission denied"},
84+
{Name: " timeout-check ", Passed: false, Reason: "command timeout"},
85+
{Name: " lookup-check ", Passed: false, Reason: "binary not found"},
86+
{Name: " pass-check ", Passed: true, Reason: " skipped "},
87+
},
88+
})
89+
service.emitVerificationLifecycleEvents(context.Background(), nil, controlplane.CompletionState{}, acceptgate.Report{})
90+
var nilService *Service
91+
nilService.emitVerificationLifecycleEvents(context.Background(), &state, controlplane.CompletionState{}, acceptgate.Report{})
92+
93+
events := collectRuntimeEvents(service.Events())
94+
if len(events) != 6 {
95+
t.Fatalf("event count = %d, want 6", len(events))
96+
}
97+
98+
finished, ok := events[len(events)-1].Payload.(VerificationFinishedPayload)
99+
if !ok {
100+
t.Fatalf("finished payload type = %T", events[len(events)-1].Payload)
101+
}
102+
if finished.ErrorClass != "" {
103+
t.Fatalf("ErrorClass = %q, want empty for accepted verification", finished.ErrorClass)
104+
}
105+
106+
var gotClasses []string
107+
for _, event := range events[1 : len(events)-1] {
108+
payload, ok := event.Payload.(VerificationStageFinishedPayload)
109+
if !ok {
110+
t.Fatalf("stage payload type = %T", event.Payload)
111+
}
112+
gotClasses = append(gotClasses, payload.ErrorClass)
113+
}
114+
wantClasses := []string{"permission_denied", "timeout", "command_not_found", ""}
115+
for i := range wantClasses {
116+
if gotClasses[i] != wantClasses[i] {
117+
t.Fatalf("stage error class[%d] = %q, want %q", i, gotClasses[i], wantClasses[i])
118+
}
119+
}
120+
}

0 commit comments

Comments
 (0)