| 1 | package agent |
| 2 | |
| 3 | import ( |
| 4 | "strings" |
| 5 | "testing" |
| 6 | |
| 7 | "reasonix/internal/provider" |
| 8 | ) |
| 9 | |
| 10 | func TestSubagentStorePartialOutcomeCanBeReadAndResumed(t *testing.T) { |
| 11 | store := NewSubagentStore(t.TempDir()) |
| 12 | spec := testSubagentSpec(t, "explore") |
| 13 | run, err := store.PrepareFresh(spec) |
| 14 | if err != nil { |
| 15 | t.Fatalf("PrepareFresh: %v", err) |
| 16 | } |
| 17 | run.Session.Add(provider.Message{Role: provider.RoleUser, Content: "inspect"}) |
| 18 | run.Session.Add(provider.Message{Role: provider.RoleAssistant, Content: "partial finding"}) |
| 19 | if err := store.SaveOutcome(run, SubagentOutcome{Ref: run.Ref, Status: SubagentOutcomePartial, FinalAnswer: "partial finding", ErrorCode: "completion_uncertain", Retryable: true}); err != nil { |
| 20 | t.Fatalf("SaveOutcome: %v", err) |
| 21 | } |
| 22 | run.Release() |
| 23 | answer, status, err := store.ReadFinalAnswer(run.Ref, spec.ParentSession, spec.WorkspaceRoot) |
| 24 | if err != nil || answer != "partial finding" || status != SubagentStatus(SubagentOutcomePartial) { |
| 25 | t.Fatalf("ReadFinalAnswer = %q/%q/%v, want partial result", answer, status, err) |
| 26 | } |
| 27 | meta, err := store.LoadMeta(run.Ref) |
| 28 | if err != nil || meta.Status != SubagentFailed || meta.Outcome != string(SubagentOutcomePartial) || !meta.Retryable { |
| 29 | t.Fatalf("partial metadata = %+v/%v", meta, err) |
| 30 | } |
| 31 | continued, err := store.PrepareContinue(run.Ref, spec) |
| 32 | if err != nil { |
| 33 | t.Fatalf("PrepareContinue partial: %v", err) |
| 34 | } |
| 35 | meta, err = store.LoadMeta(run.Ref) |
| 36 | if err != nil || meta.Status != SubagentRunning || meta.Outcome != "" { |
| 37 | t.Fatalf("resumed metadata = %+v/%v, want running without stale outcome", meta, err) |
| 38 | } |
| 39 | continued.Release() |
| 40 | } |
| 41 | |
| 42 | func TestSubagentStoreDuplicateTerminalOutcomeIsIdempotent(t *testing.T) { |
| 43 | store := NewSubagentStore(t.TempDir()) |
| 44 | spec := testSubagentSpec(t, "explore") |
| 45 | run, err := store.PrepareFresh(spec) |
| 46 | if err != nil { |
| 47 | t.Fatalf("PrepareFresh: %v", err) |
| 48 | } |
| 49 | defer run.Release() |
| 50 | answer := "same terminal answer" |
| 51 | run.Session.Add(provider.Message{Role: provider.RoleAssistant, Content: answer}) |
| 52 | outcome := SubagentOutcome{Ref: run.Ref, Status: SubagentOutcomePartial, ErrorCode: "completion_uncertain", Retryable: true} |
| 53 | if err := store.SaveOutcome(run, outcome); err != nil { |
| 54 | t.Fatalf("first SaveOutcome: %v", err) |
| 55 | } |
| 56 | if err := store.SaveOutcome(run, outcome); err != nil { |
| 57 | t.Fatalf("duplicate SaveOutcome: %v", err) |
| 58 | } |
| 59 | if err := store.SaveOutcome(run, SubagentOutcome{Ref: run.Ref, Status: SubagentOutcomeFailed, ErrorCode: "provider_error"}); err == nil { |
| 60 | t.Fatal("different terminal outcome overwrote a persisted partial outcome") |
| 61 | } |
| 62 | meta, err := store.LoadMeta(run.Ref) |
| 63 | if err != nil || meta.Outcome != string(SubagentOutcomePartial) || meta.Status != SubagentFailed || !meta.Retryable { |
| 64 | t.Fatalf("duplicate terminal metadata = %+v/%v", meta, err) |
| 65 | } |
| 66 | } |
| 67 | |
| 68 | func TestSubagentStoreFailedOutcomeRetainsReadableLastAnswer(t *testing.T) { |
| 69 | store := NewSubagentStore(t.TempDir()) |
| 70 | spec := testSubagentSpec(t, "explore") |
| 71 | run, err := store.PrepareFresh(spec) |
| 72 | if err != nil { |
| 73 | t.Fatalf("PrepareFresh: %v", err) |
| 74 | } |
| 75 | run.Session.Add(provider.Message{Role: provider.RoleAssistant, Content: "last useful answer"}) |
| 76 | if err := store.SaveOutcome(run, SubagentOutcome{Ref: run.Ref, Status: SubagentOutcomeFailed, ErrorCode: "provider_error"}); err != nil { |
| 77 | t.Fatalf("SaveOutcome: %v", err) |
| 78 | } |
| 79 | run.Release() |
| 80 | answer, status, err := store.ReadFinalAnswer(run.Ref, spec.ParentSession, spec.WorkspaceRoot) |
| 81 | if err != nil || answer != "last useful answer" || status != SubagentStatus(SubagentOutcomeFailed) { |
| 82 | t.Fatalf("ReadFinalAnswer = %q/%q/%v, want retained failed answer", answer, status, err) |
| 83 | } |
| 84 | if _, err := store.PrepareContinue(run.Ref, spec); err == nil || !strings.Contains(err.Error(), "failed and cannot be continued") { |
| 85 | t.Fatalf("non-retryable failed continuation error = %v", err) |
| 86 | } |
| 87 | } |
| 88 |