Skip to content

Commit 120e40f

Browse files
fix(sync): honor changed-path provider sources in shadow
Changed-path classification now carries provider-selected source identity, so shadow observation has to consume that same source instead of performing a fresh lookup from the legacy file path. Otherwise tombstone, virtual, and stored-hint sources can be compared against the wrong provider source or lose forced-parse semantics. Route shadow observation through the shared provider source resolver and propagate file-level force parsing into provider observation so shadow comparisons exercise the source selected by changed-path classification. Validation: go test -tags "fts5" ./internal/sync -run 'TestProcessFileShadow(ObservesProviderWithoutReplacingLegacy|UsesChangedPathProviderSource)|TestClassifyProviderChangedPath|TestProviderVirtualSourceBackedByEventPreservesHashInDBPath' -count=1; go test -tags "fts5" ./internal/db ./internal/parser ./internal/sync -count=1; go vet ./...; go fmt ./...; ./custom-gcl run --config .golangci.nilaway.yml ./internal/sync; git diff --check
1 parent 7d971a4 commit 120e40f

2 files changed

Lines changed: 101 additions & 6 deletions

File tree

internal/sync/engine.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4560,11 +4560,7 @@ func (e *Engine) observeProviderShadow(
45604560
Roots: e.agentDirs[file.Agent],
45614561
Machine: e.machine,
45624562
})
4563-
source, found, err := provider.FindSource(ctx, parser.FindSourceRequest{
4564-
StoredFilePath: file.Path,
4565-
FingerprintKey: file.Path,
4566-
RequireFreshSource: !e.forceParse,
4567-
})
4563+
source, found, err := e.providerSourceForDiscoveredFile(ctx, provider, file)
45684564
comparison.Err = err
45694565
if err == nil && found {
45704566
comparison.Source = source
@@ -4574,7 +4570,7 @@ func (e *Engine) observeProviderShadow(
45744570
ProviderObserveRequest{
45754571
Source: source,
45764572
Machine: e.machine,
4577-
ForceParse: e.forceParse,
4573+
ForceParse: e.forceParse || file.ForceParse,
45784574
},
45794575
)
45804576
if comparison.Err == nil {

internal/sync/provider_shadow_caller_test.go

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -237,6 +237,105 @@ func TestClassifyProviderChangedPathRunsAlongsideLegacyClassifier(
237237
assert.Equal(t, sourcePath, files[0].ProviderSource.DisplayPath)
238238
}
239239

240+
func TestProcessFileShadowUsesChangedPathProviderSource(t *testing.T) {
241+
root := t.TempDir()
242+
sourcePath := filepath.Join(root, "-Users-dev-code-demo", "shadow-provider-source.jsonl")
243+
require.NoError(t, os.MkdirAll(filepath.Dir(sourcePath), 0o755))
244+
require.NoError(t, os.WriteFile(
245+
sourcePath,
246+
[]byte(testjsonl.JoinJSONL(
247+
testjsonl.ClaudeUserJSON(
248+
"provider source should win",
249+
"2026-06-01T10:00:00Z",
250+
"/Users/dev/code/demo",
251+
),
252+
testjsonl.ClaudeAssistantJSON(
253+
"force parse should propagate",
254+
"2026-06-01T10:01:00Z",
255+
),
256+
)),
257+
0o644,
258+
))
259+
260+
legacyResults, legacyExcluded, err := parser.ParseClaudeSessionWithExclusions(
261+
sourcePath, "demo", "devbox",
262+
)
263+
require.NoError(t, err)
264+
require.Len(t, legacyResults, 1)
265+
require.Empty(t, legacyExcluded)
266+
info, err := os.Stat(sourcePath)
267+
require.NoError(t, err)
268+
providerResult := legacyResults[0]
269+
providerResult.Session.File.Inode, providerResult.Session.File.Device = getFileIdentity(info)
270+
hash, err := ComputeFileHash(sourcePath)
271+
require.NoError(t, err)
272+
providerResult.Session.File.Hash = hash
273+
274+
changedSource := parser.SourceRef{
275+
Provider: parser.AgentClaude,
276+
Key: "changed-path-source",
277+
DisplayPath: sourcePath,
278+
FingerprintKey: sourcePath,
279+
ProjectHint: "demo",
280+
}
281+
findFound := false
282+
provider := &shadowCallerProvider{
283+
shadowTestProvider: shadowTestProvider{
284+
ProviderBase: parser.ProviderBase{
285+
Def: parser.AgentDef{
286+
Type: parser.AgentClaude,
287+
DisplayName: "Claude Code",
288+
},
289+
},
290+
fingerprint: parser.SourceFingerprint{
291+
Key: sourcePath,
292+
Size: info.Size(),
293+
MTimeNS: info.ModTime().UnixNano(),
294+
},
295+
outcome: parser.ParseOutcome{
296+
Results: []parser.ParseResultOutcome{{
297+
Result: providerResult,
298+
DataVersion: parser.DataVersionCurrent,
299+
}},
300+
ResultSetComplete: true,
301+
},
302+
},
303+
findFound: &findFound,
304+
}
305+
var comparisons []ProviderShadowComparison
306+
engine := NewEngine(dbtest.OpenTestDB(t), EngineConfig{
307+
AgentDirs: map[parser.AgentType][]string{
308+
parser.AgentClaude: {root},
309+
},
310+
Machine: "devbox",
311+
ProviderFactories: []parser.ProviderFactory{
312+
shadowCallerFactory{provider: provider},
313+
},
314+
ProviderMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
315+
parser.AgentClaude: parser.ProviderMigrationShadowCompare,
316+
},
317+
ProviderShadowRecorder: func(comparison ProviderShadowComparison) {
318+
comparisons = append(comparisons, comparison)
319+
},
320+
})
321+
322+
result := engine.processFile(context.Background(), parser.DiscoveredFile{
323+
Path: sourcePath,
324+
Agent: parser.AgentClaude,
325+
ForceParse: true,
326+
ProviderSource: &changedSource,
327+
})
328+
329+
require.NoError(t, result.err)
330+
require.Len(t, comparisons, 1)
331+
assert.NoError(t, comparisons[0].Err)
332+
assert.Empty(t, comparisons[0].Mismatches)
333+
assert.Equal(t, changedSource, comparisons[0].Source)
334+
assert.Equal(t, changedSource, provider.parseRequest.Source)
335+
assert.True(t, provider.parseRequest.ForceParse)
336+
assert.Empty(t, provider.findRequest)
337+
}
338+
240339
func TestClassifyProviderChangedPathMarksAuthoritativeProviderProcess(
241340
t *testing.T,
242341
) {

0 commit comments

Comments
 (0)