fix(opencode): enforce storage path invariants (#29666)

This commit is contained in:
Luke Parker
2026-06-02 10:54:41 +10:00
committed by GitHub
parent a821029258
commit f0c7febb02
15 changed files with 1993 additions and 17 deletions

View File

@@ -13,6 +13,7 @@ import { GlobalBus, type GlobalEvent } from "@/bus/global"
import { Database } from "@opencode-ai/core/database/database"
import { ProjectV2 } from "@opencode-ai/core/project"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { Session as SessionNs } from "@/session/session"
import { SessionID } from "@/session/schema"
import { SessionTable } from "@opencode-ai/core/session/sql"
@@ -304,7 +305,7 @@ function insertProject(id: ProjectV2.ID, worktree: string) {
.insert(ProjectTable)
.values({
id,
worktree,
worktree: AbsolutePath.make(worktree),
vcs: null,
name: null,
time_created: Date.now(),

View File

@@ -4,6 +4,7 @@ import { Database } from "@opencode-ai/core/database/database"
import { eq } from "drizzle-orm"
import { SessionTable } from "@opencode-ai/core/session/sql"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { ProjectV2 } from "@opencode-ai/core/project"
import { SessionID } from "../../src/session/schema"
import * as Log from "@opencode-ai/core/util/log"
@@ -48,7 +49,7 @@ function ensureGlobal() {
.insert(ProjectTable)
.values({
id: ProjectV2.ID.global,
worktree: "/",
worktree: AbsolutePath.make("/"),
time_created: Date.now(),
time_updated: Date.now(),
sandboxes: [],

View File

@@ -99,6 +99,33 @@ describe("session.list", () => {
{ git: true },
)
it.instance(
"matches a session regardless of directory separator on Windows",
() =>
Effect.gen(function* () {
if (process.platform !== "win32") return
const test = yield* TestInstance
const dir = path.join(test.directory, "packages", "opencode")
yield* Effect.promise(() => mkdir(dir, { recursive: true }))
const created = yield* withSession({ title: "separator" }).pipe(provideInstance(dir))
// A forward-slash query (e.g. from the SDK/HTTP layer) must still find it —
// this is the regression: backslash-stored vs forward-slash-queried.
const forwardIDs = (yield* SessionNs.Service.use((session) =>
session.list({ directory: dir.replaceAll("\\", "/") }),
)).map((session) => session.id)
expect(forwardIDs).toContain(created.id)
// The native form must keep matching too.
const nativeIDs = (yield* SessionNs.Service.use((session) => session.list({ directory: dir }))).map(
(session) => session.id,
)
expect(nativeIDs).toContain(created.id)
}),
{ git: true },
)
it.instance(
"filters by path and ignores directory when path is provided",
() =>
@@ -132,6 +159,14 @@ describe("session.list", () => {
expect(pathIDs).toContain(current.id)
expect(pathIDs).toContain(deeper.id)
expect(pathIDs).not.toContain(sibling.id)
if (process.platform === "win32") {
const windowsPathIDs = (yield* SessionNs.Service.use((session) =>
session.list({ path: "packages\\opencode\\src" }),
)).map((session) => session.id)
expect(windowsPathIDs).toContain(current.id)
expect(windowsPathIDs).toContain(deeper.id)
}
}),
{ git: true },
)

View File

@@ -9,6 +9,7 @@ import { JsonMigration } from "@/storage/json-migration"
import { Global } from "@opencode-ai/core/global"
import { ProjectTable } from "@opencode-ai/core/project/sql"
import { ProjectV2 } from "@opencode-ai/core/project"
import { AbsolutePath } from "@opencode-ai/core/schema"
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "@opencode-ai/core/session/sql"
import { SessionShareTable } from "@opencode-ai/core/share/sql"
import { SessionID, MessageID, PartID } from "../../src/session/schema"
@@ -128,9 +129,39 @@ describe("JSON to SQLite migration", () => {
const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1)
expect(projects[0].id).toBe(ProjectV2.ID.make("proj_test123abc"))
expect(projects[0].worktree).toBe("/test/path")
expect(projects[0].worktree).toBe(AbsolutePath.make("/test/path"))
expect(projects[0].name).toBe("Test Project")
expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
expect(projects[0].sandboxes).toEqual([AbsolutePath.make("/test/sandbox")])
})
test("stores imported Windows project and session paths in storage form", async () => {
if (process.platform !== "win32") return
await writeProject(storageDir, {
id: "proj_test123abc",
worktree: "C:\\Repo\\Thing",
vcs: "git",
sandboxes: ["C:\\Repo\\Thing\\sandbox"],
})
await writeSession(storageDir, "proj_test123abc", {
id: "ses_test456def",
slug: "storage-path",
directory: "C:\\Repo\\Thing\\packages\\api",
path: "packages\\api",
title: "Storage Path",
version: "test",
})
await JsonMigration.run(db)
expect(sqlite.query("SELECT worktree, sandboxes FROM project WHERE id = ?").get("proj_test123abc")).toEqual({
worktree: "C:/Repo/Thing",
sandboxes: JSON.stringify(["C:/Repo/Thing/sandbox"]),
})
expect(sqlite.query("SELECT directory, path FROM session WHERE id = ?").get("ses_test456def")).toEqual({
directory: "C:/Repo/Thing/packages/api",
path: "packages/api",
})
})
test("uses filename for project id when JSON has different value", async () => {