Skip to content

Commit 058ca18

Browse files
fix(parser): validate db-backed stored sources
Stored source-path hints for Forge, Piebald, and Warp are virtual DB-row identities, so a fresh lookup must prove both the canonical root DB and the requested row still exist. Accepting stale virtual paths would let changed-path migration parse the wrong source or skip a deleted row. Keep non-fresh lookup permissive for tombstone parsing, but make RequireFreshSource authoritative for stored hints and raw-session mismatches before falling back to session lookup. Validation: go test -tags "fts5" ./internal/parser -run 'TestDBBackedProvider(StoredVirtualPathFreshness|RejectsInvalidStoredVirtualPaths)' -count=1 -v; go test -tags "fts5" ./internal/parser -run 'Test(Forge|Piebald|Warp|DBBackedProvider)' -count=1 -v; go test -tags "fts5" ./internal/sync -run 'TestObserveProviderSourceMatchesDBBackedLegacyParsers|TestProcessFileProviderChangedPathForgeVirtualSource|TestProcessFileProviderAuthoritativeSourceErrorsOnlyForceParse' -count=1 -v; go vet ./...; GOMAXPROCS=1 GOGC=5 GOMEMLIMIT=128MiB ./custom-gcl run --config .golangci.nilaway.yml ./internal/parser ./internal/sync; git diff --check. Broad go test -tags "fts5" ./internal/parser ./internal/sync -count=1 still fails on existing TestSyncPathsCodexIndexEventRefreshesStoredDuplicate.
1 parent c42834d commit 058ca18

2 files changed

Lines changed: 133 additions & 1 deletion

File tree

internal/parser/db_backed_provider.go

