返回 DeepSeek-Reasonix
repair_transaction_test.go
根目录 / internal / sessioncatalog / repair_transaction_test.go
1 package sessioncatalog
2
3 import (
4 "context"
5 "errors"
6 "path/filepath"
7 "testing"
8 "time"
9
10 "reasonix/internal/agent"
11 )
12
13 func newRepairPublicationTestCatalog(t *testing.T, now *time.Time) (*Catalog, string) {
14 t.Helper()
15 dir := t.TempDir()
16 path := filepath.Join(dir, "legacy.jsonl")
17 catalog, err := Open(context.Background(), Options{
18 Path: filepath.Join(t.TempDir(), "catalog.sqlite"), DisableRepair: true,
19 Now: func() time.Time { return *now },
20 })
21 if err != nil {
22 t.Fatal(err)
23 }
24 t.Cleanup(func() { _ = catalog.Close(context.Background()) })
25 if _, err := catalog.upsertSessionsWithNotification(context.Background(), []SessionRecord{{
26 Path: path, Directory: dir, Scope: "global", TurnsState: TurnsUnknown, Health: HealthOK,
27 }}, nil, "seed", false, upsertDirectoryProjection); err != nil {
28 t.Fatal(err)
29 }
30 catalog.testRepairSessionHook = func(context.Context, string) (agent.SessionListingRepairResult, error) {
31 return agent.SessionListingRepairResult{Status: agent.SessionListingRepairApplied, Preview: "ok", Turns: 1}, nil
32 }
33 return catalog, path
34 }
35
36 func TestRepairBatchPublicationFailuresDoNotPublishPartialState(t *testing.T) {
37 for _, stage := range []string{"begin", "update", "revision", "commit"} {
38 t.Run(stage, func(t *testing.T) {
39 now := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
40 catalog, path := newRepairPublicationTestCatalog(t, &now)
41 beforeRevision := catalog.revision.Load()
42 catalog.testRepairBatchError = func(got string) error {
43 if got == stage {
44 return errors.New("injected " + stage)
45 }
46 return nil
47 }
48 catalog.runRepairWave(context.Background())
49 var state, turnsState string
50 var retryAt int64
51 if err := catalog.db.QueryRow(`SELECT repair_state,repair_retry_at,turns_state FROM catalog_sessions WHERE path_key=?`,
52 catalog.pathKey(path)).Scan(&state, &retryAt, &turnsState); err != nil {
53 t.Fatal(err)
54 }
55 if state != "active" || turnsState != string(TurnsUnknown) || retryAt != now.Add(30*time.Second).UnixMilli() {
56 t.Fatalf("row after %s failure = %s/%s/%d", stage, state, turnsState, retryAt)
57 }
58 if catalog.revision.Load() != beforeRevision {
59 t.Fatalf("revision published after %s failure", stage)
60 }
61 })
62 }
63 }
64
65 func TestExpiredActiveRepairReclaimsWithExponentialLeaseInSameProcess(t *testing.T) {
66 now := time.Date(2026, 9, 1, 0, 0, 0, 0, time.UTC)
67 catalog, path := newRepairPublicationTestCatalog(t, &now)
68 failures := 2
69 catalog.testRepairBatchError = func(stage string) error {
70 if stage == "commit" && failures > 0 {
71 failures--
72 return errors.New("injected commit")
73 }
74 return nil
75 }
76 catalog.runRepairWave(context.Background())
77 now = now.Add(30 * time.Second)
78 catalog.runRepairWave(context.Background())
79 now = now.Add(59 * time.Second)
80 catalog.runRepairWave(context.Background())
81 var state string
82 if err := catalog.db.QueryRow(`SELECT repair_state FROM catalog_sessions WHERE path_key=?`, catalog.pathKey(path)).Scan(&state); err != nil {
83 t.Fatal(err)
84 }
85 if state != "active" {
86 t.Fatalf("repair ran before 60-second lease: %s", state)
87 }
88 now = now.Add(time.Second)
89 catalog.runRepairWave(context.Background())
90 if err := catalog.db.QueryRow(`SELECT repair_state FROM catalog_sessions WHERE path_key=?`, catalog.pathKey(path)).Scan(&state); err != nil {
91 t.Fatal(err)
92 }
93 if state != "complete" {
94 t.Fatalf("same-process reclaimed repair = %s, want complete", state)
95 }
96 }
97
97 lines GO