From 28ea397282205e5ed3b31c18a4d9a61ebf362e02 Mon Sep 17 00:00:00 2001 From: Virgil Date: Sat, 4 Apr 2026 08:28:07 +0000 Subject: [PATCH] fix(workspace): normalise orphan recovery paths Handle the documented .core/state/ form the same as the default cache key and add a regression test for cached orphan recovery. Co-Authored-By: Virgil --- workspace.go | 14 +++++++++++--- workspace_test.go | 23 +++++++++++++++++++++++ 2 files changed, 34 insertions(+), 3 deletions(-) diff --git a/workspace.go b/workspace.go index 1745c34..1f0dfcc 100644 --- a/workspace.go +++ b/workspace.go @@ -194,14 +194,21 @@ func discoverOrphanWorkspaces(stateDirectory string, backingStore *Store) []*Wor return orphanWorkspaces } +func normaliseWorkspaceStateDirectory(stateDirectory string) string { + for stateDirectory != "" && core.HasSuffix(stateDirectory, "/") { + stateDirectory = core.TrimSuffix(stateDirectory, "/") + } + return stateDirectory +} + func workspaceNameFromPath(stateDirectory, databasePath string) string { relativePath := core.TrimPrefix(databasePath, joinPath(stateDirectory, "")) return core.TrimSuffix(relativePath, ".duckdb") } -// RecoverOrphans(".core/state") returns orphaned workspaces such as +// RecoverOrphans(".core/state/") returns orphaned workspaces such as // `scroll-session.duckdb` so callers can inspect Aggregate() and then Discard(). -// Usage example: `orphans := storeInstance.RecoverOrphans(".core/state")` +// Usage example: `orphans := storeInstance.RecoverOrphans(".core/state/")` func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace { if storeInstance == nil { return nil @@ -210,8 +217,9 @@ func (storeInstance *Store) RecoverOrphans(stateDirectory string) []*Workspace { if stateDirectory == "" { stateDirectory = defaultWorkspaceStateDirectory } + stateDirectory = normaliseWorkspaceStateDirectory(stateDirectory) - if stateDirectory == defaultWorkspaceStateDirectory { + if stateDirectory == normaliseWorkspaceStateDirectory(defaultWorkspaceStateDirectory) { storeInstance.orphanWorkspacesLock.Lock() cachedWorkspaces := storeInstance.orphanWorkspaces storeInstance.orphanWorkspaces = nil diff --git a/workspace_test.go b/workspace_test.go index 56f0b2f..3bfa8ef 100644 --- a/workspace_test.go +++ b/workspace_test.go @@ -273,6 +273,29 @@ func TestWorkspace_New_Good_CachesOrphansDuringConstruction(t *testing.T) { orphans[0].Discard() } +func TestWorkspace_RecoverOrphans_Good_TrailingSlashUsesCache(t *testing.T) { + stateDirectory := useWorkspaceStateDirectory(t) + requireCoreOK(t, testFilesystem().EnsureDir(stateDirectory)) + + orphanDatabasePath := workspaceFilePath(stateDirectory, "orphan-session") + orphanDatabase, err := openWorkspaceDatabase(orphanDatabasePath) + require.NoError(t, err) + require.NoError(t, orphanDatabase.Close()) + assert.True(t, testFilesystem().Exists(orphanDatabasePath)) + + storeInstance, err := New(":memory:") + require.NoError(t, err) + defer storeInstance.Close() + + requireCoreOK(t, testFilesystem().DeleteAll(stateDirectory)) + assert.False(t, testFilesystem().Exists(orphanDatabasePath)) + + orphans := storeInstance.RecoverOrphans(stateDirectory + "/") + require.Len(t, orphans, 1) + assert.Equal(t, "orphan-session", orphans[0].Name()) + orphans[0].Discard() +} + func TestWorkspace_Close_Good_PreservesOrphansForRecovery(t *testing.T) { stateDirectory := useWorkspaceStateDirectory(t) requireCoreOK(t, testFilesystem().EnsureDir(stateDirectory)) -- 2.45.3