Lines changed: 35 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -264,16 +264,34 @@ func (s dbBackedSourceSet) FindSource(
264264
if err := ctx.Err(); err != nil {
265265
return SourceRef{}, false, err
266266
}
267+
freshStoredSource := req.RequireFreshSource &&
268+
(req.StoredFilePath != "" || req.FingerprintKey != "")
267269
for _, path := range []string{req.StoredFilePath, req.FingerprintKey} {
268270
if path == "" {
269271
continue
270272
}
271273
for _, root := range s.roots {
272274
if source, ok := s.sourceRef(root, path, true); ok {
275+
src := source.Opaque.(dbBackedSource)
276+
if req.RawSessionID != "" && src.SessionID != req.RawSessionID {
277+
continue
278+
}
279+
if req.RequireFreshSource {
280+
fresh, err := s.sourceExists(src)
281+
if err != nil {
282+
return SourceRef{}, false, err
283+
}
284+
if !fresh {
285+
continue
286+
}
287+
}
273288
return source, true, nil
274289
}
275290
}
276291
}
292+
if freshStoredSource {
293+
return SourceRef{}, false, nil
294+
}
277295
if req.RawSessionID == "" {
278296
return SourceRef{}, false, nil
279297
}
@@ -295,6 +313,22 @@ func (s dbBackedSourceSet) FindSource(
295313
return SourceRef{}, false, nil
296314
}
297315

316+
func (s dbBackedSourceSet) sourceExists(src dbBackedSource) (bool, error) {
317+
if !IsRegularFile(src.DBPath) {
318+
return false, nil
319+
}
320+
metas, err := s.spec.listMeta(src.DBPath)
321+
if err != nil {
322+
return false, err
323+
}
324+
for _, meta := range metas {
325+
if meta.SessionID == src.SessionID {
326+
return true, nil
327+
}
328+
}
329+
return false, nil
330+
}
331+
298332
func (s dbBackedSourceSet) Fingerprint(
299333
ctx context.Context,
300334
source SourceRef,
@@ -361,7 +395,7 @@ func (s dbBackedSourceSet) sourceRef(
361395
if filepath.Base(dbPath) != s.spec.dbName {
362396
return SourceRef{}, false
363397
}
364-
if !pathIsUnderRoot(dbPath, root) {
398+
if !samePath(dbPath, filepath.Join(root, s.spec.dbName)) {
365399
return SourceRef{}, false
366400
}
367401
if !allowMissing && !IsRegularFile(dbPath) {

internal/parser/db_backed_provider_test.go

Lines changed: 98 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,104 @@ func TestDBBackedProviderDeletedRowFingerprintsTombstoneAndSkips(t *testing.T) {
207207
assert.Empty(t, outcome.Results)
208208
}
209209

210+
func TestDBBackedProviderStoredVirtualPathFreshness(t *testing.T) {
211+
dbPath, seeder, db := newForgeTestDB(t)
212+
seedForgeConversation(t, seeder)
213+
root := filepath.Dir(dbPath)
214+
virtualPath := dbPath + "#conv-001"
215+
216+
provider, ok := NewProvider(AgentForge, ProviderConfig{Roots: []string{root}})
217+
require.True(t, ok)
218+
found, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
219+
StoredFilePath: virtualPath,
220+
RequireFreshSource: true,
221+
})
222+
require.NoError(t, err)
223+
require.True(t, ok)
224+
assert.Equal(t, virtualPath, found.DisplayPath)
225+
226+
_, err = db.Exec(`DELETE FROM conversations WHERE conversation_id = ?`, "conv-001")
227+
require.NoError(t, err)
228+
_, ok, err = provider.FindSource(context.Background(), FindSourceRequest{
229+
StoredFilePath: virtualPath,
230+
RequireFreshSource: true,
231+
})
232+
require.NoError(t, err)
233+
assert.False(t, ok, "fresh lookup must reject a deleted DB row")
234+
235+
staleSource, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
236+
StoredFilePath: virtualPath,
237+
})
238+
require.NoError(t, err)
239+
require.True(t, ok, "non-fresh lookup keeps virtual tombstone identity")
240+
assert.Equal(t, virtualPath, staleSource.DisplayPath)
241+
outcome, err := provider.Parse(context.Background(), ParseRequest{
242+
Source: staleSource,
243+
})
244+
require.NoError(t, err)
245+
assert.True(t, outcome.ResultSetComplete)
246+
assert.True(t, outcome.ForceReplace)
247+
assert.Equal(t, SkipNoSession, outcome.SkipReason)
248+
assert.Empty(t, outcome.Results)
249+
250+
require.NoError(t, db.Close())
251+
require.NoError(t, os.Remove(dbPath))
252+
_, ok, err = provider.FindSource(context.Background(), FindSourceRequest{
253+
StoredFilePath: virtualPath,
254+
RequireFreshSource: true,
255+
})
256+
require.NoError(t, err)
257+
assert.False(t, ok, "fresh lookup must reject a deleted DB file")
258+
}
259+
260+
func TestDBBackedProviderRejectsInvalidStoredVirtualPaths(t *testing.T) {
261+
dbPath, seeder, db := newForgeTestDB(t)
262+
defer db.Close()
263+
seedForgeConversation(t, seeder)
264+
root := filepath.Dir(dbPath)
265+
virtualPath := dbPath + "#conv-001"
266+
otherPath := dbPath + "#conv-002"
267+
seeder.AddConversation(
268+
"conv-002",
269+
"Other",
270+
123,
271+
`{"conversation_id":"conv-002","messages":[]}`,
272+
"2026-05-03 09:58:15.000000000",
273+
"2026-05-03 10:00:16.000000000",
274+
"",
275+
)
276+
277+
provider, ok := NewProvider(AgentForge, ProviderConfig{Roots: []string{root}})
278+
require.True(t, ok)
279+
for _, path := range []string{
280+
dbPath + "#",
281+
filepath.Join(root, "forge-copy.db") + "#conv-001",
282+
filepath.Join(root, "nested", forgeDBFilename) + "#conv-001",
283+
} {
284+
_, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
285+
StoredFilePath: path,
286+
RequireFreshSource: true,
287+
})
288+
require.NoError(t, err)
289+
assert.False(t, ok, "stored path %q", path)
290+
}
291+
292+
_, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
293+
RawSessionID: "conv-001",
294+
StoredFilePath: otherPath,
295+
RequireFreshSource: true,
296+
})
297+
require.NoError(t, err)
298+
assert.False(t, ok, "fresh lookup must reject a stored path for a different session")
299+
300+
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
301+
StoredFilePath: virtualPath,
302+
})
303+
require.NoError(t, err)
304+
require.True(t, ok)
305+
assert.Equal(t, virtualPath, source.DisplayPath)
306+
}
307+
210308
func TestDBBackedProviderMissingDBFingerprintsTombstoneAndSkips(t *testing.T) {
211309
dbPath, seeder, db := newForgeTestDB(t)
212310
seedForgeConversation(t, seeder)

0 commit comments

Comments
 (0)