From 408543b1194ba2af9f3587319ec93ebfa4497870 Mon Sep 17 00:00:00 2001 From: Amolith Date: Wed, 29 Oct 2025 18:07:41 -0600 Subject: [PATCH] feat(internal/session): add LatestArchivedByPath Co-authored-by: Crush --- internal/session/session.go | 59 ++++++++++++ internal/session/session_test.go | 151 +++++++++++++++++++++++++++++++ 2 files changed, 210 insertions(+) diff --git a/internal/session/session.go b/internal/session/session.go index 6612dc0691ea5bc806585f11b368e7cfd1a0c11f..43ae7c9a86673a10ae86e4db06591ea2fe2a0664 100644 --- a/internal/session/session.go +++ b/internal/session/session.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "strings" "time" "git.secluded.site/np/internal/db" @@ -344,3 +345,61 @@ func (s *Store) Archive(ctx context.Context, sid string) (Document, error) { return doc, err } + +// LatestArchivedByPath returns the most recently archived session for path +// or any of its parents. The returned boolean reports whether a session was found. +func (s *Store) LatestArchivedByPath(ctx context.Context, path string) (Document, bool, error) { + canonical, err := db.CanonicalizeDir(path) + if err != nil { + return Document{}, false, fmt.Errorf("session: canonicalise path: %w", err) + } + + var doc Document + found := false + + err = s.db.View(ctx, func(txn *db.Txn) error { + for _, candidate := range db.ParentWalk(canonical) { + hash := db.DirHash(candidate) + prefix := db.PrefixDirArchived(hash) + + // Iterate in reverse to get most recent first + iterErr := txn.Iterate(db.IterateOptions{ + Prefix: prefix, + Reverse: true, + }, func(item db.Item) error { + // Extract SID from key: dir/{hash}/archived/{ts}/{sid} + key := item.KeyString() + i := strings.LastIndex(key, "/") + if i < 0 || i+1 >= len(key) { + return nil + } + sid := key[i+1:] + + // Try to load the document; if not found, continue to next older + loaded, err := loadDocument(txn, sid) + if err != nil { + if errors.Is(err, ErrNotFound) { + return nil // Try next older entry + } + return err + } + + doc = loaded + found = true + return db.ErrTxnAborted + }) + if iterErr != nil { + return iterErr + } + + if found { + return nil + } + } + return nil + }) + if err != nil { + return Document{}, false, err + } + return doc, found, nil +} diff --git a/internal/session/session_test.go b/internal/session/session_test.go index 3700d0612f3fd2222f9459e97db5960492b633f5..bdb28ff99b071644d6ccd230de16656a94ac8392 100644 --- a/internal/session/session_test.go +++ b/internal/session/session_test.go @@ -246,3 +246,154 @@ func TestTxnStoreTouchAt(t *testing.T) { t.Fatalf("Expected LastUpdatedAt %v, got %v", want, autoUpdated.LastUpdatedAt) } } + +func TestLatestArchivedByPath(t *testing.T) { + ctx := context.Background() + database := testutil.OpenDB(t) + + clock := &testutil.SequenceClock{ + Times: []time.Time{ + time.Date(2025, time.April, 1, 9, 0, 0, 0, time.UTC), + time.Date(2025, time.April, 1, 10, 0, 0, 0, time.UTC), + time.Date(2025, time.April, 2, 9, 0, 0, 0, time.UTC), + time.Date(2025, time.April, 2, 10, 0, 0, 0, time.UTC), + time.Date(2025, time.April, 3, 9, 0, 0, 0, time.UTC), + time.Date(2025, time.April, 3, 10, 0, 0, 0, time.UTC), + }, + } + + store := session.NewStore(database, clock) + workdir := t.TempDir() + + // Create and archive first session + session1, err := store.Start(ctx, workdir) + if err != nil { + t.Fatalf("Start session1: %v", err) + } + _, err = store.Archive(ctx, session1.SID) + if err != nil { + t.Fatalf("Archive session1: %v", err) + } + + // Create and archive second session (more recent) + session2, err := store.Start(ctx, workdir) + if err != nil { + t.Fatalf("Start session2: %v", err) + } + _, err = store.Archive(ctx, session2.SID) + if err != nil { + t.Fatalf("Archive session2: %v", err) + } + + // Create and archive third session (most recent) + session3, err := store.Start(ctx, workdir) + if err != nil { + t.Fatalf("Start session3: %v", err) + } + archived3, err := store.Archive(ctx, session3.SID) + if err != nil { + t.Fatalf("Archive session3: %v", err) + } + + // LatestArchivedByPath should return the most recent archived session + latest, found, err := store.LatestArchivedByPath(ctx, workdir) + if err != nil { + t.Fatalf("LatestArchivedByPath: %v", err) + } + if !found { + t.Fatalf("expected to find archived session") + } + if latest.SID != archived3.SID { + t.Fatalf("expected latest SID %s, got %s", archived3.SID, latest.SID) + } + if latest.State != session.StateArchived { + t.Fatalf("expected archived state, got %s", latest.State) + } + + // Test with child directory (parent walk) + childDir := filepath.Join(workdir, "nested", "deep") + if err := os.MkdirAll(childDir, 0o755); err != nil { + t.Fatalf("mkdir nested: %v", err) + } + + latestFromChild, found, err := store.LatestArchivedByPath(ctx, childDir) + if err != nil { + t.Fatalf("LatestArchivedByPath from child: %v", err) + } + if !found { + t.Fatalf("expected to find archived session from child dir") + } + if latestFromChild.SID != archived3.SID { + t.Fatalf("expected latest SID %s from child, got %s", archived3.SID, latestFromChild.SID) + } + + // Test with directory that has no archived sessions + emptyDir := t.TempDir() + _, found, err = store.LatestArchivedByPath(ctx, emptyDir) + if err != nil { + t.Fatalf("LatestArchivedByPath on empty dir: %v", err) + } + if found { + t.Fatalf("expected not to find archived session in empty dir") + } +} + +func TestLatestArchivedByPathWithMissingDoc(t *testing.T) { + ctx := context.Background() + database := testutil.OpenDB(t) + + clock := &testutil.SequenceClock{ + Times: []time.Time{ + time.Date(2025, time.April, 1, 9, 0, 0, 0, time.UTC), + time.Date(2025, time.April, 1, 10, 0, 0, 0, time.UTC), + time.Date(2025, time.April, 2, 9, 0, 0, 0, time.UTC), + time.Date(2025, time.April, 2, 10, 0, 0, 0, time.UTC), + }, + } + + store := session.NewStore(database, clock) + workdir := t.TempDir() + + // Create and archive first session + session1, err := store.Start(ctx, workdir) + if err != nil { + t.Fatalf("Start session1: %v", err) + } + archived1, err := store.Archive(ctx, session1.SID) + if err != nil { + t.Fatalf("Archive session1: %v", err) + } + + // Create and archive second session (most recent) + session2, err := store.Start(ctx, workdir) + if err != nil { + t.Fatalf("Start session2: %v", err) + } + archived2, err := store.Archive(ctx, session2.SID) + if err != nil { + t.Fatalf("Archive session2: %v", err) + } + + // Corrupt the most recent session by deleting its meta key + // This simulates a missing/corrupted document + err = database.Update(ctx, func(txn *db.Txn) error { + metaKey := db.KeySessionMeta(archived2.SID) + return txn.Delete(metaKey) + }) + if err != nil { + t.Fatalf("Delete meta: %v", err) + } + + // LatestArchivedByPath should skip the corrupted session and find the older valid one + latest, found, err := store.LatestArchivedByPath(ctx, workdir) + if err != nil { + t.Fatalf("LatestArchivedByPath: %v", err) + } + if !found { + t.Fatalf("expected to find archived session (should skip corrupted and find older)") + } + if latest.SID != archived1.SID { + t.Fatalf("expected to find session1 %s, got %s", archived1.SID, latest.SID) + } +} +