From c3b613cac8d597853275dc57ddae667c2995e896 Mon Sep 17 00:00:00 2001 From: MohammadAlMasalmeh Date: Wed, 21 Oct 2026 00:26:53 -0500 Subject: [PATCH] fix(core): resolve relative directories during V1 migration (#57618) Co-authored-by: Aiden Cline --- packages/core/src/database/path.ts | 11 +++-- .../core/src/database/v1-migration.bun.ts | 28 ++++++++++--- packages/core/test/v1-migration.test.ts | 40 ++++++++++++++++++- 3 files changed, 67 insertions(+), 12 deletions(-) diff --git a/packages/core/src/database/path.ts b/packages/core/src/database/path.ts index 66a130b46d0..8ddef89e807 100644 --- a/packages/core/src/database/path.ts +++ b/packages/core/src/database/path.ts @@ -12,12 +12,15 @@ function isWindowsStoragePath(input: string) { return /^[A-Za-z]:\//.test(input) || input.startsWith("//") } -function absolute(input: string) { +export function storedAbsolutePath(input: string) { const result = storagePath(input) // Persisted projects and sessions can move between operating systems during migration. - if (!nodePath.posix.isAbsolute(result) && !isWindowsStoragePath(result)) { - throw new Error(`Path is not absolute: ${input}`) - } + if (nodePath.posix.isAbsolute(result) || isWindowsStoragePath(result)) return result +} + +function absolute(input: string) { + const result = storedAbsolutePath(input) + if (result === undefined) throw new Error(`Path is not absolute: ${input}`) return result } diff --git a/packages/core/src/database/v1-migration.bun.ts b/packages/core/src/database/v1-migration.bun.ts index 591c7924fb9..b13aee50594 100644 --- a/packages/core/src/database/v1-migration.bun.ts +++ b/packages/core/src/database/v1-migration.bun.ts @@ -2,6 +2,7 @@ export * as V1Migration from "./v1-migration.js" import { Cause, Effect, Layer, Option, Schema, Semaphore } from "effect" import { Database } from "./database.js" +import { storedAbsolutePath } from "./path.js" import { SessionMessageTable, SessionTable } from "../session/sql.js" import { SessionV1 } from "@opencode/schema/session-v1" import { SessionMessage } from "../session/message.js" @@ -580,16 +581,19 @@ export function run(options: Options = {}): Effect.Effect(sql`SELECT id FROM project`)).map((project) => project.id), + const projects = new Map( + (yield* db.all<{ id: string; worktree: string }>(sql`SELECT id, worktree FROM project`)).map((project) => [ + project.id, + project.worktree, + ]), ) while (true) { const state = yield* readState(db) const cursorValue = state?.phase === "sessions" ? state.cursor : undefined - const nextID = yield* db.get<{ id: string; project_id: string }>( + const nextID = yield* db.get<{ id: string; project_id: string; directory: string }>( cursorValue === undefined - ? sql`SELECT id, project_id FROM session ORDER BY id DESC LIMIT 1` - : sql`SELECT id, project_id FROM session WHERE id < ${cursorValue} ORDER BY id DESC LIMIT 1`, + ? sql`SELECT id, project_id, directory FROM session ORDER BY id DESC LIMIT 1` + : sql`SELECT id, project_id, directory FROM session WHERE id < ${cursorValue} ORDER BY id DESC LIMIT 1`, ) if (!nextID) break yield* db @@ -609,6 +613,18 @@ export function run(options: Options = {}): Effect.Effect { ) }) - test("reassigns V1 sessions whose projects are missing to the global project", async () => { + test.each([ + { worktree: "/repo", directory: ".", expected: "/repo" }, + { worktree: "/repo", directory: "src", expected: "/repo" }, + { worktree: "C:/repo", directory: ".", expected: "C:/repo" }, + { worktree: "//server/share/repo", directory: ".", expected: "//server/share/repo" }, + { worktree: "/repo", directory: "C:/other", expected: "C:/other" }, + { worktree: "/repo", directory: "//server/share/other", expected: "//server/share/other" }, + { worktree: "/repo", directory: "", expected: "" }, + { worktree: "relative", directory: ".", expected: "" }, + ])("migrates $directory in $worktree", async (input) => { + await database( + Effect.gen(function* () { + const { db } = yield* Database.Service + yield* db.run(sql` + INSERT INTO project (id, worktree, time_created, time_updated, sandboxes) + VALUES ('saved-project', ${input.worktree}, 1, 2, '[]') + `) + yield* db.run(sql` + INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) + VALUES ('ses_test', 'saved-project', 'test', ${input.directory}, 'Test', '1', 1, 2) + `) + + expect(yield* V1Migration.run()).toEqual({ status: "completed" }) + expect(yield* db.get(sql`SELECT directory FROM session_v2 WHERE id = 'ses_test'`)).toEqual({ + directory: input.expected, + }) + expect(yield* db.get(sql`SELECT directory FROM session WHERE id = 'ses_test'`)).toEqual({ + directory: input.directory, + }) + }), + ) + }) + + test.each(["/tmp/orphan", "."])("reassigns V1 sessions with missing projects and directory %s", async (directory) => { await database( Effect.gen(function* () { const { db } = yield* Database.Service const global = yield* Global.Service yield* db.run( - sql`INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES ('ses_orphan', 'missing-project', 'orphan', '/tmp/orphan', 'Orphan', '1', 1, 2)`, + sql`INSERT INTO session (id, project_id, slug, directory, title, version, time_created, time_updated) VALUES ('ses_orphan', 'missing-project', 'orphan', ${directory}, 'Orphan', '1', 1, 2)`, ) expect(yield* V1Migration.run()).toEqual({ status: "completed" }) @@ -1136,6 +1169,9 @@ describe("V1Migration database workflow", () => { expect(yield* db.get(sql`SELECT worktree FROM project WHERE id = 'global'`)).toEqual({ worktree: path.parse(global.data).root, }) + expect(yield* db.get(sql`SELECT directory FROM session_v2 WHERE id = 'ses_orphan'`)).toEqual({ + directory: directory === "." ? path.parse(global.data).root.replaceAll("\\", "/") : directory, + }) expect(yield* db.get(sql`SELECT value FROM kv WHERE key = 'migration.v1-v2'`)).toEqual({ value: '{"phase":"completed"}', })