返回 DeepSeek-Reasonix
session_catalog_integrity_lifecycle_test.go
根目录 / desktop / session_catalog_integrity_lifecycle_test.go
1 package main
2
3 import (
4 "context"
5 "database/sql"
6 "errors"
7 "os"
8 "path/filepath"
9 "testing"
10 "time"
11
12 "reasonix/internal/config"
13 "reasonix/internal/sessioncatalog"
14 )
15
16 func TestSessionCatalogDeferredCorruptionReplacesOwnerAndRevokesOldReads(t *testing.T) {
17 isolateDesktopUserDirs(t)
18 path := sessioncatalog.DefaultPath()
19 seed, err := sessioncatalog.Open(t.Context(), sessioncatalog.Options{Path: path, MetadataOnly: true, StartPaused: true, RevisionFloor: 700})
20 if err != nil {
21 t.Fatal(err)
22 }
23 if err := seed.Close(t.Context()); err != nil {
24 t.Fatal(err)
25 }
26 db, err := sql.Open("sqlite", path)
27 if err != nil {
28 t.Fatal(err)
29 }
30 // This defect is invisible to ordinary metadata queries, but the full
31 // integrity audit must still detect it before certifying this generation.
32 _, err = db.Exec(`CREATE TABLE integrity_fixture(value INTEGER CHECK(value>=0)); PRAGMA ignore_check_constraints=ON; INSERT INTO integrity_fixture VALUES(-1)`)
33 closeErr := db.Close()
34 if err != nil || closeErr != nil {
35 t.Fatal(err, closeErr)
36 }
37 if err := os.MkdirAll(config.SessionDir(), 0700); err != nil {
38 t.Fatal(err)
39 }
40 source := filepath.Join(config.SessionDir(), "untouched.jsonl")
41 body := []byte("authoritative source must not be repaired by discovery\n")
42 if err := os.WriteFile(source, body, 0600); err != nil {
43 t.Fatal(err)
44 }
45 app := NewApp()
46 app.tabsRestored = make(chan struct{})
47 app.startSessionCatalog()
48 t.Cleanup(func() {
49 if !app.stopSessionCatalog(5 * time.Second) {
50 t.Error("replacement did not stop cleanly")
51 }
52 })
53 old := waitForSessionCatalogForTest(t, app, nil)
54 lease, err := old.OpenReadLease(t.Context())
55 if err != nil {
56 t.Fatal(err)
57 }
58 defer lease.Close()
59 if _, err := old.ListOrdinarySessions(lease.Context(t.Context()), sessioncatalog.OrdinaryPageRequest{Scope: "global", Limit: 50}); err != nil {
60 t.Fatalf("first metadata page waited for full audit: %v", err)
61 }
62 app.markTabsRestored()
63 select {
64 case <-old.Invalidated():
65 case <-time.After(sessionCatalogTestDeadline):
66 t.Fatal("full background audit did not find corruption")
67 }
68 replacement := waitForSessionCatalogForTest(t, app, old)
69 if status := replacement.Status(); status.Revision <= 700 || status.QuarantinedPath == "" {
70 t.Fatalf("replacement lost revision/quarantine evidence: %+v", status)
71 }
72 if _, err := old.ListOrdinarySessions(lease.Context(context.Background()), sessioncatalog.OrdinaryPageRequest{Scope: "global"}); !errors.Is(err, sessioncatalog.ErrCatalogInvalidated) {
73 t.Fatalf("old cursor was not revoked: %v", err)
74 }
75 reader := &lazyTopicPageReader{catalog: old, lease: lease, positions: map[int]topicPagePosition{0: {}}}
76 _, _, readErr := reader.page(t.Context(), 0, 50)
77 var operationErr *SessionOperationError
78 if !errors.As(readErr, &operationErr) || operationErr.Code != "stale_cursor" {
79 t.Fatalf("replaced catalog cursor lost its typed status: %v", readErr)
80 }
81 got, err := os.ReadFile(source)
82 if err != nil || string(got) != string(body) {
83 t.Fatalf("metadata rebuild changed original content: %v", err)
84 }
85 assertSessionCatalogWatcherRunning(t, app)
86 }
87
87 lines GO