fix(migration): list the projects a recovered session was routed back into

This commit is contained in:
esengine committed 2026-10-16 02:03:14 -07:00
1 parent 8c35e9c8b2
commit e0bbc694d7
6 files changed
+94 -5

No files matched your search

+7
View File
@@ -328,6 +328,13 @@ func (h *Hub) importLegacySessions(w http.ResponseWriter, r *http.Request) {
return
}
result := migration.RunLegacySessionImportInto(strings.TrimSpace(body.Path), SessionDirFor(known), legacyImportLog)
for _, root := range result.Workspaces {
// A folder the import routed sessions back into is a project the person
// had in 1.x; without a place in the list its conversations are unreachable.
if err := addRememberedWorkspace(r.Context(), root); err != nil && !errors.Is(err, errWorkspaceListFull) {
slog.Warn("serve: could not list an imported project", "root", root, "err", err)
}
}
count := 0
for _, imported := range result.SessionImports {
count += imported.Count
@@ -262,3 +262,42 @@ func TestImportLegacyAttributesReadAndWriteFailuresToTheirOwnClass(t *testing.T)
}
}
}
// Sessions that go back into the folder they were made in are only reachable
// if that folder has a place in the list: the tree lists remembered folders and
// open panes, and the importer's fallback workspace is neither.
func TestImportLegacyListsTheProjectsItRoutedSessionsBackInto(t *testing.T) {
home := testenv.TempDir(t)
t.Setenv("REASONIX_HOME", home)
t.Setenv("REASONIX_STATE_HOME", home)
root := testenv.TempDir(t)
project := testenv.TempDir(t)
h := NewHub(HubOptions{})
hubRuntime(t, h, root)
srv := httptest.NewServer(operatorHandler(h))
defer srv.Close()
old := testenv.TempDir(t)
sessions := filepath.Join(old, "sessions")
if err := os.MkdirAll(sessions, 0o755); err != nil {
t.Fatal(err)
}
log := `{"role":"user","content":"hello from the old project"}` + "\n" + `{"role":"assistant","content":"hi"}` + "\n"
if err := os.WriteFile(filepath.Join(sessions, "a.jsonl"), []byte(log), 0o644); err != nil {
t.Fatal(err)
}
meta, _ := json.Marshal(map[string]string{"workspace": project})
if err := os.WriteFile(filepath.Join(sessions, "a.meta.json"), meta, 0o644); err != nil {
t.Fatal(err)
}
if got := importLegacy(t, srv, old, root); got.Imported != 1 {
t.Fatalf("imported %d, want 1", got.Imported)
}
for _, ws := range hubGet[[]treeWorkspace](t, srv, "/tree") {
if filepath.Clean(ws.Root) == filepath.Clean(project) && ws.Remembered && len(ws.Sessions) == 1 {
return
}
}
t.Fatalf("the project %s with its imported session is not in the tree", project)
}
+2 -1
View File
@@ -40,7 +40,7 @@ func desktopSessionOwners(tree fs.FS, home string) map[string]string {
// routeToOwner sends a session to its former workspace's session directory
// while that workspace still exists, and otherwise declines.
func routeToOwner(owners map[string]string) func(string) string {
func routeToOwner(owners map[string]string, note func(root string)) func(string) string {
if len(owners) == 0 {
return nil
}
@@ -52,6 +52,7 @@ func routeToOwner(owners map[string]string) func(string) string {
if info, err := os.Stat(root); err != nil || !info.IsDir() {
return ""
}
note(root)
return config.ProjectSessionDir(root)
}
}
+21 -4
View File
@@ -8,6 +8,7 @@ import (
"os"
"path"
"path/filepath"
"slices"
"strings"
"reasonix/internal/contract/config"
@@ -47,6 +48,9 @@ type Result struct {
SessionImports []SessionImport
SessionErrs []error
SessionSkips []SessionSkip
// Workspaces are the folders an explicit import routed sessions back into:
// each still exists on disk and now holds sessions of its own.
Workspaces []string
// Unrecognised is set when an explicit import found no 1.x session
// store under the chosen folder, as opposed to finding nothing new.
Unrecognised bool
@@ -160,7 +164,13 @@ func RunLegacySessionImportInto(sourceRoot, fallbackDest string, sink event.Sink
result := Result{}
sourceRoot = strings.TrimSpace(sourceRoot)
emit(event.LevelInfo, "migration rescue: scanning explicit legacy sessions from "+sourceRoot)
sources, closeRoot, err := explicitLegacySessionSources(sourceRoot)
var routed []string
note := func(root string) {
if !slices.Contains(routed, root) {
routed = append(routed, root)
}
}
sources, closeRoot, err := explicitLegacySessionSources(sourceRoot, note)
defer closeRoot()
if err != nil {
result.SessionErrs = append(result.SessionErrs, err)
@@ -185,7 +195,13 @@ func RunLegacySessionImportInto(sourceRoot, fallbackDest string, sink event.Sink
}
} else {
var rep *sessionstore.LegacyReport
n, rep, err = sessionstore.ImportLegacySessionsFromExplicitDir(src.dir, fallbackDest, config.ProjectSessionDir)
n, rep, err = sessionstore.ImportLegacySessionsFromExplicitDir(src.dir, fallbackDest, func(root string) string {
dir := config.ProjectSessionDir(root)
if dir != "" {
note(root)
}
return dir
})
skipped = rep.Skipped
}
for _, sk := range skipped {
@@ -206,6 +222,7 @@ func RunLegacySessionImportInto(sourceRoot, fallbackDest string, sink event.Sink
if len(result.SessionImports) == 0 && len(result.SessionErrs) == 0 && len(result.SessionSkips) == 0 {
emit(event.LevelInfo, "migration rescue: no legacy sessions needed migration from "+sourceRoot)
}
result.Workspaces = routed
emit(event.LevelInfo, result.Summary())
return result
}
@@ -568,7 +585,7 @@ func trimMatchingQuotes(s string) string {
// explicitLegacySessionSources finds the session stores under a user-picked
// folder. Every probe goes through one os.Root, so a symlink inside the folder
// cannot lead the scan outside it.
func explicitLegacySessionSources(picked string) ([]explicitSessionSource, func(), error) {
func explicitLegacySessionSources(picked string, note func(root string)) ([]explicitSessionSource, func(), error) {
picked = strings.TrimSpace(picked)
if picked == "" {
return nil, func() {}, fmt.Errorf("--from requires a legacy directory path")
@@ -598,7 +615,7 @@ func explicitLegacySessionSources(picked string) ([]explicitSessionSource, func(
for _, home := range []string{".", ".reasonix", "reasonix"} {
add(path.Join(home, "sessions"), false, nil)
add(path.Join(home, "sessions-v4"), true, nil)
add(path.Join(home, "desktop-sessions-v5", "by-id"), true, routeToOwner(desktopSessionOwners(tree, home)))
add(path.Join(home, "desktop-sessions-v5", "by-id"), true, routeToOwner(desktopSessionOwners(tree, home), note))
projects, _ := fs.ReadDir(tree, path.Join(home, "projects"))
for _, project := range projects {
if !project.IsDir() {
@@ -3,6 +3,7 @@ package migration
import (
"os"
"path/filepath"
"strconv"
"strings"
"testing"
@@ -533,3 +534,25 @@ func TestExplicitImportFindsSessionsAddedAfterAnEarlierRun(t *testing.T) {
t.Fatalf("second run imported %d, want the new session only", got)
}
}
func TestExplicitImportNamesTheFoldersItRoutedSessionsBackInto(t *testing.T) {
home := isolateMigrationHome(t)
root := filepath.Join(home, "Roaming", "reasonix")
project := filepath.Join(home, "work", "old-project")
sessions := filepath.Join(root, "sessions")
for _, dir := range []string{project, sessions} {
if err := os.MkdirAll(dir, 0o755); err != nil {
t.Fatal(err)
}
}
if err := os.WriteFile(filepath.Join(sessions, "a.jsonl"), []byte(legacyMessageLog), 0o644); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(sessions, "a.meta.json"), []byte(`{"workspace":`+strconv.Quote(project)+`}`), 0o644); err != nil {
t.Fatal(err)
}
res := RunLegacySessionImportInto(root, filepath.Join(config.SessionDir(), "ws"), event.Discard)
if len(res.Workspaces) != 1 || res.Workspaces[0] != project {
t.Fatalf("Workspaces = %v, want [%s]", res.Workspaces, project)
}
}
+2
View File
@@ -0,0 +1,2 @@
- 从旧版 Reasonix 找回会话后,原来的项目文件夹会自动出现在侧栏里,找回的会话不再“导入了却看不到”。关联 #13412。
- After recovering sessions from an older Reasonix, the project folders they came from now appear in the sidebar, so recovered sessions are no longer imported but invisible. Fixes #13412.