f99010fae1
Desktop Artifacts / Desktop Build (Linux) (push) Waiting to run
Desktop Artifacts / Desktop Build (Windows) (push) Waiting to run
Desktop Artifacts / Desktop Build (Linux (arm64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (aarch64)) (push) Waiting to run
Desktop Artifacts (macOS) / Desktop Build (macOS (x86_64)) (push) Waiting to run
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
377 lines
11 KiB
Go
377 lines
11 KiB
Go
package sync
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.kenn.io/agentsview/internal/db"
|
|
"go.kenn.io/agentsview/internal/parser"
|
|
)
|
|
|
|
type verifiedSourceCountingProvider struct {
|
|
parser.ProviderBase
|
|
root string
|
|
sources map[string]parser.SourceRef
|
|
fingerprintCalls int
|
|
}
|
|
|
|
func (p *verifiedSourceCountingProvider) WatchPlan(
|
|
context.Context,
|
|
) (parser.WatchPlan, error) {
|
|
return parser.WatchPlan{Roots: []parser.WatchRoot{{
|
|
Path: p.root, Recursive: true,
|
|
}}}, nil
|
|
}
|
|
|
|
func (p *verifiedSourceCountingProvider) SourcesForChangedPath(
|
|
_ context.Context,
|
|
req parser.ChangedPathRequest,
|
|
) ([]parser.SourceRef, error) {
|
|
if source, ok := p.sources[filepath.Clean(req.Path)]; ok {
|
|
return []parser.SourceRef{source}, nil
|
|
}
|
|
return nil, nil
|
|
}
|
|
|
|
func (p *verifiedSourceCountingProvider) Fingerprint(
|
|
_ context.Context,
|
|
source parser.SourceRef,
|
|
) (parser.SourceFingerprint, error) {
|
|
p.fingerprintCalls++
|
|
return verifiedSourceFingerprint(source.DisplayPath)
|
|
}
|
|
|
|
func (p *verifiedSourceCountingProvider) Parse(
|
|
context.Context,
|
|
parser.ParseRequest,
|
|
) (parser.ParseOutcome, error) {
|
|
return parser.ParseOutcome{}, fmt.Errorf(
|
|
"unexpected parse after seeding stored source state",
|
|
)
|
|
}
|
|
|
|
type verifiedSourceCountingFactory struct {
|
|
provider *verifiedSourceCountingProvider
|
|
}
|
|
|
|
func (f verifiedSourceCountingFactory) Definition() parser.AgentDef {
|
|
return f.provider.Definition()
|
|
}
|
|
|
|
func (f verifiedSourceCountingFactory) Capabilities() parser.Capabilities {
|
|
return f.provider.Capabilities()
|
|
}
|
|
|
|
func (f verifiedSourceCountingFactory) NewProvider(
|
|
parser.ProviderConfig,
|
|
) parser.Provider {
|
|
return f.provider
|
|
}
|
|
|
|
func verifiedSourceFingerprint(path string) (parser.SourceFingerprint, error) {
|
|
info, err := os.Stat(path)
|
|
if err != nil {
|
|
return parser.SourceFingerprint{}, err
|
|
}
|
|
return parser.SourceFingerprint{
|
|
Key: path,
|
|
Size: info.Size(),
|
|
MTimeNS: parser.CodexEffectiveMtime(path, info.ModTime().UnixNano()),
|
|
Hash: "verified:" + filepath.Base(path),
|
|
}, nil
|
|
}
|
|
|
|
func newVerifiedSourceArchive(
|
|
t *testing.T,
|
|
count int,
|
|
) (*Engine, *verifiedSourceCountingProvider, []parser.DiscoveredFile) {
|
|
return newVerifiedSourceArchiveWithRewriter(t, count, nil)
|
|
}
|
|
|
|
func newVerifiedSourceArchiveWithRewriter(
|
|
t *testing.T,
|
|
count int,
|
|
pathRewriter func(string) string,
|
|
) (*Engine, *verifiedSourceCountingProvider, []parser.DiscoveredFile) {
|
|
t.Helper()
|
|
root := t.TempDir()
|
|
database := openTestDB(t)
|
|
provider := &verifiedSourceCountingProvider{
|
|
ProviderBase: parser.ProviderBase{
|
|
Def: parser.AgentDef{
|
|
Type: parser.AgentCodex, DisplayName: "Codex",
|
|
IDPrefix: "codex:", FileBased: true,
|
|
},
|
|
Caps: parser.Capabilities{Source: parser.SourceCapabilities{
|
|
WatchSources: parser.CapabilitySupported,
|
|
ClassifyChangedPath: parser.CapabilitySupported,
|
|
CompositeFingerprint: parser.CapabilitySupported,
|
|
VerifiedLocalStat: parser.CapabilitySupported,
|
|
}},
|
|
},
|
|
root: root,
|
|
sources: make(map[string]parser.SourceRef, count),
|
|
}
|
|
factory := verifiedSourceCountingFactory{provider: provider}
|
|
engine := NewEngine(database, EngineConfig{
|
|
AgentDirs: map[parser.AgentType][]string{
|
|
parser.AgentCodex: {root},
|
|
},
|
|
Machine: "host",
|
|
PathRewriter: pathRewriter,
|
|
ProviderFactories: []parser.ProviderFactory{factory},
|
|
ProviderMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
|
|
parser.AgentCodex: parser.ProviderMigrationProviderAuthoritative,
|
|
},
|
|
})
|
|
|
|
files := make([]parser.DiscoveredFile, 0, count)
|
|
for i := range count {
|
|
uuid := fmt.Sprintf("00000000-0000-0000-0000-%012d", i+1)
|
|
path := filepath.Join(
|
|
root,
|
|
"rollout-2026-07-11T00-00-00-"+uuid+".jsonl",
|
|
)
|
|
require.NoError(t, os.WriteFile(path, []byte("session\n"), 0o600))
|
|
fingerprint, err := verifiedSourceFingerprint(path)
|
|
require.NoError(t, err)
|
|
filePath := path
|
|
if pathRewriter != nil {
|
|
filePath = pathRewriter(path)
|
|
}
|
|
fileSize := fingerprint.Size
|
|
fileMtime := fingerprint.MTimeNS
|
|
fileHash := fingerprint.Hash
|
|
require.NoError(t, database.UpsertSession(db.Session{
|
|
ID: "codex:" + uuid, Project: "project", Machine: "host",
|
|
Agent: string(parser.AgentCodex), FilePath: &filePath,
|
|
FileSize: &fileSize, FileMtime: &fileMtime, FileHash: &fileHash,
|
|
}))
|
|
require.NoError(t, database.SetSessionDataVersion(
|
|
"codex:"+uuid, db.CurrentDataVersion(),
|
|
))
|
|
|
|
source := parser.SourceRef{
|
|
Provider: parser.AgentCodex, Key: path,
|
|
DisplayPath: path, FingerprintKey: path,
|
|
ProjectHint: "project",
|
|
}
|
|
provider.sources[filepath.Clean(path)] = source
|
|
sourceCopy := source
|
|
files = append(files, parser.DiscoveredFile{
|
|
Path: path, Agent: parser.AgentCodex,
|
|
ProviderSource: &sourceCopy, ProviderProcess: true,
|
|
})
|
|
}
|
|
return engine, provider, files
|
|
}
|
|
|
|
func runVerifiedSourcePass(
|
|
t *testing.T,
|
|
engine *Engine,
|
|
files []parser.DiscoveredFile,
|
|
) {
|
|
t.Helper()
|
|
pass := engine.beginVerifiedSourcePass()
|
|
for _, file := range files {
|
|
res := engine.processFile(context.Background(), file)
|
|
require.NoError(t, res.err)
|
|
assert.True(t, res.skip)
|
|
}
|
|
engine.finishVerifiedSourcePass(pass, true)
|
|
}
|
|
|
|
func TestVerifiedSourceGateWarmFingerprintWorkIsCardinalityIndependent(
|
|
t *testing.T,
|
|
) {
|
|
for _, count := range []int{3, 40} {
|
|
t.Run(fmt.Sprintf("sources=%d", count), func(t *testing.T) {
|
|
engine, provider, files := newVerifiedSourceArchive(t, count)
|
|
|
|
runVerifiedSourcePass(t, engine, files)
|
|
assert.Equal(t, count, provider.fingerprintCalls,
|
|
"cold pass must deep-verify every source")
|
|
|
|
runVerifiedSourcePass(t, engine, files)
|
|
assert.Equal(t, count, provider.fingerprintCalls,
|
|
"warm pass must perform zero content fingerprints")
|
|
assert.Len(t, engine.verifiedSources, count)
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifiedSourceGateWarmTrustDoesNotMaskDatabaseRepair(t *testing.T) {
|
|
const sessionID = "codex:00000000-0000-0000-0000-000000000001"
|
|
tests := []struct {
|
|
name string
|
|
mutate func(*testing.T, *db.DB)
|
|
}{
|
|
{
|
|
name: "missing row",
|
|
mutate: func(t *testing.T, database *db.DB) {
|
|
t.Helper()
|
|
require.NoError(t, database.DeleteSession(sessionID))
|
|
},
|
|
},
|
|
{
|
|
name: "stale data version",
|
|
mutate: func(t *testing.T, database *db.DB) {
|
|
t.Helper()
|
|
require.NoError(t, database.SetSessionDataVersion(
|
|
sessionID, db.CurrentDataVersion()-1,
|
|
))
|
|
},
|
|
},
|
|
{
|
|
name: "project requires reparse",
|
|
mutate: func(t *testing.T, database *db.DB) {
|
|
t.Helper()
|
|
session, err := database.GetSessionFull(
|
|
context.Background(), sessionID,
|
|
)
|
|
require.NoError(t, err)
|
|
require.NotNil(t, session)
|
|
session.Project = "_tmp_workspace"
|
|
require.NoError(t, database.UpsertSession(*session))
|
|
},
|
|
},
|
|
{
|
|
name: "file mtimes reset",
|
|
mutate: func(t *testing.T, database *db.DB) {
|
|
t.Helper()
|
|
require.NoError(t, database.ResetAllMtimes())
|
|
},
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
engine, provider, files := newVerifiedSourceArchive(t, 1)
|
|
runVerifiedSourcePass(t, engine, files)
|
|
runVerifiedSourcePass(t, engine, files)
|
|
require.Equal(t, 1, provider.fingerprintCalls)
|
|
|
|
tt.mutate(t, engine.db)
|
|
res := engine.processFile(context.Background(), files[0])
|
|
|
|
require.ErrorContains(t, res.err,
|
|
"unexpected parse after seeding stored source state")
|
|
assert.Equal(t, 2, provider.fingerprintCalls,
|
|
"persisted state requiring repair must bypass warm trust")
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestVerifiedSourceGateRechecksAfterStatAndWatcherInvalidation(t *testing.T) {
|
|
engine, provider, files := newVerifiedSourceArchive(t, 1)
|
|
file := files[0]
|
|
runVerifiedSourcePass(t, engine, files)
|
|
runVerifiedSourcePass(t, engine, files)
|
|
require.Equal(t, 1, provider.fingerprintCalls)
|
|
|
|
info, err := os.Stat(file.Path)
|
|
require.NoError(t, err)
|
|
require.NoError(t, os.WriteFile(file.Path, []byte("changed\n"), 0o600))
|
|
require.NoError(t, os.Chtimes(file.Path, info.ModTime(), info.ModTime()))
|
|
runVerifiedSourcePass(t, engine, files)
|
|
assert.Equal(t, 2, provider.fingerprintCalls,
|
|
"same-size rewrite with restored mtime must deep-verify")
|
|
|
|
classified := engine.classifyPaths([]string{file.Path})
|
|
require.Len(t, classified, 1)
|
|
res := engine.processFile(context.Background(), classified[0])
|
|
require.NoError(t, res.err)
|
|
assert.True(t, res.skip)
|
|
assert.Equal(t, 3, provider.fingerprintCalls,
|
|
"a watcher-classified source must invalidate warm trust")
|
|
|
|
engine.clearWatcherOverflowCaches()
|
|
res = engine.processFile(context.Background(), file)
|
|
require.NoError(t, res.err)
|
|
assert.True(t, res.skip)
|
|
assert.Equal(t, 4, provider.fingerprintCalls,
|
|
"watcher overflow must clear every verified-source trust record")
|
|
}
|
|
|
|
func TestVerifiedSourceGateBypassesPathRewrittenSources(t *testing.T) {
|
|
rewriter := func(path string) string { return "remote:" + path }
|
|
engine, provider, files := newVerifiedSourceArchiveWithRewriter(
|
|
t, 1, rewriter,
|
|
)
|
|
|
|
runVerifiedSourcePass(t, engine, files)
|
|
runVerifiedSourcePass(t, engine, files)
|
|
assert.Equal(t, 2, provider.fingerprintCalls,
|
|
"remote materializations must deep-verify on every pass")
|
|
assert.Empty(t, engine.verifiedSources)
|
|
}
|
|
|
|
func TestVerifiedSourceGateLegacyClaudeRowMustEstablishFingerprint(t *testing.T) {
|
|
root := t.TempDir()
|
|
path := filepath.Join(root, "legacy-session.jsonl")
|
|
require.NoError(t, os.WriteFile(path, []byte("session\n"), 0o600))
|
|
info, err := os.Stat(path)
|
|
require.NoError(t, err)
|
|
|
|
database := openTestDB(t)
|
|
fileSize := info.Size()
|
|
fileMtime := info.ModTime().UnixNano()
|
|
require.NoError(t, database.UpsertSession(db.Session{
|
|
ID: "claude:legacy-session", Project: "project", Machine: "host",
|
|
Agent: string(parser.AgentClaude), FilePath: &path,
|
|
FileSize: &fileSize, FileMtime: &fileMtime,
|
|
}))
|
|
require.NoError(t, database.SetSessionDataVersion(
|
|
"claude:legacy-session", db.CurrentDataVersion(),
|
|
))
|
|
|
|
provider := &verifiedSourceCountingProvider{
|
|
ProviderBase: parser.ProviderBase{
|
|
Def: parser.AgentDef{
|
|
Type: parser.AgentClaude, DisplayName: "Claude",
|
|
IDPrefix: "claude:", FileBased: true,
|
|
},
|
|
Caps: parser.Capabilities{Source: parser.SourceCapabilities{
|
|
IncrementalAppend: parser.CapabilitySupported,
|
|
VerifiedLocalStat: parser.CapabilitySupported,
|
|
}},
|
|
},
|
|
root: root,
|
|
sources: make(map[string]parser.SourceRef),
|
|
}
|
|
engine := NewEngine(database, EngineConfig{
|
|
AgentDirs: map[parser.AgentType][]string{
|
|
parser.AgentClaude: {root},
|
|
},
|
|
Machine: "host",
|
|
ProviderFactories: []parser.ProviderFactory{verifiedSourceCountingFactory{provider}},
|
|
ProviderMigrationModes: map[parser.AgentType]parser.ProviderMigrationMode{
|
|
parser.AgentClaude: parser.ProviderMigrationProviderAuthoritative,
|
|
},
|
|
})
|
|
source := parser.SourceRef{
|
|
Provider: parser.AgentClaude, Key: path,
|
|
DisplayPath: path, FingerprintKey: path,
|
|
ProjectHint: "project",
|
|
}
|
|
res := engine.processFile(context.Background(), parser.DiscoveredFile{
|
|
Path: path, Agent: parser.AgentClaude,
|
|
ProviderSource: &source, ProviderProcess: true,
|
|
})
|
|
|
|
require.ErrorContains(t, res.err,
|
|
"unexpected parse after seeding stored source state")
|
|
assert.Equal(t, 1, provider.fingerprintCalls,
|
|
"a legacy row without file_hash must not keep taking the stat-only skip")
|
|
record, ok := engine.verifiedSources[path]
|
|
require.True(t, ok)
|
|
assert.False(t, record.trusted,
|
|
"failed parsing must not promote source trust")
|
|
}
|