f99010fae1
CI / lint (push) Failing after 1s
CI / frontend (push) Failing after 1s
CI / scripts (push) Failing after 1s
CI / Go Test (ubuntu-latest) (push) Failing after 0s
CI / frontend-node-25 (push) Failing after 1s
CI / docs (push) Failing after 0s
CI / coverage (push) Failing after 0s
CI / e2e (push) Failing after 0s
Docker / build-and-push (push) Failing after 1s
CI / integration (push) Failing after 4m43s
CI / Go Test (windows-latest) (push) Has been cancelled
CI / Desktop Unit Tests (Windows) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Linux) (push) Has been cancelled
Desktop Artifacts / Desktop Build (Windows) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Has been cancelled
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Has been cancelled
787 lines
27 KiB
Go
787 lines
27 KiB
Go
package parser
|
|
|
|
import (
|
|
"context"
|
|
"database/sql"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func TestZedProviderCapabilities(t *testing.T) {
|
|
factory, ok := ProviderFactoryByType(AgentZed)
|
|
require.True(t, ok)
|
|
require.NotNil(t, factory)
|
|
caps := factory.Capabilities()
|
|
assert.Equal(t, CapabilityUnsupported, caps.Content.Relationships)
|
|
assert.Equal(t, CapabilitySupported, caps.Content.AggregateUsageEvents)
|
|
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{
|
|
Roots: []string{t.TempDir()},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
require.NotNil(t, provider)
|
|
}
|
|
|
|
func TestZedProviderSourceMethods(t *testing.T) {
|
|
|
|
fixture := zedProviderReadFixture(t)
|
|
root := fixture.Root
|
|
dbPath := fixture.DBPath
|
|
threadID := fixture.FirstThreadID
|
|
virtualPath := ZedSQLiteVirtualPath(dbPath, threadID)
|
|
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
|
|
plan, err := provider.WatchPlan(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, plan.Roots, 1)
|
|
assert.Equal(t, filepath.Join(root, "threads"), plan.Roots[0].Path)
|
|
assert.False(t, plan.Roots[0].Recursive)
|
|
assert.Equal(t, []string{"threads.db", "threads.db-*"}, plan.Roots[0].IncludeGlobs)
|
|
|
|
discovered, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, discovered, 1)
|
|
assert.Equal(t, AgentZed, discovered[0].Provider)
|
|
assert.Equal(t, dbPath, discovered[0].DisplayPath)
|
|
assert.Equal(t, dbPath, discovered[0].FingerprintKey)
|
|
|
|
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
FullSessionID: "host~zed:" + threadID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, found.DisplayPath)
|
|
assert.Equal(t, virtualPath, found.FingerprintKey)
|
|
|
|
fingerprint, err := provider.Fingerprint(context.Background(), found)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, virtualPath, fingerprint.Key)
|
|
assert.Positive(t, fingerprint.Size)
|
|
assert.NotZero(t, fingerprint.MTimeNS)
|
|
assert.NotEmpty(t, fingerprint.Hash)
|
|
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{Path: dbPath + "-wal", EventKind: "write", WatchRoot: filepath.Dir(dbPath)},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
assert.Equal(t, dbPath, changed[0].DisplayPath)
|
|
}
|
|
|
|
func TestZedProviderParsePhysicalAndVirtualSources(t *testing.T) {
|
|
|
|
fixture := zedProviderReadFixture(t)
|
|
root := fixture.Root
|
|
threadOne := fixture.FirstThreadID
|
|
threadTwo := fixture.SecondThreadID
|
|
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
sources, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, sources, 1)
|
|
|
|
allOutcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: sources[0],
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, allOutcome.ResultSetComplete)
|
|
require.True(t, allOutcome.ForceReplace)
|
|
require.Len(t, allOutcome.Results, 2)
|
|
assert.Equal(t, "zed:"+threadOne, allOutcome.Results[0].Result.Session.ID)
|
|
assert.Equal(t, "zed:"+threadTwo, allOutcome.Results[1].Result.Session.ID)
|
|
|
|
virtualSource, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
RawSessionID: threadTwo,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
oneOutcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: virtualSource,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, oneOutcome.ResultSetComplete)
|
|
require.True(t, oneOutcome.ForceReplace)
|
|
require.Len(t, oneOutcome.Results, 1)
|
|
assert.Equal(t, "zed:"+threadTwo, oneOutcome.Results[0].Result.Session.ID)
|
|
assert.Equal(t, "devbox", oneOutcome.Results[0].Result.Session.Machine)
|
|
assert.Len(t, oneOutcome.Results[0].Result.Messages, 1)
|
|
}
|
|
|
|
func TestZedProviderFingerprintIncludesWALSiblings(t *testing.T) {
|
|
|
|
root := t.TempDir()
|
|
dbPath := filepath.Join(root, zedThreadsDBRelPath)
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0o755))
|
|
createZedThreadsDBAt(t, dbPath, []zedTestThread{{
|
|
id: "10431c84-c47b-4e6c-b2df-f9f3b9ad025b",
|
|
summary: "Provider thread",
|
|
updatedAt: "2026-06-08T09:14:10Z",
|
|
dataType: "json",
|
|
data: []byte(`{"messages":[{"User":{"content":[{"Text":"Hello Zed"}]}}]}`),
|
|
}})
|
|
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
sources, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, sources, 1)
|
|
before, err := provider.Fingerprint(context.Background(), sources[0])
|
|
require.NoError(t, err)
|
|
|
|
walPath := dbPath + "-wal"
|
|
writeSourceFile(t, walPath, "wal")
|
|
walTime := time.Unix(0, before.MTimeNS+int64(time.Second))
|
|
require.NoError(t, os.Chtimes(walPath, walTime, walTime))
|
|
after, err := provider.Fingerprint(context.Background(), sources[0])
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, before.Size, after.Size)
|
|
assert.Greater(t, after.MTimeNS, before.MTimeNS)
|
|
}
|
|
|
|
func TestZedProviderClassifiesDeletedPhysicalDB(t *testing.T) {
|
|
root := t.TempDir()
|
|
dbPath := filepath.Join(root, zedThreadsDBRelPath)
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0o755))
|
|
createZedThreadsDBAt(t, dbPath, []zedTestThread{{
|
|
id: "10431c84-c47b-4e6c-b2df-f9f3b9ad025b",
|
|
summary: "Provider thread",
|
|
updatedAt: "2026-06-08T09:14:10Z",
|
|
dataType: "json",
|
|
data: []byte(`{"messages":[{"User":{"content":[{"Text":"Hello Zed"}]}}]}`),
|
|
}})
|
|
require.NoError(t, os.Remove(dbPath))
|
|
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{Path: dbPath, EventKind: "remove", WatchRoot: filepath.Dir(dbPath)},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
assert.Equal(t, dbPath, changed[0].DisplayPath)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: changed[0]})
|
|
require.NoError(t, err)
|
|
assert.True(t, outcome.ResultSetComplete)
|
|
// The backing DB file is gone, so the outcome must not force-replace: the
|
|
// persistent archive preserves sessions whose source file no longer exists.
|
|
assert.False(t, outcome.ForceReplace)
|
|
assert.Equal(t, SkipNoSession, outcome.SkipReason)
|
|
assert.Empty(t, outcome.Results)
|
|
}
|
|
|
|
func TestZedProviderStoredVirtualPathFreshness(t *testing.T) {
|
|
root := t.TempDir()
|
|
dbPath := filepath.Join(root, zedThreadsDBRelPath)
|
|
threadID := "10431c84-c47b-4e6c-b2df-f9f3b9ad025b"
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0o755))
|
|
createZedThreadsDBAt(t, dbPath, []zedTestThread{{
|
|
id: threadID,
|
|
summary: "Provider thread",
|
|
updatedAt: "2026-06-08T09:14:10Z",
|
|
dataType: "json",
|
|
data: []byte(`{"messages":[{"User":{"content":[{"Text":"Hello Zed"}]}}]}`),
|
|
}})
|
|
virtualPath := ZedSQLiteVirtualPath(dbPath, threadID)
|
|
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: virtualPath,
|
|
RequireFreshSource: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, found.DisplayPath)
|
|
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
require.NoError(t, err)
|
|
_, err = db.Exec(`DELETE FROM threads WHERE id = ?`, threadID)
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.Close())
|
|
|
|
_, ok, err = provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: virtualPath,
|
|
RequireFreshSource: true,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, ok, "fresh lookup must reject a deleted virtual row")
|
|
|
|
staleSource, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: virtualPath,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok, "non-fresh lookup keeps tombstone source identity")
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: staleSource,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, outcome.ResultSetComplete)
|
|
assert.True(t, outcome.ForceReplace)
|
|
assert.Equal(t, SkipNoSession, outcome.SkipReason)
|
|
assert.Empty(t, outcome.Results)
|
|
}
|
|
|
|
// TestZedProviderChangedPathTombstonesDeletedThread verifies the changed-path
|
|
// classifier emits a tombstone for a stored Zed thread deleted from a
|
|
// still-present database, so the engine force-replaces it out of the archive.
|
|
// The surviving thread is left to the whole-DB fan-out; a vanished database
|
|
// emits no tombstone (stored sessions preserved per the persistent-archive
|
|
// rule).
|
|
func TestZedProviderChangedPathTombstonesDeletedThread(t *testing.T) {
|
|
root := t.TempDir()
|
|
dbPath := filepath.Join(root, zedThreadsDBRelPath)
|
|
threadsDir := filepath.Join(root, "threads")
|
|
survivingID := "10431c84-c47b-4e6c-b2df-f9f3b9ad025b"
|
|
deletedID := "20431c84-c47b-4e6c-b2df-f9f3b9ad025b"
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0o755))
|
|
createZedThreadsDBAt(t, dbPath, []zedTestThread{
|
|
{
|
|
id: survivingID, summary: "Surviving thread",
|
|
updatedAt: "2026-06-08T09:14:10Z", dataType: "json",
|
|
data: []byte(`{"messages":[{"User":{"content":[{"Text":"Hello"}]}}]}`),
|
|
},
|
|
{
|
|
id: deletedID, summary: "Doomed thread",
|
|
updatedAt: "2026-06-08T09:15:10Z", dataType: "json",
|
|
data: []byte(`{"messages":[{"User":{"content":[{"Text":"Bye"}]}}]}`),
|
|
},
|
|
})
|
|
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
|
|
survivingPath := ZedSQLiteVirtualPath(dbPath, survivingID)
|
|
deletedPath := ZedSQLiteVirtualPath(dbPath, deletedID)
|
|
|
|
db, err := sql.Open("sqlite3", dbPath)
|
|
require.NoError(t, err)
|
|
_, err = db.Exec(`DELETE FROM threads WHERE id = ?`, deletedID)
|
|
require.NoError(t, err)
|
|
require.NoError(t, db.Close())
|
|
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{
|
|
Path: dbPath,
|
|
EventKind: "write",
|
|
WatchRoot: threadsDir,
|
|
StoredSourcePaths: []string{survivingPath, deletedPath},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
gotPaths := make([]string, len(changed))
|
|
for i, src := range changed {
|
|
gotPaths[i] = src.DisplayPath
|
|
}
|
|
assert.ElementsMatch(t, []string{dbPath, deletedPath}, gotPaths,
|
|
"whole-DB source plus a tombstone for the deleted thread only")
|
|
|
|
var tombstone SourceRef
|
|
for _, src := range changed {
|
|
if src.DisplayPath == deletedPath {
|
|
tombstone = src
|
|
}
|
|
}
|
|
require.NotEmpty(t, tombstone.DisplayPath, "deleted-thread tombstone source")
|
|
|
|
// The fingerprint of a deleted-but-present-DB thread must be keyed-empty:
|
|
// it must not error (or the engine aborts before Parse) and must not carry
|
|
// the physical DB size/mtime/hash. A DB-level fingerprint here lets the
|
|
// engine's pre-parse freshness check skip Parse whenever stored metadata
|
|
// happens to match, stranding the deleted thread. This mirrors Shelley and
|
|
// Kiro tombstone fingerprinting.
|
|
fingerprint, err := provider.Fingerprint(context.Background(), tombstone)
|
|
require.NoError(t, err, "missing-thread fingerprint must not error")
|
|
assert.Equal(t, tombstone.FingerprintKey, fingerprint.Key)
|
|
assert.Zero(t, fingerprint.Size,
|
|
"deleted-thread fingerprint must not carry the DB size")
|
|
assert.Zero(t, fingerprint.MTimeNS,
|
|
"deleted-thread fingerprint must not carry the DB mtime")
|
|
assert.Empty(t, fingerprint.Hash,
|
|
"deleted-thread fingerprint must not carry the DB hash")
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: tombstone,
|
|
Fingerprint: fingerprint,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, outcome.ResultSetComplete)
|
|
assert.True(t, outcome.ForceReplace,
|
|
"a thread deleted from a present DB is force-replaced out of the archive")
|
|
assert.Equal(t, SkipNoSession, outcome.SkipReason)
|
|
assert.Empty(t, outcome.Results)
|
|
|
|
require.NoError(t, os.Remove(dbPath))
|
|
gone, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{
|
|
Path: dbPath,
|
|
EventKind: "remove",
|
|
WatchRoot: threadsDir,
|
|
StoredSourcePaths: []string{survivingPath, deletedPath},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
for _, src := range gone {
|
|
assert.NotEqual(t, deletedPath, src.DisplayPath,
|
|
"a vanished database must not tombstone stored sessions")
|
|
assert.NotEqual(t, survivingPath, src.DisplayPath)
|
|
}
|
|
}
|
|
|
|
func TestZedProviderRejectsInvalidStoredVirtualPaths(t *testing.T) {
|
|
root := t.TempDir()
|
|
dbPath := filepath.Join(root, zedThreadsDBRelPath)
|
|
threadID := "10431c84-c47b-4e6c-b2df-f9f3b9ad025b"
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(dbPath), 0o755))
|
|
createZedThreadsDBAt(t, dbPath, []zedTestThread{{
|
|
id: threadID,
|
|
summary: "Provider thread",
|
|
updatedAt: "2026-06-08T09:14:10Z",
|
|
dataType: "json",
|
|
data: []byte(`{"messages":[{"User":{"content":[{"Text":"Hello Zed"}]}}]}`),
|
|
}})
|
|
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
for _, path := range []string{
|
|
dbPath + "#",
|
|
filepath.Join(root, "threads", "threads-copy.db") + "#" + threadID,
|
|
filepath.Join(root, "debug", "threads.db") + "#" + threadID,
|
|
} {
|
|
_, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: path,
|
|
RequireFreshSource: true,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, ok, "stored path %q", path)
|
|
}
|
|
}
|
|
|
|
func TestZedProviderIgnoresUnrelatedSidecarBasename(t *testing.T) {
|
|
root := t.TempDir()
|
|
provider, ok := NewProvider(AgentZed, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{
|
|
Path: filepath.Join(root, "other", "threads.db-wal"),
|
|
EventKind: "remove",
|
|
WatchRoot: filepath.Join(root, "other"),
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, changed)
|
|
}
|
|
|
|
func TestShelleyProviderCapabilities(t *testing.T) {
|
|
factory, ok := ProviderFactoryByType(AgentShelley)
|
|
require.True(t, ok)
|
|
require.NotNil(t, factory)
|
|
caps := factory.Capabilities()
|
|
assert.Equal(t, CapabilitySupported, caps.Content.Relationships)
|
|
assert.Equal(t, CapabilityUnsupported, caps.Content.AggregateUsageEvents)
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{
|
|
Roots: []string{t.TempDir()},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
require.NotNil(t, provider)
|
|
}
|
|
|
|
func TestShelleyProviderSourceMethods(t *testing.T) {
|
|
|
|
fixture := shelleyProviderReadFixture(t)
|
|
root := fixture.Root
|
|
dbPath := fixture.DBPath
|
|
virtualPath := ShelleyVirtualPath(dbPath, "cMAIN1")
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
|
|
plan, err := provider.WatchPlan(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, plan.Roots, 1)
|
|
assert.Equal(t, root, plan.Roots[0].Path)
|
|
assert.False(t, plan.Roots[0].Recursive)
|
|
assert.Equal(t, []string{shelleyDBName, shelleyDBName + "-*"}, plan.Roots[0].IncludeGlobs)
|
|
|
|
discovered, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, discovered, 1)
|
|
assert.Equal(t, AgentShelley, discovered[0].Provider)
|
|
assert.Equal(t, dbPath, discovered[0].DisplayPath)
|
|
assert.Equal(t, dbPath, discovered[0].FingerprintKey)
|
|
|
|
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
FullSessionID: "host~shelley:cMAIN1",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, found.DisplayPath)
|
|
assert.Equal(t, virtualPath, found.FingerprintKey)
|
|
|
|
fingerprint, err := provider.Fingerprint(context.Background(), found)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, virtualPath, fingerprint.Key)
|
|
assert.Positive(t, fingerprint.Size)
|
|
assert.NotZero(t, fingerprint.MTimeNS)
|
|
assert.NotEmpty(t, fingerprint.Hash)
|
|
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{Path: dbPath + "-shm", EventKind: "write", WatchRoot: root},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
assert.Equal(t, dbPath, changed[0].DisplayPath)
|
|
}
|
|
|
|
func TestShelleyProviderParsePhysicalAndVirtualSources(t *testing.T) {
|
|
|
|
fixture := shelleyProviderReadFixture(t)
|
|
root := fixture.Root
|
|
dbPath := fixture.DBPath
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
sources, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, sources, 1)
|
|
|
|
allOutcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: sources[0],
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, allOutcome.ResultSetComplete)
|
|
require.True(t, allOutcome.ForceReplace)
|
|
require.Len(t, allOutcome.Results, 2)
|
|
assert.Equal(t, "shelley:cAUX1", allOutcome.Results[0].Result.Session.ID)
|
|
assert.Equal(t, "shelley:cMAIN1", allOutcome.Results[1].Result.Session.ID)
|
|
|
|
virtualSource, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: ShelleyVirtualPath(dbPath, "cMAIN1"),
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
oneOutcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: virtualSource,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, oneOutcome.ResultSetComplete)
|
|
require.True(t, oneOutcome.ForceReplace)
|
|
require.Len(t, oneOutcome.Results, 1)
|
|
assert.Equal(t, "shelley:cMAIN1", oneOutcome.Results[0].Result.Session.ID)
|
|
assert.Equal(t, "devbox", oneOutcome.Results[0].Result.Session.Machine)
|
|
assert.Len(t, oneOutcome.Results[0].Result.Messages, 5)
|
|
}
|
|
|
|
func TestShelleyProviderFingerprintChangesForSameSecondRewrite(t *testing.T) {
|
|
|
|
root, _, db := newShelleyTestDB(t)
|
|
seedShelleyMainConversation(t, db)
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
RawSessionID: "cMAIN1",
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
before, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
_, err = db.Exec(
|
|
`UPDATE messages
|
|
SET llm_data = ?
|
|
WHERE conversation_id = ? AND sequence_id = ?`,
|
|
`{"Role":1,"Content":[{"Type":2,"Text":"Changed content."}]}`,
|
|
"cMAIN1",
|
|
4,
|
|
)
|
|
require.NoError(t, err)
|
|
after, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, before.MTimeNS, after.MTimeNS)
|
|
assert.NotEqual(t, before.Hash, after.Hash)
|
|
}
|
|
|
|
func TestShelleyProviderFingerprintIncludesWALSiblings(t *testing.T) {
|
|
|
|
root, dbPath, db := newShelleyTestDB(t)
|
|
seedShelleyMainConversation(t, db)
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
sources, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, sources, 1)
|
|
before, err := provider.Fingerprint(context.Background(), sources[0])
|
|
require.NoError(t, err)
|
|
|
|
walPath := dbPath + "-wal"
|
|
writeSourceFile(t, walPath, "wal")
|
|
walTime := time.Unix(0, before.MTimeNS+int64(time.Second))
|
|
require.NoError(t, os.Chtimes(walPath, walTime, walTime))
|
|
after, err := provider.Fingerprint(context.Background(), sources[0])
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, before.Size, after.Size)
|
|
assert.Greater(t, after.MTimeNS, before.MTimeNS)
|
|
}
|
|
|
|
func TestShelleyProviderClassifiesDeletedVirtualPath(t *testing.T) {
|
|
root, dbPath, db := newShelleyTestDB(t)
|
|
seedShelleyMainConversation(t, db)
|
|
virtualPath := ShelleyVirtualPath(dbPath, "cMAIN1")
|
|
// Close the setup handle before deleting; Windows will not unlink a file
|
|
// this process still holds open.
|
|
require.NoError(t, db.Close())
|
|
require.NoError(t, os.Remove(dbPath))
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{Path: virtualPath, EventKind: "remove", WatchRoot: root},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
assert.Equal(t, virtualPath, changed[0].DisplayPath)
|
|
}
|
|
|
|
func TestShelleyProviderClassifiesDeletedPhysicalDB(t *testing.T) {
|
|
root, dbPath, db := newShelleyTestDB(t)
|
|
seedShelleyMainConversation(t, db)
|
|
// Close the setup handle before deleting; Windows will not unlink a file
|
|
// this process still holds open.
|
|
require.NoError(t, db.Close())
|
|
require.NoError(t, os.Remove(dbPath))
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{Path: dbPath, EventKind: "remove", WatchRoot: root},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
assert.Equal(t, dbPath, changed[0].DisplayPath)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: changed[0]})
|
|
require.NoError(t, err)
|
|
assert.True(t, outcome.ResultSetComplete)
|
|
// The backing DB file is gone, so the outcome must not force-replace: the
|
|
// persistent archive preserves sessions whose source file no longer exists.
|
|
assert.False(t, outcome.ForceReplace)
|
|
assert.Equal(t, SkipNoSession, outcome.SkipReason)
|
|
assert.Empty(t, outcome.Results)
|
|
}
|
|
|
|
// TestShelleyProviderChangedPathTombstonesDeletedConversation verifies the
|
|
// changed-path classifier emits a tombstone for a stored Shelley conversation
|
|
// deleted from a still-present database, and that the tombstone's fingerprint
|
|
// no longer errors (it previously aborted before Parse and stranded the stale
|
|
// session). The surviving conversation is left to the whole-DB fan-out; a
|
|
// vanished database emits no tombstone.
|
|
func TestShelleyProviderChangedPathTombstonesDeletedConversation(t *testing.T) {
|
|
root, dbPath, db := newShelleyTestDB(t)
|
|
seedShelleyMainConversation(t, db) // cMAIN1 survives
|
|
seedShelleyConversation(
|
|
t, db, "cDEL1", "Doomed conversation",
|
|
"/home/user/dev/myapp", "claude-sonnet-4-6", "", true,
|
|
"2026-06-15T11:00:00Z", "2026-06-15T11:05:00Z",
|
|
)
|
|
seedShelleyMessage(t, db, "cDEL1", 1, 1, "user",
|
|
`{"Role":0,"Content":[{"Type":2,"Text":"hi"}]}`, "", "",
|
|
"2026-06-15T11:00:00Z")
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{
|
|
Roots: []string{root},
|
|
Machine: "devbox",
|
|
})
|
|
require.True(t, ok)
|
|
|
|
survivingPath := ShelleyVirtualPath(dbPath, "cMAIN1")
|
|
deletedPath := ShelleyVirtualPath(dbPath, "cDEL1")
|
|
|
|
_, err := db.Exec(`DELETE FROM messages WHERE conversation_id = ?`, "cDEL1")
|
|
require.NoError(t, err)
|
|
_, err = db.Exec(`DELETE FROM conversations WHERE conversation_id = ?`, "cDEL1")
|
|
require.NoError(t, err)
|
|
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{
|
|
Path: dbPath,
|
|
EventKind: "write",
|
|
WatchRoot: root,
|
|
StoredSourcePaths: []string{survivingPath, deletedPath},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
gotPaths := make([]string, len(changed))
|
|
for i, src := range changed {
|
|
gotPaths[i] = src.DisplayPath
|
|
}
|
|
assert.ElementsMatch(t, []string{dbPath, deletedPath}, gotPaths,
|
|
"whole-DB source plus a tombstone for the deleted conversation only")
|
|
|
|
var tombstone SourceRef
|
|
for _, src := range changed {
|
|
if src.DisplayPath == deletedPath {
|
|
tombstone = src
|
|
}
|
|
}
|
|
require.NotEmpty(t, tombstone.DisplayPath, "deleted-conversation tombstone source")
|
|
|
|
// The fingerprint of a deleted-but-present-DB member must not error, or the
|
|
// engine aborts before Parse and the stale session is never dropped.
|
|
fingerprint, err := provider.Fingerprint(context.Background(), tombstone)
|
|
require.NoError(t, err, "missing-member fingerprint must not error")
|
|
assert.Equal(t, tombstone.FingerprintKey, fingerprint.Key)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: tombstone,
|
|
Fingerprint: fingerprint,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, outcome.ResultSetComplete)
|
|
assert.True(t, outcome.ForceReplace,
|
|
"a conversation deleted from a present DB is force-replaced out of the archive")
|
|
assert.Equal(t, SkipNoSession, outcome.SkipReason)
|
|
assert.Empty(t, outcome.Results)
|
|
|
|
// A vanished database file emits no tombstone (stored sessions preserved).
|
|
require.NoError(t, db.Close())
|
|
require.NoError(t, os.Remove(dbPath))
|
|
gone, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{
|
|
Path: dbPath,
|
|
EventKind: "remove",
|
|
WatchRoot: root,
|
|
StoredSourcePaths: []string{survivingPath, deletedPath},
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
for _, src := range gone {
|
|
assert.NotEqual(t, deletedPath, src.DisplayPath,
|
|
"a vanished database must not tombstone stored sessions")
|
|
assert.NotEqual(t, survivingPath, src.DisplayPath)
|
|
}
|
|
}
|
|
|
|
func TestShelleyProviderStoredVirtualPathFreshness(t *testing.T) {
|
|
root, dbPath, db := newShelleyTestDB(t)
|
|
seedShelleyMainConversation(t, db)
|
|
virtualPath := ShelleyVirtualPath(dbPath, "cMAIN1")
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: virtualPath,
|
|
RequireFreshSource: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, found.DisplayPath)
|
|
|
|
_, err = db.Exec(`DELETE FROM messages WHERE conversation_id = ?`, "cMAIN1")
|
|
require.NoError(t, err)
|
|
_, err = db.Exec(`DELETE FROM conversations WHERE conversation_id = ?`, "cMAIN1")
|
|
require.NoError(t, err)
|
|
|
|
_, ok, err = provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: virtualPath,
|
|
RequireFreshSource: true,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, ok, "fresh lookup must reject a deleted virtual row")
|
|
|
|
staleSource, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: virtualPath,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok, "non-fresh lookup keeps tombstone source identity")
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{
|
|
Source: staleSource,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.True(t, outcome.ResultSetComplete)
|
|
assert.True(t, outcome.ForceReplace)
|
|
assert.Equal(t, SkipNoSession, outcome.SkipReason)
|
|
assert.Empty(t, outcome.Results)
|
|
}
|
|
|
|
func TestShelleyProviderRejectsInvalidStoredVirtualPaths(t *testing.T) {
|
|
root, dbPath, db := newShelleyTestDB(t)
|
|
seedShelleyMainConversation(t, db)
|
|
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
for _, path := range []string{
|
|
dbPath + "#",
|
|
filepath.Join(root, "shelley-debug.db") + "#cMAIN1",
|
|
filepath.Join(root, "nested", shelleyDBName) + "#cMAIN1",
|
|
} {
|
|
_, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: path,
|
|
RequireFreshSource: true,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, ok, "stored path %q", path)
|
|
}
|
|
}
|
|
|
|
func TestShelleyProviderIgnoresUnrelatedSidecarBasename(t *testing.T) {
|
|
root := t.TempDir()
|
|
provider, ok := NewProvider(AgentShelley, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
changed, err := provider.SourcesForChangedPath(
|
|
context.Background(),
|
|
ChangedPathRequest{
|
|
Path: filepath.Join(root, "nested", shelleyDBName+"-wal"),
|
|
EventKind: "remove",
|
|
WatchRoot: filepath.Join(root, "nested"),
|
|
},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, changed)
|
|
}
|