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
1108 lines
36 KiB
Go
1108 lines
36 KiB
Go
package db
|
|
|
|
import (
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"runtime"
|
|
"strings"
|
|
"sync"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
|
|
"go.kenn.io/agentsview/internal/export"
|
|
)
|
|
|
|
func TestProjectObservationDatabaseIDIsCreatedAndStable(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
first, err := d.GetDatabaseID(ctx)
|
|
require.NoError(t, err)
|
|
assert.Regexp(t, regexp.MustCompile(
|
|
`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`,
|
|
), first)
|
|
|
|
second, err := d.GetOrCreateDatabaseID(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, first, second)
|
|
|
|
require.NoError(t, d.SetDatabaseIDForTest(ctx, "test-database-id"))
|
|
overridden, err := d.GetOrCreateDatabaseID(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "test-database-id", overridden)
|
|
}
|
|
|
|
func TestProjectObservationArchiveIDIsCreatedAndStable(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
first, err := d.GetArchiveID(ctx)
|
|
require.NoError(t, err)
|
|
assert.Regexp(t, regexp.MustCompile(
|
|
`^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$`,
|
|
), first)
|
|
|
|
second, err := d.GetOrCreateArchiveID(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, first, second)
|
|
}
|
|
|
|
func TestProjectIdentityPublicationRevisionTracksSnapshotChanges(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
before, err := d.ProjectIdentityPublicationRevision(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "revision-session", Project: "app", Machine: "local", Agent: "codex",
|
|
}))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
SessionID: "revision-session", Project: "app", Machine: "local",
|
|
RootPath: "/tmp/app", ObservedAt: time.Now().UTC(),
|
|
}))
|
|
afterInsert, err := d.ProjectIdentityPublicationRevision(ctx)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, afterInsert, before)
|
|
|
|
require.NoError(t, d.DeleteSession("revision-session"))
|
|
afterDelete, err := d.ProjectIdentityPublicationRevision(ctx)
|
|
require.NoError(t, err)
|
|
assert.Greater(t, afterDelete, afterInsert)
|
|
}
|
|
|
|
func TestSessionProjectIdentitySnapshotPreservesFirstRootKeyUntilRemoteEvidence(
|
|
t *testing.T,
|
|
) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "root-snapshot", Project: "app", Machine: "local", Agent: "codex",
|
|
}))
|
|
firstObservedAt := time.Date(2026, 7, 1, 12, 0, 0, 0, time.UTC)
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
SessionID: "root-snapshot", Project: "app", Machine: "local",
|
|
RootPath: "/first/app", ObservedAt: firstObservedAt,
|
|
}))
|
|
first, err := d.listSessionProjectIdentitySnapshots(
|
|
ctx, []string{"root-snapshot"},
|
|
)
|
|
require.NoError(t, err)
|
|
require.Contains(t, first, "root-snapshot")
|
|
require.NotEmpty(t, first["root-snapshot"].Key)
|
|
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
SessionID: "root-snapshot", Project: "app", Machine: "local",
|
|
RootPath: "/later/app", ObservedAt: firstObservedAt.Add(time.Hour),
|
|
}))
|
|
afterRoot, err := d.listSessionProjectIdentitySnapshots(
|
|
ctx, []string{"root-snapshot"},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "/first/app", afterRoot["root-snapshot"].RootPath)
|
|
assert.Equal(t, first["root-snapshot"].Key,
|
|
afterRoot["root-snapshot"].Key)
|
|
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
SessionID: "root-snapshot", Project: "app", Machine: "local",
|
|
RootPath: "/later/app", GitRemote: "https://example.com/example/app.git",
|
|
ObservedAt: firstObservedAt.Add(2 * time.Hour),
|
|
}))
|
|
afterRemote, err := d.listSessionProjectIdentitySnapshots(
|
|
ctx, []string{"root-snapshot"},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, export.ProjectResolutionResolved,
|
|
afterRemote["root-snapshot"].RemoteResolution)
|
|
assert.Equal(t, "https://example.com/example/app.git",
|
|
afterRemote["root-snapshot"].GitRemote)
|
|
assert.NotEqual(t, first["root-snapshot"].Key,
|
|
afterRemote["root-snapshot"].Key)
|
|
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "ambiguous-snapshot", Project: "app", Machine: "local", Agent: "codex",
|
|
}))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
SessionID: "ambiguous-snapshot", Project: "app", Machine: "local",
|
|
RootPath: "/first/ambiguous", ObservedAt: firstObservedAt,
|
|
}))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
SessionID: "ambiguous-snapshot", Project: "app", Machine: "local",
|
|
RootPath: "/later/ambiguous", RemoteCandidateCount: 2,
|
|
RemoteResolution: export.ProjectResolutionAmbiguous,
|
|
ObservedAt: firstObservedAt.Add(time.Hour),
|
|
}))
|
|
afterAmbiguous, err := d.listSessionProjectIdentitySnapshots(
|
|
ctx, []string{"ambiguous-snapshot"},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, export.ProjectResolutionAmbiguous,
|
|
afterAmbiguous["ambiguous-snapshot"].RemoteResolution)
|
|
assert.Equal(t, 2,
|
|
afterAmbiguous["ambiguous-snapshot"].RemoteCandidateCount)
|
|
}
|
|
|
|
func TestLoadProjectIdentityPublicationDeltaReturnsRowsAndTombstones(
|
|
t *testing.T,
|
|
) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
observedAt := time.Date(2026, 7, 11, 12, 0, 0, 0, time.UTC)
|
|
for _, project := range []string{"alpha", "beta"} {
|
|
sessionID := "identity-" + project
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: sessionID, Project: project, Machine: "local", Agent: "codex",
|
|
}))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
SessionID: sessionID, Project: project, Machine: "local",
|
|
RootPath: "/workspace/" + project,
|
|
GitRemote: "https://example.com/" + project + ".git",
|
|
GitRemoteName: "origin",
|
|
RemoteResolution: export.ProjectResolutionResolved,
|
|
ObservedAt: observedAt,
|
|
},
|
|
))
|
|
}
|
|
baseline, err := d.ProjectIdentityPublicationRevision(ctx)
|
|
require.NoError(t, err)
|
|
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
SessionID: "identity-alpha", Project: "alpha", Machine: "local",
|
|
RootPath: "/workspace/alpha",
|
|
GitRemote: "https://example.com/alpha.git",
|
|
GitRemoteName: "upstream",
|
|
RemoteResolution: export.ProjectResolutionResolved,
|
|
ObservedAt: observedAt.Add(time.Hour),
|
|
},
|
|
))
|
|
_, err = d.rawWriter().ExecContext(ctx, `
|
|
DELETE FROM project_identity_observations
|
|
WHERE project = ? AND machine = ? AND root_path = ? AND git_remote = ?`,
|
|
"beta", "local", "/workspace/beta", "https://example.com/beta.git",
|
|
)
|
|
require.NoError(t, err)
|
|
require.NoError(t, d.DeleteSession("identity-alpha"))
|
|
through, err := d.ProjectIdentityPublicationRevision(ctx)
|
|
require.NoError(t, err)
|
|
|
|
delta, err := d.LoadProjectIdentityPublicationDelta(
|
|
ctx, baseline, through, nil, nil,
|
|
)
|
|
require.NoError(t, err)
|
|
require.Len(t, delta.Observations, 1)
|
|
assert.Equal(t, "alpha", delta.Observations[0].Project)
|
|
assert.Equal(t, "upstream", delta.Observations[0].GitRemoteName)
|
|
assert.Equal(t, []ProjectIdentityObservationKey{{
|
|
Project: "beta", Machine: "local", RootPath: "/workspace/beta",
|
|
GitRemote: "https://example.com/beta.git",
|
|
}}, delta.ObservationDeletes)
|
|
assert.Empty(t, delta.Snapshots)
|
|
assert.Equal(t, []SessionProjectIdentitySnapshotKey{{
|
|
SessionID: "identity-alpha", Project: "alpha",
|
|
}}, delta.SnapshotDeletes)
|
|
|
|
betaDelta, err := d.LoadProjectIdentityPublicationDelta(
|
|
ctx, baseline, through, []string{"beta"}, nil,
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Empty(t, betaDelta.Observations)
|
|
assert.Equal(t, delta.ObservationDeletes, betaDelta.ObservationDeletes)
|
|
assert.Empty(t, betaDelta.Snapshots)
|
|
assert.Empty(t, betaDelta.SnapshotDeletes)
|
|
}
|
|
|
|
func TestCopyArchiveIdentityFromPreservesLogicalArchiveAndNewGeneration(
|
|
t *testing.T,
|
|
) {
|
|
ctx := context.Background()
|
|
dir := t.TempDir()
|
|
sourcePath := filepath.Join(dir, "source.db")
|
|
source, err := Open(sourcePath)
|
|
require.NoError(t, err)
|
|
require.NoError(t, source.SetDatabaseIDForTest(ctx, "source-generation"))
|
|
require.NoError(t, source.SetArchiveIdentityForTest(
|
|
ctx, "stable-archive", strings.Repeat("a", 64),
|
|
))
|
|
require.NoError(t, source.Close())
|
|
|
|
target := testDB(t)
|
|
require.NoError(t, target.SetDatabaseIDForTest(ctx, "new-generation"))
|
|
require.NoError(t, target.SetArchiveIdentityForTest(
|
|
ctx, "temporary-archive", strings.Repeat("b", 64),
|
|
))
|
|
require.NoError(t, target.CopyArchiveIdentityFrom(sourcePath))
|
|
|
|
archiveID, err := target.GetArchiveID(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "stable-archive", archiveID)
|
|
archiveSalt, err := target.GetArchiveSalt(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, strings.Repeat("a", 64), archiveSalt)
|
|
databaseID, err := target.GetDatabaseID(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "new-generation", databaseID)
|
|
}
|
|
|
|
func TestProjectObservationArchiveSaltIsCreatedAndStable(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
first, err := d.GetArchiveSalt(ctx)
|
|
require.NoError(t, err)
|
|
assert.Regexp(t, regexp.MustCompile(`^[0-9a-f]{64}$`), first)
|
|
|
|
second, err := d.GetOrCreateArchiveSalt(ctx)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, first, second)
|
|
}
|
|
|
|
func TestProjectObservationArchiveSaltRejectsCorruptPersistedValue(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, d.SetArchiveIdentityForTest(ctx, "archive", "not-a-salt"))
|
|
|
|
_, err := d.GetArchiveSalt(ctx)
|
|
require.ErrorIs(t, err, ErrArchiveSaltInvalid)
|
|
_, err = d.GetOrCreateArchiveSalt(ctx)
|
|
require.ErrorIs(t, err, ErrArchiveSaltInvalid,
|
|
"a corrupt established salt must not be silently replaced")
|
|
}
|
|
|
|
func TestCopyArchiveIdentityFromRejectsCorruptSalt(t *testing.T) {
|
|
ctx := context.Background()
|
|
sourcePath := filepath.Join(t.TempDir(), "source.db")
|
|
source, err := Open(sourcePath)
|
|
require.NoError(t, err)
|
|
require.NoError(t, source.SetArchiveIdentityForTest(
|
|
ctx, "archive", "truncated",
|
|
))
|
|
require.NoError(t, source.Close())
|
|
|
|
target := testDB(t)
|
|
err = target.CopyArchiveIdentityFrom(sourcePath)
|
|
require.ErrorIs(t, err, ErrArchiveSaltInvalid)
|
|
}
|
|
|
|
func TestProjectIdentityObservationRoundTripsRepositoryContext(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
want := export.ProjectIdentityObservation{
|
|
SourceArchiveID: "archive-source",
|
|
SourceArchiveSalt: "archive-salt",
|
|
Project: "app",
|
|
Machine: "host",
|
|
RootPath: "/private/app/worktree",
|
|
RepositoryPath: "/private/app/.git",
|
|
WorktreeRootPath: "/private/app/worktree",
|
|
WorktreeRelationship: export.WorktreeLinked,
|
|
CheckoutState: export.CheckoutDetached,
|
|
GitBranch: "",
|
|
RemoteResolution: export.ProjectResolutionAmbiguous,
|
|
RemoteCandidateCount: 2,
|
|
ObservedAt: time.Date(2026, 7, 10, 12, 0, 0, 0, time.UTC),
|
|
}
|
|
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx, want))
|
|
got, err := d.ListProjectIdentityObservations(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, want.RepositoryPath, got[0].RepositoryPath)
|
|
assert.Equal(t, want.SourceArchiveID, got[0].SourceArchiveID)
|
|
assert.Equal(t, want.SourceArchiveSalt, got[0].SourceArchiveSalt)
|
|
assert.Equal(t, want.WorktreeRelationship, got[0].WorktreeRelationship)
|
|
assert.Equal(t, want.CheckoutState, got[0].CheckoutState)
|
|
assert.Equal(t, want.RemoteResolution, got[0].RemoteResolution)
|
|
assert.Equal(t, want.RemoteCandidateCount, got[0].RemoteCandidateCount)
|
|
}
|
|
|
|
func TestProjectObservationDatabaseIDInitializedForReadOnlyOpen(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "test.db")
|
|
writable := testDBAtPath(t, path, "read-only database id seed")
|
|
seeded, err := writable.GetDatabaseID(context.Background())
|
|
require.NoError(t, err)
|
|
require.NotEmpty(t, seeded)
|
|
require.NoError(t, writable.Close())
|
|
|
|
readonly, err := OpenReadOnly(path)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, readonly.Close()) })
|
|
|
|
got, err := readonly.GetDatabaseID(context.Background())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, seeded, got)
|
|
}
|
|
|
|
func TestProjectObservationDatabaseIDReadOnlyExisting(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "test.db")
|
|
writable := testDBAtPath(t, path, "read-only database id seed")
|
|
require.NoError(t, writable.SetDatabaseIDForTest(
|
|
context.Background(), "read-only-db-id"))
|
|
require.NoError(t, writable.Close())
|
|
|
|
readonly, err := OpenReadOnly(path)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, readonly.Close()) })
|
|
|
|
got, err := readonly.GetDatabaseID(context.Background())
|
|
require.NoError(t, err)
|
|
assert.Equal(t, "read-only-db-id", got)
|
|
}
|
|
|
|
func TestProjectObservationDatabaseIDMissingDoesNotCreate(t *testing.T) {
|
|
d := testDB(t)
|
|
_, err := d.rawWriter().Exec(`
|
|
DELETE FROM archive_metadata WHERE key = ?`,
|
|
archiveMetadataDatabaseIDKey,
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
_, err = d.GetDatabaseID(context.Background())
|
|
require.ErrorIs(t, err, ErrDatabaseIDMissing)
|
|
|
|
var count int
|
|
require.NoError(t, d.rawReader().QueryRow(`
|
|
SELECT COUNT(*) FROM archive_metadata
|
|
WHERE key = ?`, archiveMetadataDatabaseIDKey).Scan(&count))
|
|
assert.Zero(t, count)
|
|
}
|
|
|
|
func TestProjectObservationRawValuesAreAuthoritativeForExportKeys(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
obs := export.ProjectIdentityObservation{
|
|
Project: "app",
|
|
Machine: "laptop",
|
|
RootPath: "/tmp/app",
|
|
GitRemote: "git@github.com:Org/Repo.git",
|
|
GitRemoteName: "origin",
|
|
ObservedAt: time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC),
|
|
NormalizedRemote: "stale/normalized",
|
|
KeySource: "root_path",
|
|
Key: "stale-key",
|
|
}
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx, obs))
|
|
|
|
got, err := d.ListProjectIdentityObservations(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "github.com:Org/Repo.git", got[0].GitRemote)
|
|
assert.Equal(t, "/tmp/app", got[0].RootPath)
|
|
|
|
projects := export.BuildProjectsMap([]string{"app"}, got)
|
|
require.Equal(t, export.ProjectResolutionResolved, projects["app"].Resolution)
|
|
require.NotNil(t, projects["app"].Identity)
|
|
assert.Equal(t, "github.com/Org/Repo", projects["app"].Identity.NormalizedRemote)
|
|
assert.Equal(t, export.ProjectKindGitRemote, projects["app"].Identity.Kind)
|
|
assert.NotEmpty(t, projects["app"].Identity.Key)
|
|
}
|
|
|
|
func TestProjectObservationStripsGitRemoteCredentialsBeforeStorage(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app",
|
|
Machine: "laptop",
|
|
RootPath: "/tmp/app",
|
|
GitRemote: "https://" + "user:token@" + "example.com/acme/app.git",
|
|
GitRemoteName: "origin",
|
|
},
|
|
))
|
|
|
|
got, err := d.ListProjectIdentityObservations(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "https://example.com/acme/app.git", got[0].GitRemote)
|
|
assert.Equal(t, "example.com/acme/app", got[0].NormalizedRemote)
|
|
assert.Equal(t, export.ProjectIdentityKeySourceGitRemote, got[0].KeySource)
|
|
}
|
|
|
|
func TestProjectObservationMigrationStripsStoredGitRemoteCredentials(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "test.db")
|
|
d := testDBAtPath(t, path, "credential migration seed")
|
|
_, err := d.rawWriter().Exec(`
|
|
INSERT INTO project_identity_observations (
|
|
project, machine, root_path, git_remote, git_remote_name,
|
|
worktree_name, worktree_root_path, observed_at,
|
|
normalized_remote, key_source, key
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
"app", "laptop", "/tmp/app",
|
|
"https://"+"user:token@"+"example.com/acme/app.git", "origin",
|
|
"", "", "2026-07-03T12:00:00Z",
|
|
"example.com/acme/app", export.ProjectIdentityKeySourceGitRemote,
|
|
projectIdentitySHA("git_remote\n"+"example.com/acme/app"),
|
|
)
|
|
require.NoError(t, err)
|
|
_, err = d.rawWriter().Exec(`DELETE FROM stats WHERE key = ?`,
|
|
projectIdentityRemoteScrubCompletedKey)
|
|
require.NoError(t, err)
|
|
require.NoError(t, d.Close())
|
|
|
|
reopened, err := Open(path)
|
|
require.NoError(t, err)
|
|
t.Cleanup(func() { require.NoError(t, reopened.Close()) })
|
|
|
|
got, err := reopened.ListProjectIdentityObservations(
|
|
context.Background(), []string{"app"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "https://example.com/acme/app.git", got[0].GitRemote)
|
|
assert.Equal(t, "example.com/acme/app", got[0].NormalizedRemote)
|
|
var scrubbed string
|
|
require.NoError(t, reopened.getReader().QueryRowContext(
|
|
context.Background(), `SELECT value FROM stats WHERE key = ?`,
|
|
projectIdentityRemoteScrubCompletedKey,
|
|
).Scan(&scrubbed))
|
|
assert.Equal(t, "1", scrubbed)
|
|
}
|
|
|
|
func TestProjectObservationListFiltersLabelsAndKeepsPersistedRemoteMachineRows(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "old-project",
|
|
Machine: "remote-host",
|
|
RootPath: "remote-host:/srv/app",
|
|
GitRemote: "https://github.com/acme/old-project.git",
|
|
},
|
|
))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "other",
|
|
Machine: "remote-host",
|
|
RootPath: "s3://bucket/archive",
|
|
GitRemote: "https://github.com/acme/other.git",
|
|
},
|
|
))
|
|
|
|
got, err := d.ListProjectIdentityObservations(ctx, []string{"old-project"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "remote-host", got[0].Machine)
|
|
assert.Equal(t, "remote-host:/srv/app", got[0].RootPath)
|
|
assert.Equal(t, "https://github.com/acme/old-project.git", got[0].GitRemote)
|
|
}
|
|
|
|
func TestProjectIdentityNilLabelsListAllButEmptyLabelsMapEmpty(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
seedProjectIdentityObservation(t, d, "observed-project")
|
|
|
|
all, err := d.ListProjectIdentityObservations(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.Len(t, all, 1)
|
|
assert.Equal(t, "observed-project", all[0].Project)
|
|
|
|
empty, err := d.BuildProjectIdentityMap(ctx, []string{})
|
|
require.NoError(t, err)
|
|
assert.Empty(t, empty)
|
|
}
|
|
|
|
func TestProjectIdentityGoldenFixtureObservationsAreDeterministic(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
observedAt := time.Date(2026, 7, 3, 12, 0, 0, 0, time.UTC)
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "remote-project",
|
|
Machine: "golden-host",
|
|
RootPath: "/fixtures/remote-project/worktrees/feature",
|
|
GitRemote: "https://github.com/acme/remote-project.git",
|
|
GitRemoteName: "origin",
|
|
WorktreeName: "feature",
|
|
WorktreeRootPath: "/fixtures/remote-project",
|
|
ObservedAt: observedAt,
|
|
},
|
|
))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "path-project",
|
|
Machine: "golden-host",
|
|
RootPath: "/fixtures/path-project",
|
|
RepositoryPath: "/fixtures/path-project/.git",
|
|
WorktreeRelationship: export.WorktreeMain,
|
|
ObservedAt: observedAt,
|
|
},
|
|
))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx,
|
|
[]string{"remote-project", "path-project", "unknown-project"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, export.ProjectResolutionResolved,
|
|
got["remote-project"].Resolution)
|
|
require.NotNil(t, got["remote-project"].Identity)
|
|
assert.Equal(t, export.ProjectKindGitRemote, got["remote-project"].Identity.Kind)
|
|
assert.Equal(t, "github.com/acme/remote-project",
|
|
got["remote-project"].Identity.NormalizedRemote)
|
|
assert.NotEmpty(t, got["remote-project"].Identity.Key)
|
|
|
|
require.Equal(t, export.ProjectResolutionResolved,
|
|
got["path-project"].Resolution)
|
|
require.NotNil(t, got["path-project"].Identity)
|
|
assert.Equal(t, export.ProjectKindMachineRoot, got["path-project"].Identity.Kind)
|
|
assert.Empty(t, got["path-project"].Identity.NormalizedRemote)
|
|
encoded, err := json.Marshal(got["path-project"])
|
|
require.NoError(t, err)
|
|
assert.NotContains(t, string(encoded), "/fixtures/path-project")
|
|
|
|
assert.Equal(t, export.ProjectResolutionUnknown,
|
|
got["unknown-project"].Resolution)
|
|
assert.Nil(t, got["unknown-project"].Identity)
|
|
}
|
|
|
|
func TestProjectObservationSessionBatchWritePersistsObservation(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
result, err := d.WriteSessionBatch([]SessionBatchWrite{{
|
|
Session: Session{
|
|
ID: "batch-observation",
|
|
Project: "mapped-project",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
},
|
|
IdentityObservation: export.ProjectIdentityObservation{
|
|
Project: "mapped-project",
|
|
Machine: "laptop",
|
|
RootPath: "/tmp/worktree",
|
|
WorktreeName: "feature",
|
|
WorktreeRootPath: "/tmp/worktree",
|
|
},
|
|
DataVersion: CurrentDataVersion(),
|
|
ReplaceMessages: true,
|
|
}})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, 1, result.WrittenSessions)
|
|
|
|
got, err := d.ListProjectIdentityObservations(ctx, []string{"mapped-project"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "/tmp/worktree", got[0].RootPath)
|
|
assert.Empty(t, got[0].GitRemote)
|
|
|
|
projects := export.BuildProjectsMap([]string{"mapped-project"}, got)
|
|
require.Equal(t, export.ProjectResolutionUnknown, projects["mapped-project"].Resolution)
|
|
assert.Nil(t, projects["mapped-project"].Identity)
|
|
}
|
|
|
|
func seedProjectIdentityObservation(t *testing.T, d *DB, project string) {
|
|
t.Helper()
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(context.Background(),
|
|
export.ProjectIdentityObservation{
|
|
Project: project,
|
|
Machine: "test-machine",
|
|
RootPath: "/tmp/" + project,
|
|
GitRemote: "https://github.com/acme/" + project + ".git",
|
|
},
|
|
))
|
|
}
|
|
|
|
func TestProjectObservationRemoteReplacesSameRootFallback(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
root := filepath.Join(t.TempDir(), "repo")
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app",
|
|
Machine: "laptop",
|
|
RootPath: root,
|
|
},
|
|
))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app",
|
|
Machine: "laptop",
|
|
RootPath: root,
|
|
GitRemote: "https://github.com/acme/app.git",
|
|
GitRemoteName: "origin",
|
|
},
|
|
))
|
|
|
|
got, err := d.ListProjectIdentityObservations(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "https://github.com/acme/app.git", got[0].GitRemote)
|
|
|
|
projects := export.BuildProjectsMap([]string{"app"}, got)
|
|
require.Equal(t, export.ProjectResolutionResolved, projects["app"].Resolution)
|
|
require.NotNil(t, projects["app"].Identity)
|
|
assert.Equal(t, "github.com/acme/app", projects["app"].Identity.NormalizedRemote)
|
|
}
|
|
|
|
func TestProjectObservationFallbackDoesNotRecreateSameRootWhenRemoteExists(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
root := filepath.Join(t.TempDir(), "repo")
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app",
|
|
Machine: "laptop",
|
|
RootPath: root,
|
|
GitRemote: "https://github.com/acme/app.git",
|
|
GitRemoteName: "origin",
|
|
},
|
|
))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app",
|
|
Machine: "laptop",
|
|
RootPath: root,
|
|
},
|
|
))
|
|
|
|
got, err := d.ListProjectIdentityObservations(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Equal(t, "https://github.com/acme/app.git", got[0].GitRemote)
|
|
}
|
|
|
|
func TestProjectObservationAmbiguousSupersedesResolvedAggregate(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
root := filepath.Join(t.TempDir(), "repo")
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app", Machine: "laptop", RootPath: root,
|
|
GitRemote: "https://github.com/acme/app.git",
|
|
},
|
|
))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app", Machine: "laptop", RootPath: root,
|
|
RemoteResolution: export.ProjectResolutionAmbiguous,
|
|
RemoteCandidateCount: 2,
|
|
},
|
|
))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, export.ProjectResolutionAmbiguous, got["app"].Resolution)
|
|
assert.Nil(t, got["app"].Identity)
|
|
}
|
|
|
|
func TestProjectObservationResolvedDoesNotDiscardAmbiguousEvidence(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
root := filepath.Join(t.TempDir(), "repo")
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app", Machine: "laptop", RootPath: root,
|
|
RemoteResolution: export.ProjectResolutionAmbiguous,
|
|
RemoteCandidateCount: 2,
|
|
},
|
|
))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app", Machine: "laptop", RootPath: root,
|
|
GitRemote: "https://github.com/acme/app.git",
|
|
},
|
|
))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, export.ProjectResolutionAmbiguous, got["app"].Resolution)
|
|
assert.Nil(t, got["app"].Identity)
|
|
}
|
|
|
|
func TestProjectObservationUnknownDoesNotReplaceAmbiguousEvidence(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
root := filepath.Join(t.TempDir(), "repo")
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app", Machine: "laptop", RootPath: root,
|
|
RemoteResolution: export.ProjectResolutionAmbiguous,
|
|
RemoteCandidateCount: 2,
|
|
},
|
|
))
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app", Machine: "laptop", RootPath: root,
|
|
RemoteResolution: export.ProjectResolutionUnknown,
|
|
},
|
|
))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, export.ProjectResolutionAmbiguous, got["app"].Resolution)
|
|
assert.Nil(t, got["app"].Identity)
|
|
}
|
|
|
|
func TestProjectObservationScrubDowngradesUnusableRemoteToFallback(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
root := filepath.Join(t.TempDir(), "repo")
|
|
_, err := d.getWriter().ExecContext(ctx, `
|
|
INSERT INTO project_identity_observations (
|
|
project, machine, root_path, git_remote, git_remote_name,
|
|
worktree_name, worktree_root_path, observed_at,
|
|
normalized_remote, key_source, key
|
|
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
|
"app", "laptop", root, "file:///tmp/app.git", "origin",
|
|
"", "", "2026-07-03T12:00:00Z", "", "", "",
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
tx, err := d.getWriter().BeginTx(ctx, nil)
|
|
require.NoError(t, err)
|
|
require.NoError(t, scrubProjectIdentityGitRemoteCredentialsTx(ctx, tx))
|
|
require.NoError(t, tx.Commit())
|
|
|
|
got, err := d.ListProjectIdentityObservations(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
require.Len(t, got, 1)
|
|
assert.Empty(t, got[0].GitRemote)
|
|
assert.Equal(t, export.ProjectIdentityKeySourceRootPath, got[0].KeySource)
|
|
assert.NotEmpty(t, got[0].Key)
|
|
}
|
|
|
|
func TestProjectObservationExportReadsDoNotMutateArchive(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "app",
|
|
Machine: "laptop",
|
|
RootPath: "/tmp/app",
|
|
GitRemote: "https://github.com/acme/app.git",
|
|
},
|
|
))
|
|
|
|
before := projectObservationRowCount(t, d)
|
|
_, err := d.ListProjectIdentityObservations(ctx, []string{"app"})
|
|
require.NoError(t, err)
|
|
_, err = d.ListProjectIdentityObservations(ctx, []string{"missing"})
|
|
require.NoError(t, err)
|
|
assert.Equal(t, before, projectObservationRowCount(t, d))
|
|
}
|
|
|
|
func TestProjectObservationDatabaseIDConcurrentCreateReturnsSingleID(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
const workers = 64
|
|
var wg sync.WaitGroup
|
|
start := make(chan struct{})
|
|
ids := make(chan string, workers)
|
|
errs := make(chan error, workers)
|
|
|
|
for range workers {
|
|
wg.Go(func() {
|
|
<-start
|
|
id, err := d.GetOrCreateDatabaseID(ctx)
|
|
if err != nil {
|
|
errs <- err
|
|
return
|
|
}
|
|
ids <- id
|
|
})
|
|
}
|
|
close(start)
|
|
wg.Wait()
|
|
close(ids)
|
|
close(errs)
|
|
require.Empty(t, errs)
|
|
|
|
seen := map[string]bool{}
|
|
for id := range ids {
|
|
seen[id] = true
|
|
}
|
|
require.Len(t, seen, 1)
|
|
}
|
|
|
|
func TestProjectIdentityMapUsesPersistedFactsWithoutFilesystemInference(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("test uses POSIX-style temp git paths")
|
|
}
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
persistedRoot := filepath.Join(t.TempDir(), "persisted")
|
|
liveRoot := filepath.Join(t.TempDir(), "live")
|
|
mappedRoot := filepath.Join(t.TempDir(), "mapped")
|
|
fileParentRoot := filepath.Join(t.TempDir(), "file-parent")
|
|
require.NoError(t, os.MkdirAll(filepath.Join(persistedRoot, ".git"), 0o755))
|
|
require.NoError(t, os.MkdirAll(filepath.Join(liveRoot, ".git"), 0o755))
|
|
require.NoError(t, os.MkdirAll(mappedRoot, 0o755))
|
|
require.NoError(t, os.MkdirAll(filepath.Join(fileParentRoot, ".git"), 0o755))
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(persistedRoot, ".git", "config"),
|
|
[]byte("[remote \"origin\"]\n\turl = https://github.com/live/wrong.git\n"),
|
|
0o644,
|
|
))
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(liveRoot, ".git", "config"),
|
|
[]byte("[remote \"origin\"]\n\turl = https://github.com/acme/live.git\n"),
|
|
0o644,
|
|
))
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(fileParentRoot, ".git", "config"),
|
|
[]byte("[remote \"origin\"]\n\turl = https://github.com/acme/file-parent.git\n"),
|
|
0o644,
|
|
))
|
|
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "persisted",
|
|
Machine: "laptop",
|
|
RootPath: persistedRoot,
|
|
GitRemote: "git@github.com:acme/persisted.git",
|
|
},
|
|
))
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "persisted-session",
|
|
Project: "persisted",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
Cwd: persistedRoot,
|
|
}))
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "live-session",
|
|
Project: "live",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
Cwd: liveRoot,
|
|
}))
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "mapped-session",
|
|
Project: "mapped",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
Cwd: filepath.Join(mappedRoot, "feature"),
|
|
}))
|
|
fileParentPath := filepath.Join(fileParentRoot, "session.jsonl")
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "file-parent-session",
|
|
Project: "file-parent",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
FilePath: &fileParentPath,
|
|
}))
|
|
_, err := d.CreateWorktreeProjectMapping(ctx, WorktreeProjectMapping{
|
|
Machine: "laptop",
|
|
PathPrefix: mappedRoot,
|
|
Project: "mapped",
|
|
Enabled: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "remote-session",
|
|
Project: "remote",
|
|
Machine: "remote-host",
|
|
Agent: "codex",
|
|
Cwd: "remote-host:/srv/app",
|
|
}))
|
|
|
|
before := projectObservationRowCount(t, d)
|
|
got, err := d.BuildProjectIdentityMap(ctx,
|
|
[]string{"persisted", "live", "mapped", "file-parent", "remote", "unknown"},
|
|
)
|
|
require.NoError(t, err)
|
|
assert.Equal(t, before, projectObservationRowCount(t, d))
|
|
|
|
require.Equal(t, export.ProjectResolutionResolved, got["persisted"].Resolution)
|
|
require.NotNil(t, got["persisted"].Identity)
|
|
assert.Equal(t, "github.com/acme/persisted", got["persisted"].Identity.NormalizedRemote)
|
|
|
|
require.Equal(t, export.ProjectResolutionUnknown, got["live"].Resolution)
|
|
assert.Nil(t, got["live"].Identity)
|
|
|
|
require.Equal(t, export.ProjectResolutionUnknown, got["mapped"].Resolution)
|
|
assert.Nil(t, got["mapped"].Identity)
|
|
|
|
require.Equal(t, export.ProjectResolutionUnknown, got["file-parent"].Resolution)
|
|
assert.Nil(t, got["file-parent"].Identity)
|
|
|
|
assert.Equal(t, export.ProjectResolutionUnknown, got["remote"].Resolution)
|
|
assert.Nil(t, got["remote"].Identity)
|
|
assert.Equal(t, export.ProjectResolutionUnknown, got["unknown"].Resolution)
|
|
assert.Nil(t, got["unknown"].Identity)
|
|
}
|
|
|
|
func TestProjectIdentityMapLegacyFallbackAcceptsWindowsDriveRoots(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "windows-backslash-session",
|
|
Project: "windows-backslash",
|
|
Machine: "windows-host",
|
|
Agent: "codex",
|
|
Cwd: `C:\repo\`,
|
|
}))
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "windows-slash-session",
|
|
Project: "windows-slash",
|
|
Machine: "windows-host",
|
|
Agent: "codex",
|
|
Cwd: "C:/repo/",
|
|
}))
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "remote-prefixed-session",
|
|
Project: "remote-prefixed",
|
|
Machine: "remote-host",
|
|
Agent: "codex",
|
|
Cwd: "host:/srv/repo",
|
|
}))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx,
|
|
[]string{"windows-backslash", "windows-slash", "remote-prefixed"},
|
|
)
|
|
require.NoError(t, err)
|
|
|
|
require.Equal(t, export.ProjectResolutionUnknown, got["windows-backslash"].Resolution)
|
|
assert.Nil(t, got["windows-backslash"].Identity)
|
|
|
|
require.Equal(t, export.ProjectResolutionUnknown, got["windows-slash"].Resolution)
|
|
assert.Nil(t, got["windows-slash"].Identity)
|
|
|
|
assert.Equal(t, export.ProjectResolutionUnknown, got["remote-prefixed"].Resolution)
|
|
assert.Nil(t, got["remote-prefixed"].Identity)
|
|
}
|
|
|
|
func TestProjectIdentityMapUnknownPersistedObservationUsesLegacyFallback(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
root := filepath.Join(t.TempDir(), "fallback")
|
|
require.NoError(t, os.MkdirAll(filepath.Join(root, ".git"), 0o755))
|
|
|
|
require.NoError(t, d.UpsertProjectIdentityObservation(ctx,
|
|
export.ProjectIdentityObservation{
|
|
Project: "fallback",
|
|
Machine: "laptop",
|
|
RootPath: "remote-host:/srv/app",
|
|
ObservedAt: time.Date(2026, 5, 1, 0, 0, 0, 0, time.UTC),
|
|
}), "upsert unresolved persisted observation")
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "fallback-session",
|
|
Project: "fallback",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
Cwd: root,
|
|
}))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx, []string{"fallback"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, export.ProjectResolutionUnknown,
|
|
got["fallback"].Resolution)
|
|
assert.Nil(t, got["fallback"].Identity)
|
|
}
|
|
|
|
func TestProjectIdentityMapLegacyFallbackUsesNoRemoteGitRoot(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
repo := t.TempDir()
|
|
require.NoError(t, os.Mkdir(filepath.Join(repo, ".git"), 0o755))
|
|
subdir := filepath.Join(repo, "pkg", "feature")
|
|
require.NoError(t, os.MkdirAll(subdir, 0o755))
|
|
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "git-root-session",
|
|
Project: "git-root",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
Cwd: subdir,
|
|
}))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx, []string{"git-root"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, export.ProjectResolutionUnknown,
|
|
got["git-root"].Resolution)
|
|
assert.Nil(t, got["git-root"].Identity)
|
|
}
|
|
|
|
func TestProjectIdentityMapLegacyFallbackUsesRepoDotWorktreesMapping(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx := context.Background()
|
|
parent := t.TempDir()
|
|
worktreesDir := filepath.Join(parent, "acme-app.worktrees")
|
|
|
|
_, err := d.CreateWorktreeProjectMapping(ctx, WorktreeProjectMapping{
|
|
Machine: "laptop",
|
|
PathPrefix: parent,
|
|
Layout: WorktreeMappingLayoutRepoDotWorktrees,
|
|
Enabled: true,
|
|
})
|
|
require.NoError(t, err)
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "branch-a-session",
|
|
Project: "acme_app",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
Cwd: filepath.Join(worktreesDir, "branch-a"),
|
|
}))
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "branch-b-session",
|
|
Project: "acme_app",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
Cwd: filepath.Join(worktreesDir, "branch-b"),
|
|
}))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx, []string{"acme_app"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, export.ProjectResolutionUnknown, got["acme_app"].Resolution)
|
|
assert.Nil(t, got["acme_app"].Identity)
|
|
}
|
|
|
|
func TestProjectIdentityMapLegacyFallbackClosesRowsBeforeMappingLookup(t *testing.T) {
|
|
d := testDB(t)
|
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second)
|
|
defer cancel()
|
|
reader := d.rawReader()
|
|
oldMaxOpen := reader.Stats().MaxOpenConnections
|
|
reader.SetMaxOpenConns(1)
|
|
t.Cleanup(func() { reader.SetMaxOpenConns(oldMaxOpen) })
|
|
|
|
root := t.TempDir()
|
|
_, err := d.CreateWorktreeProjectMapping(ctx,
|
|
WorktreeProjectMapping{
|
|
Machine: "laptop",
|
|
PathPrefix: root,
|
|
Project: "mapped",
|
|
Enabled: true,
|
|
})
|
|
require.NoError(t, err, "CreateWorktreeProjectMapping")
|
|
require.NoError(t, d.UpsertSession(Session{
|
|
ID: "mapped-session",
|
|
Project: "mapped",
|
|
Machine: "laptop",
|
|
Agent: "codex",
|
|
Cwd: filepath.Join(root, "pkg"),
|
|
}))
|
|
|
|
got, err := d.BuildProjectIdentityMap(ctx, []string{"mapped"})
|
|
require.NoError(t, err)
|
|
require.Equal(t, export.ProjectResolutionUnknown,
|
|
got["mapped"].Resolution)
|
|
assert.Nil(t, got["mapped"].Identity)
|
|
}
|
|
|
|
func projectObservationRowCount(t *testing.T, d *DB) int {
|
|
t.Helper()
|
|
var n int
|
|
require.NoError(t, d.getReader().QueryRow(
|
|
`SELECT COUNT(*) FROM project_identity_observations`,
|
|
).Scan(&n))
|
|
return n
|
|
}
|
|
|
|
func projectIdentitySHA(s string) string {
|
|
sum := sha256.Sum256([]byte(s))
|
|
return "sha256:" + hex.EncodeToString(sum[:])
|
|
}
|