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
661 lines
28 KiB
Go
661 lines
28 KiB
Go
package parser
|
|
|
|
import (
|
|
"context"
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
func assertDevinErrorRedacted(t *testing.T, err error, secretFragments ...string) {
|
|
t.Helper()
|
|
require.Error(t, err)
|
|
for _, fragment := range secretFragments {
|
|
assert.NotContains(t, err.Error(), fragment)
|
|
}
|
|
}
|
|
|
|
func TestDevinProviderCapabilities(t *testing.T) {
|
|
caps := devinProviderCapabilities()
|
|
assert.Equal(t, CapabilitySupported, caps.Content.PerMessageTokenUsage)
|
|
assert.Equal(t, CapabilityUnsupported, caps.Content.AggregateUsageEvents)
|
|
}
|
|
|
|
func TestDevinProviderDiscoverFindParse(t *testing.T) {
|
|
const sessionID = "session-123"
|
|
dbPath, _ := newDevinSessionFixture(t, devinSessionRow{
|
|
ID: sessionID,
|
|
Title: "DB title wins",
|
|
WorkingDirectory: "/Users/alice/code/my-app",
|
|
Model: "db-model",
|
|
CreatedAtMillis: new(int64(1704103199000)),
|
|
LastActivityMillis: new(int64(1704103265000)),
|
|
}, `{
|
|
"agent":{"model_name":"devin-1"},
|
|
"steps":[
|
|
{"step_id":"step-1","source":"user","timestamp":"2024-01-01T10:00:01Z","message":"Fix the login bug"},
|
|
{"step_id":"step-2","source":"agent","timestamp":"2024-01-01T10:00:05Z","message":[{"type":"text","text":"Inspecting files."}]}
|
|
]
|
|
}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
virtualPath := VirtualSourcePath(dbPath, sessionID)
|
|
|
|
provider, ok := NewProvider(AgentDevin, 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, 2)
|
|
assert.Equal(t, filepath.Join(root, "cli"), plan.Roots[0].Path)
|
|
assert.False(t, plan.Roots[0].Recursive)
|
|
assert.ElementsMatch(t,
|
|
[]string{devinDBFilename, devinDBFilename + "-*"},
|
|
plan.Roots[0].IncludeGlobs,
|
|
)
|
|
assert.Equal(t, filepath.Join(root, "cli", "transcripts"), plan.Roots[1].Path)
|
|
assert.False(t, plan.Roots[1].Recursive)
|
|
assert.Equal(t, []string{"*.json"}, plan.Roots[1].IncludeGlobs)
|
|
|
|
discovered, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, discovered, 1)
|
|
assert.Equal(t, virtualPath, discovered[0].Key)
|
|
assert.Equal(t, virtualPath, discovered[0].DisplayPath)
|
|
assert.Equal(t, virtualPath, discovered[0].FingerprintKey)
|
|
assert.Equal(t, int64(1704103265000000000), discovered[0].DiscoveryMTimeNS)
|
|
|
|
changed, err := provider.SourcesForChangedPath(context.Background(), ChangedPathRequest{
|
|
Path: filepath.Join(root, "cli", "transcripts", sessionID+".json"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli", "transcripts"),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
assert.Equal(t, virtualPath, changed[0].DisplayPath)
|
|
|
|
fullIDSource, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
FullSessionID: "host~devin:" + sessionID,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, fullIDSource.DisplayPath)
|
|
|
|
storedSource, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: virtualPath,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, storedSource.DisplayPath)
|
|
|
|
fingerprint, err := provider.Fingerprint(context.Background(), storedSource)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, virtualPath, fingerprint.Key)
|
|
assert.NotZero(t, fingerprint.MTimeNS)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: storedSource})
|
|
require.NoError(t, err)
|
|
require.True(t, outcome.ResultSetComplete)
|
|
require.True(t, outcome.ForceReplace)
|
|
require.Len(t, outcome.Results, 1)
|
|
result := outcome.Results[0]
|
|
assert.Equal(t, DataVersionCurrent, result.DataVersion)
|
|
assert.Equal(t, "devin:"+sessionID, result.Result.Session.ID)
|
|
assert.Equal(t, virtualPath, result.Result.Session.File.Path)
|
|
assert.Equal(t, "devbox", result.Result.Session.Machine)
|
|
assert.Len(t, result.Result.Messages, 2)
|
|
}
|
|
|
|
func TestDevinProviderDBEventsFanOutAndPreserveTombstones(t *testing.T) {
|
|
const liveSessionID = "session-live"
|
|
fixture := newDevinTestFixture(t,
|
|
devinSessionRow{ID: liveSessionID, Title: "Live", WorkingDirectory: "/tmp/live", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))},
|
|
devinSessionRow{ID: "session-deleted", Title: "Deleted", WorkingDirectory: "/tmp/deleted", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103264000))},
|
|
)
|
|
fixture.writeTranscript(t, liveSessionID, `{"steps":[]}`)
|
|
root := fixture.Root
|
|
liveVirtualPath := fixture.sessionVirtualPath(liveSessionID)
|
|
deletedVirtualPath := fixture.sessionVirtualPath("session-deleted")
|
|
dbPath := fixture.DBPath
|
|
execDevinTestSQL(t, dbPath, `DELETE FROM sessions WHERE id = 'session-deleted'`)
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
for _, changedPath := range []string{dbPath, dbPath + "-wal", dbPath + "-shm"} {
|
|
changed, err := provider.SourcesForChangedPath(context.Background(), ChangedPathRequest{
|
|
Path: changedPath,
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli"),
|
|
StoredSourcePaths: []string{deletedVirtualPath},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 2, changedPath)
|
|
assert.ElementsMatch(t,
|
|
[]string{liveVirtualPath, deletedVirtualPath},
|
|
[]string{changed[0].DisplayPath, changed[1].DisplayPath},
|
|
)
|
|
}
|
|
}
|
|
|
|
func TestDevinProviderTranscriptEventsTargetLiveOrStoredSession(t *testing.T) {
|
|
const liveSessionID = "session-live"
|
|
fixture := newDevinTestFixture(t,
|
|
devinSessionRow{ID: liveSessionID, Title: "Live", WorkingDirectory: "/tmp/live", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))},
|
|
devinSessionRow{ID: "session-deleted", Title: "Deleted", WorkingDirectory: "/tmp/deleted", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103264000))},
|
|
)
|
|
fixture.writeTranscript(t, liveSessionID, `{"steps":[]}`)
|
|
root := fixture.Root
|
|
liveVirtualPath := fixture.sessionVirtualPath(liveSessionID)
|
|
deletedVirtualPath := fixture.sessionVirtualPath("session-deleted")
|
|
dbPath := fixture.DBPath
|
|
execDevinTestSQL(t, dbPath, `DELETE FROM sessions WHERE id = 'session-deleted'`)
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
liveChanged, err := provider.SourcesForChangedPath(context.Background(), ChangedPathRequest{
|
|
Path: filepath.Join(root, "cli", "transcripts", liveSessionID+".json"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli", "transcripts"),
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, liveChanged, 1)
|
|
assert.Equal(t, liveVirtualPath, liveChanged[0].DisplayPath)
|
|
|
|
deletedChanged, err := provider.SourcesForChangedPath(context.Background(), ChangedPathRequest{
|
|
Path: filepath.Join(root, "cli", "transcripts", "session-deleted.json"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli", "transcripts"),
|
|
StoredSourcePaths: []string{deletedVirtualPath},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, deletedChanged, 1)
|
|
assert.Equal(t, deletedVirtualPath, deletedChanged[0].DisplayPath)
|
|
}
|
|
|
|
func TestDevinProviderRejectsUnrelatedChangedPaths(t *testing.T) {
|
|
const sessionID = "session-123"
|
|
dbPath, _ := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "Title", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
for _, req := range []ChangedPathRequest{
|
|
{
|
|
Path: filepath.Join(root, "cli", "sessions.db-backup"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli"),
|
|
},
|
|
{
|
|
Path: filepath.Join(root, "cli", "transcripts", sessionID+".txt"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli", "transcripts"),
|
|
},
|
|
{
|
|
Path: filepath.Join(root, "cli", "nested", devinDBFilename),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli"),
|
|
},
|
|
{
|
|
Path: filepath.Join(root, "other", "sessions.db"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "other"),
|
|
},
|
|
} {
|
|
changed, err := provider.SourcesForChangedPath(context.Background(), req)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, changed, "%+v", req)
|
|
}
|
|
}
|
|
|
|
func TestDevinProviderMissingTranscriptUsesMessageNodeFallback(t *testing.T) {
|
|
const sessionID = "session-db-only"
|
|
fixture := newDevinTestFixture(t,
|
|
devinSessionRow{ID: sessionID, Title: "DB only session", WorkingDirectory: "/tmp/db-only-project", Model: "db-only-model", CreatedAtMillis: new(int64(1704103200000)), LastActivityMillis: new(int64(1704103209000))},
|
|
)
|
|
root := fixture.Root
|
|
fixture.insertMessageNodes(t,
|
|
devinSyntheticMessageNodeRow{SessionID: sessionID, NodeID: 1, ChatMessage: `{"role":"user","content":"fallback user"}`, CreatedAtMillis: 1704103201000},
|
|
devinSyntheticMessageNodeRow{SessionID: sessionID, NodeID: 2, ChatMessage: `{"role":"assistant","content":"fallback assistant"}`, CreatedAtMillis: 1704103205000},
|
|
)
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}, Machine: "devbox"})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: source})
|
|
require.NoError(t, err)
|
|
assert.True(t, outcome.ResultSetComplete)
|
|
assert.True(t, outcome.ForceReplace)
|
|
require.Len(t, outcome.Results, 1)
|
|
assert.Empty(t, outcome.SourceErrors)
|
|
assert.Equal(t, "devin:"+sessionID, outcome.Results[0].Result.Session.ID)
|
|
assert.Len(t, outcome.Results[0].Result.Messages, 2)
|
|
assert.Equal(t, "fallback user", outcome.Results[0].Result.Messages[0].Content)
|
|
}
|
|
|
|
func TestDevinProviderMissingTranscriptWithoutDBMessagesReturnsProviderError(t *testing.T) {
|
|
const sessionID = "session-db-only-empty"
|
|
fixture := newDevinTestFixture(t,
|
|
devinSessionRow{ID: sessionID, Title: "DB only session", WorkingDirectory: "/tmp/db-only-project", Model: "db-only-model", CreatedAtMillis: new(int64(1704103200000)), LastActivityMillis: new(int64(1704103209000))},
|
|
)
|
|
root := fixture.Root
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}, Machine: "devbox"})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: source})
|
|
require.Error(t, err)
|
|
assert.Empty(t, outcome.Results)
|
|
assert.Empty(t, outcome.SourceErrors)
|
|
assert.ErrorContains(t, err, "missing devin transcript")
|
|
assert.NotContains(t, err.Error(), source.DisplayPath)
|
|
assert.NotContains(t, err.Error(), sessionID)
|
|
}
|
|
|
|
func TestDevinProviderCompositeFingerprintStableAndRedacted(t *testing.T) {
|
|
const sessionID = "session-fingerprint"
|
|
dbPath, transcriptPath := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "Stable title", WorkingDirectory: "/Users/alice/.config/devin/project", Model: "db-model", CreatedAtMillis: new(int64(1704103200000)), LastActivityMillis: new(int64(1704103209000)), MetadataJSON: `{"token_hint":"redacted"}`}, `{"token":"secret-token-123","steps":[{"step_id":"step-1","source":"user","message":"hello"}]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
virtualPath := VirtualSourcePath(dbPath, sessionID)
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
first, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
second, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, first, second)
|
|
assert.Equal(t, virtualPath, first.Key)
|
|
assert.NotZero(t, first.MTimeNS)
|
|
assert.NotEmpty(t, first.Hash)
|
|
assert.NotContains(t, first.Hash, "secret-token-123")
|
|
assert.NotContains(t, first.Hash, "/Users/alice/.config/devin/project")
|
|
assert.NotContains(t, first.Key, transcriptPath)
|
|
}
|
|
|
|
func TestDevinProviderFingerprintChangesWhenTranscriptChangesWithoutDBMetadataChange(t *testing.T) {
|
|
const sessionID = "session-transcript-change"
|
|
dbPath, transcriptPath := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "Transcript change", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103200000)), LastActivityMillis: new(int64(1704103209000))}, `{"steps":[{"step_id":"step-1","source":"user","message":"alpha"}]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
before, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
transcriptInfo, err := os.Stat(transcriptPath)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, os.WriteFile(transcriptPath, []byte(`{"steps":[{"step_id":"step-1","source":"user","message":"omega"}]}`), 0o644))
|
|
require.NoError(t, os.Chtimes(transcriptPath, transcriptInfo.ModTime(), transcriptInfo.ModTime()))
|
|
|
|
after, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, before.Key, after.Key)
|
|
assert.Equal(t, before.MTimeNS, after.MTimeNS)
|
|
assert.NotEqual(t, before.Hash, after.Hash)
|
|
}
|
|
|
|
func TestDevinProviderFingerprintChangesWhenLastActivityChanges(t *testing.T) {
|
|
const sessionID = "session-last-activity"
|
|
dbPath, _ := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "DB change", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103200000)), LastActivityMillis: new(int64(1704103209000))}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
before, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
execDevinTestSQL(t, dbPath, `UPDATE sessions SET last_activity_at = 1704103215000 WHERE id = 'session-last-activity'`)
|
|
|
|
after, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, before.Key, after.Key)
|
|
assert.Greater(t, after.MTimeNS, before.MTimeNS)
|
|
assert.NotEqual(t, before.Hash, after.Hash)
|
|
}
|
|
|
|
func TestDevinProviderFingerprintChangesWhenWorkingDirectoryChanges(t *testing.T) {
|
|
const sessionID = "session-cwd-change"
|
|
dbPath, _ := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "CWD change", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103200000)), LastActivityMillis: new(int64(1704103209000))}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
before, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
execDevinTestSQL(t, dbPath, `UPDATE sessions SET working_directory = '/tmp/renamed-app' WHERE id = 'session-cwd-change'`)
|
|
|
|
after, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, before.Key, after.Key)
|
|
assert.NotEqual(t, before.Hash, after.Hash)
|
|
}
|
|
|
|
func TestDevinProviderFingerprintWithoutTranscriptUsesDBFreshnessOnly(t *testing.T) {
|
|
const sessionID = "session-missing-transcript"
|
|
dbPath, transcriptPath := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "DB only session", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103200000)), LastActivityMillis: new(int64(1704103209000))}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
virtualPath := VirtualSourcePath(dbPath, sessionID)
|
|
require.NoError(t, os.Remove(transcriptPath))
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
fingerprint, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, virtualPath, fingerprint.Key)
|
|
assert.NotZero(t, fingerprint.MTimeNS)
|
|
assert.Zero(t, fingerprint.Size)
|
|
assert.NotEmpty(t, fingerprint.Hash)
|
|
assert.NotContains(t, fingerprint.Hash, transcriptPath)
|
|
}
|
|
|
|
func TestDevinProviderFingerprintWithoutTranscriptChangesWhenMessageNodesChange(t *testing.T) {
|
|
const sessionID = "session-message-node-change"
|
|
fixture := newDevinTestFixture(t,
|
|
devinSessionRow{ID: sessionID, Title: "DB messages", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103200000)), LastActivityMillis: new(int64(1704103209000))},
|
|
)
|
|
fixture.insertMessageNodes(t,
|
|
devinSyntheticMessageNodeRow{SessionID: sessionID, NodeID: 1, ChatMessage: `{"role":"user","content":"alpha"}`, CreatedAtMillis: 1704103201000},
|
|
)
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{fixture.Root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
before, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
execDevinTestSQL(t, fixture.DBPath, `UPDATE message_nodes SET chat_message = '{"role":"user","content":"omega"}' WHERE session_id = 'session-message-node-change'`)
|
|
|
|
after, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
|
|
assert.Equal(t, before.Key, after.Key)
|
|
assert.NotEqual(t, before.Hash, after.Hash)
|
|
}
|
|
|
|
func TestDevinProviderRejectsInvalidStoredVirtualPaths(t *testing.T) {
|
|
const sessionID = "session-123"
|
|
fixture := newDevinTestFixture(t,
|
|
devinSessionRow{ID: sessionID, Title: "Title", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))},
|
|
devinSessionRow{ID: "session-999", Title: "Other", WorkingDirectory: "/tmp/other", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))},
|
|
)
|
|
fixture.writeTranscript(t, sessionID, `{"steps":[]}`)
|
|
root := fixture.Root
|
|
virtualPath := fixture.sessionVirtualPath(sessionID)
|
|
otherPath := fixture.sessionVirtualPath("session-999")
|
|
dbPath := fixture.DBPath
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
for _, path := range []string{
|
|
dbPath + "#",
|
|
filepath.Join(root, "cli", "other.db") + "#" + sessionID,
|
|
filepath.Join(root, devinDBFilename) + "#" + sessionID,
|
|
filepath.Join(root, "cli", "nested", devinDBFilename) + "#" + sessionID,
|
|
} {
|
|
_, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
StoredFilePath: path,
|
|
RequireFreshSource: true,
|
|
})
|
|
require.NoError(t, err)
|
|
assert.False(t, ok, "stored path %q", path)
|
|
}
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{
|
|
RawSessionID: sessionID,
|
|
StoredFilePath: otherPath,
|
|
RequireFreshSource: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, source.DisplayPath)
|
|
}
|
|
|
|
func TestDevinProviderDedupesDuplicateRoots(t *testing.T) {
|
|
const sessionID = "session-123"
|
|
dbPath, _ := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "Title", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root, root, filepath.Join(root, ".")}})
|
|
require.True(t, ok)
|
|
|
|
discovered, err := provider.Discover(context.Background())
|
|
require.NoError(t, err)
|
|
require.Len(t, discovered, 1)
|
|
}
|
|
|
|
func TestDevinProviderDeletedRowFingerprintsTombstoneAndSkips(t *testing.T) {
|
|
const sessionID = "session-123"
|
|
dbPath, _ := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "Title", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
virtualPath := VirtualSourcePath(dbPath, sessionID)
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, source.DisplayPath)
|
|
execDevinTestSQL(t, dbPath, `DELETE FROM sessions WHERE id = 'session-123'`)
|
|
|
|
changed, err := provider.SourcesForChangedPath(context.Background(), ChangedPathRequest{
|
|
Path: dbPath,
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli"),
|
|
StoredSourcePaths: []string{virtualPath},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
|
|
fingerprint, err := provider.Fingerprint(context.Background(), changed[0])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, SourceFingerprint{Key: virtualPath}, fingerprint)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: changed[0]})
|
|
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 TestDevinProviderHiddenRowFingerprintsTombstoneAndSkips(t *testing.T) {
|
|
const sessionID = "session-hidden"
|
|
dbPath, _ := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "Title", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000)), Hidden: false}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
virtualPath := VirtualSourcePath(dbPath, sessionID)
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, virtualPath, source.DisplayPath)
|
|
execDevinTestSQL(t, dbPath, `UPDATE sessions SET hidden = 1 WHERE id = 'session-hidden'`)
|
|
|
|
changed, err := provider.SourcesForChangedPath(context.Background(), ChangedPathRequest{
|
|
Path: dbPath,
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(root, "cli"),
|
|
StoredSourcePaths: []string{virtualPath},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
|
|
fingerprint, err := provider.Fingerprint(context.Background(), changed[0])
|
|
require.NoError(t, err)
|
|
assert.Equal(t, SourceFingerprint{Key: virtualPath}, fingerprint)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: changed[0]})
|
|
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 TestDevinProviderCorruptTranscriptReturnsProviderError(t *testing.T) {
|
|
const sessionID = "session-corrupt"
|
|
dbPath, transcriptPath := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "Corrupt transcript", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
require.NoError(t, os.WriteFile(transcriptPath, []byte(`{"secret":"token-123","steps":[`), 0o644))
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: source})
|
|
assert.Empty(t, outcome.Results)
|
|
assert.Empty(t, outcome.SourceErrors)
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "invalid devin transcript")
|
|
assert.NotContains(t, err.Error(), transcriptPath)
|
|
assert.NotContains(t, err.Error(), sessionID)
|
|
assert.NotContains(t, err.Error(), "token-123")
|
|
}
|
|
|
|
func TestDevinProviderIgnoresCredentialPathsAndRedactsSecretBearingErrors(t *testing.T) {
|
|
const (
|
|
sessionID = "session-privacy"
|
|
secretSentinel = "oauth-token-SYNTHETIC-SECRET-SENTINEL"
|
|
)
|
|
fixture := newDevinTestFixture(t,
|
|
devinSessionRow{ID: sessionID, Title: "Privacy", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))},
|
|
)
|
|
transcriptPath := fixture.writeTranscript(t, sessionID, `{"api_key":"oauth-token-SYNTHETIC-SECRET-SENTINEL","steps":[`)
|
|
|
|
secretRoot := filepath.Join(t.TempDir(), secretSentinel, "config", "mcp", "oauth", "devin-root")
|
|
require.NoError(t, os.MkdirAll(filepath.Dir(secretRoot), 0o755))
|
|
require.NoError(t, os.Rename(fixture.Root, secretRoot))
|
|
|
|
dbPath := filepath.Join(secretRoot, "cli", devinDBFilename)
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{secretRoot}})
|
|
require.True(t, ok)
|
|
|
|
for _, req := range []ChangedPathRequest{
|
|
{
|
|
Path: filepath.Join(secretRoot, "cli", "config.json"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(secretRoot, "cli"),
|
|
},
|
|
{
|
|
Path: filepath.Join(secretRoot, "cli", "mcp", "oauth", "credentials.db"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(secretRoot, "cli"),
|
|
},
|
|
{
|
|
Path: filepath.Join(secretRoot, "cli", "mcp", "oauth", "token-cache.json"),
|
|
EventKind: "write",
|
|
WatchRoot: filepath.Join(secretRoot, "cli"),
|
|
},
|
|
} {
|
|
changed, err := provider.SourcesForChangedPath(context.Background(), req)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, changed, "%+v", req)
|
|
}
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{RawSessionID: sessionID})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
assert.Equal(t, VirtualSourcePath(dbPath, sessionID), source.DisplayPath)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: source})
|
|
assert.Empty(t, outcome.Results)
|
|
assert.Empty(t, outcome.SourceErrors)
|
|
require.Error(t, err)
|
|
assert.ErrorContains(t, err, "invalid devin transcript")
|
|
assertDevinErrorRedacted(t, err,
|
|
secretSentinel,
|
|
"mcp/oauth",
|
|
"config",
|
|
"api_key",
|
|
transcriptPath,
|
|
)
|
|
}
|
|
|
|
func TestDevinProviderMissingDBSkipsAndPreservesSessions(t *testing.T) {
|
|
const sessionID = "session-123"
|
|
dbPath, _ := newDevinSessionFixture(t, devinSessionRow{ID: sessionID, Title: "Title", WorkingDirectory: "/tmp/app", Model: "db-model", CreatedAtMillis: new(int64(1704103199000)), LastActivityMillis: new(int64(1704103265000))}, `{"steps":[]}`)
|
|
root := filepath.Dir(filepath.Dir(dbPath))
|
|
virtualPath := VirtualSourcePath(dbPath, sessionID)
|
|
|
|
provider, ok := NewProvider(AgentDevin, ProviderConfig{Roots: []string{root}})
|
|
require.True(t, ok)
|
|
|
|
source, ok, err := provider.FindSource(context.Background(), FindSourceRequest{StoredFilePath: virtualPath})
|
|
require.NoError(t, err)
|
|
require.True(t, ok)
|
|
|
|
require.NoError(t, os.Remove(dbPath))
|
|
changed, err := provider.SourcesForChangedPath(context.Background(), ChangedPathRequest{
|
|
Path: dbPath,
|
|
EventKind: "remove",
|
|
WatchRoot: filepath.Join(root, "cli"),
|
|
StoredSourcePaths: []string{virtualPath},
|
|
})
|
|
require.NoError(t, err)
|
|
require.Len(t, changed, 1)
|
|
|
|
fingerprint, err := provider.Fingerprint(context.Background(), source)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, SourceFingerprint{Key: virtualPath}, fingerprint)
|
|
|
|
outcome, err := provider.Parse(context.Background(), ParseRequest{Source: changed[0]})
|
|
require.NoError(t, err)
|
|
assert.True(t, outcome.ResultSetComplete)
|
|
assert.False(t, outcome.ForceReplace)
|
|
assert.Equal(t, SkipNoSession, outcome.SkipReason)
|
|
assert.Empty(t, outcome.Results)
|
|
}
|