feat(internal/session): add LatestArchivedByPath

Amolith and Crush created

Co-authored-by: Crush <crush@charm.land>

Change summary

internal/session/session.go      |  59 +++++++++++++
internal/session/session_test.go | 151 ++++++++++++++++++++++++++++++++++
2 files changed, 210 insertions(+)

Detailed changes

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
+}

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)
+	}
+}
+