Compare commits
2 commits
dev
...
fix/sessio
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fa012bebcb | ||
|
|
390f49b4b8 |
4 changed files with 17 additions and 119 deletions
|
|
@ -108,7 +108,7 @@ export const layer = Layer.effect(
|
||||||
if (!repo) return { id: ID.global, directory: input, vcs: undefined }
|
if (!repo) return { id: ID.global, directory: input, vcs: undefined }
|
||||||
|
|
||||||
const previous = yield* cached(repo.store)
|
const previous = yield* cached(repo.store)
|
||||||
const id = (yield* remote(repo)) ?? previous ?? (yield* root(repo))
|
const id = previous ?? (yield* root(repo))
|
||||||
|
|
||||||
return {
|
return {
|
||||||
previous,
|
previous,
|
||||||
|
|
|
||||||
|
|
@ -5,16 +5,11 @@ import path from "path"
|
||||||
import { Effect } from "effect"
|
import { Effect } from "effect"
|
||||||
import { Project } from "@opencode-ai/core/project"
|
import { Project } from "@opencode-ai/core/project"
|
||||||
import { AbsolutePath } from "@opencode-ai/core/schema"
|
import { AbsolutePath } from "@opencode-ai/core/schema"
|
||||||
import { Hash } from "@opencode-ai/core/util/hash"
|
|
||||||
import { tmpdir } from "./fixture/tmpdir"
|
import { tmpdir } from "./fixture/tmpdir"
|
||||||
import { testEffect } from "./lib/effect"
|
import { testEffect } from "./lib/effect"
|
||||||
|
|
||||||
const it = testEffect(Project.defaultLayer)
|
const it = testEffect(Project.defaultLayer)
|
||||||
|
|
||||||
function remoteID(remote: string) {
|
|
||||||
return Project.ID.make(Hash.fast(`git-remote:${remote}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
function abs(value: string) {
|
function abs(value: string) {
|
||||||
return AbsolutePath.make(value)
|
return AbsolutePath.make(value)
|
||||||
}
|
}
|
||||||
|
|
@ -91,7 +86,7 @@ describe("ProjectV2.resolve", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("prefers normalized origin over root commit", () =>
|
it.live("uses root commit when origin exists", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tmp = yield* Effect.acquireRelease(
|
const tmp = yield* Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
|
|
@ -102,36 +97,13 @@ describe("ProjectV2.resolve", () => {
|
||||||
|
|
||||||
const result = yield* project.resolve(abs(tmp.path))
|
const result = yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
expect(result.id).toBe(remoteID("github.com/Acme/App"))
|
expect(result.id).toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
||||||
expect(result.id).not.toBe(Project.ID.make(yield* Effect.promise(() => rootCommit(tmp.path))))
|
|
||||||
expect(result.directory).toBe(yield* real(tmp.path))
|
expect(result.directory).toBe(yield* real(tmp.path))
|
||||||
expect(result.vcs?.type).toBe("git")
|
expect(result.vcs?.type).toBe("git")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("normalizes ssh and https remotes to the same id", () =>
|
it.live("uses root commit when local remote exists", () =>
|
||||||
Effect.gen(function* () {
|
|
||||||
const ssh = yield* Effect.acquireRelease(
|
|
||||||
Effect.promise(() => tmpdir()),
|
|
||||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
||||||
)
|
|
||||||
const https = yield* Effect.acquireRelease(
|
|
||||||
Effect.promise(() => tmpdir()),
|
|
||||||
(tmp) => Effect.promise(() => tmp[Symbol.asyncDispose]()),
|
|
||||||
)
|
|
||||||
yield* Effect.promise(() => initRepo(ssh.path, { commit: true, remote: "git@github.com:owner/repo.git" }))
|
|
||||||
yield* Effect.promise(() => initRepo(https.path, { commit: true, remote: "https://github.com/owner/repo.git" }))
|
|
||||||
const project = yield* Project.Service
|
|
||||||
|
|
||||||
const a = yield* project.resolve(abs(ssh.path))
|
|
||||||
const b = yield* project.resolve(abs(https.path))
|
|
||||||
|
|
||||||
expect(a.id).toBe(remoteID("github.com/owner/repo"))
|
|
||||||
expect(b.id).toBe(a.id)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("ignores file remotes and falls back to root commit", () =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tmp = yield* Effect.acquireRelease(
|
const tmp = yield* Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
|
|
@ -146,7 +118,7 @@ describe("ProjectV2.resolve", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("returns previous cached id from common dir", () =>
|
it.live("prefers previous cached id over origin", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tmp = yield* Effect.acquireRelease(
|
const tmp = yield* Effect.acquireRelease(
|
||||||
Effect.promise(() => tmpdir()),
|
Effect.promise(() => tmpdir()),
|
||||||
|
|
@ -159,7 +131,7 @@ describe("ProjectV2.resolve", () => {
|
||||||
const result = yield* project.resolve(abs(tmp.path))
|
const result = yield* project.resolve(abs(tmp.path))
|
||||||
|
|
||||||
expect(result.previous).toBe(Project.ID.make("old-id"))
|
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||||
expect(result.id).toBe(remoteID("github.com/owner/repo"))
|
expect(result.id).toBe(Project.ID.make("old-id"))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
@ -213,7 +185,7 @@ describe("ProjectV2.resolve", () => {
|
||||||
|
|
||||||
expect(result.directory).toBe(yield* real(worktree))
|
expect(result.directory).toBe(yield* real(worktree))
|
||||||
expect(result.previous).toBe(Project.ID.make("old-id"))
|
expect(result.previous).toBe(Project.ID.make("old-id"))
|
||||||
expect(result.id).toBe(remoteID("github.com/owner/repo"))
|
expect(result.id).toBe(Project.ID.make("old-id"))
|
||||||
expect(result.vcs?.type).toBe("git")
|
expect(result.vcs?.type).toBe("git")
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,5 @@
|
||||||
import { and } from "drizzle-orm"
|
import { and, eq, sql } from "drizzle-orm"
|
||||||
import { Database } from "@/storage/db"
|
import { Database } from "@/storage/db"
|
||||||
import { eq } from "drizzle-orm"
|
|
||||||
import { ProjectTable } from "./project.sql"
|
import { ProjectTable } from "./project.sql"
|
||||||
import { PermissionTable, SessionTable } from "../session/session.sql"
|
import { PermissionTable, SessionTable } from "../session/session.sql"
|
||||||
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
import { WorkspaceTable } from "../control-plane/workspace.sql"
|
||||||
|
|
@ -220,7 +219,10 @@ export const layer = Layer.effect(
|
||||||
d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
|
d.update(PermissionTable).set({ project_id: newID }).where(eq(PermissionTable.project_id, oldID)).run()
|
||||||
}
|
}
|
||||||
|
|
||||||
d.update(SessionTable).set({ project_id: newID }).where(eq(SessionTable.project_id, oldID)).run()
|
d.update(SessionTable)
|
||||||
|
.set({ project_id: newID, time_updated: sql`${SessionTable.time_updated}` })
|
||||||
|
.where(eq(SessionTable.project_id, oldID))
|
||||||
|
.run()
|
||||||
d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
|
d.update(WorkspaceTable).set({ project_id: newID }).where(eq(WorkspaceTable.project_id, oldID)).run()
|
||||||
|
|
||||||
if (oldProject) d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
|
if (oldProject) d.delete(ProjectTable).where(eq(ProjectTable.id, oldID)).run()
|
||||||
|
|
|
||||||
|
|
@ -7,15 +7,6 @@ import path from "path"
|
||||||
import { tmpdirScoped } from "../fixture/fixture"
|
import { tmpdirScoped } from "../fixture/fixture"
|
||||||
import { GlobalBus } from "../../src/bus/global"
|
import { GlobalBus } from "../../src/bus/global"
|
||||||
import { ProjectID } from "../../src/project/schema"
|
import { ProjectID } from "../../src/project/schema"
|
||||||
import { Database } from "@/storage/db"
|
|
||||||
import { ProjectTable } from "@/project/project.sql"
|
|
||||||
import { SessionTable } from "@/session/session.sql"
|
|
||||||
import { PermissionTable } from "@/session/session.sql"
|
|
||||||
import { WorkspaceTable } from "@/control-plane/workspace.sql"
|
|
||||||
import { eq } from "drizzle-orm"
|
|
||||||
import { Hash } from "@opencode-ai/core/util/hash"
|
|
||||||
import { SessionID } from "@/session/schema"
|
|
||||||
import { WorkspaceID } from "@/control-plane/schema"
|
|
||||||
import { Cause, Effect, Exit, Layer, Stream } from "effect"
|
import { Cause, Effect, Exit, Layer, Stream } from "effect"
|
||||||
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
import { ChildProcess, ChildProcessSpawner } from "effect/unstable/process"
|
||||||
import { NodePath } from "@effect/platform-node"
|
import { NodePath } from "@effect/platform-node"
|
||||||
|
|
@ -40,10 +31,6 @@ function run<A, E>(fn: (svc: Project.Interface) => Effect.Effect<A, E>) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
function remoteProjectID(remote: string) {
|
|
||||||
return ProjectID.make(Hash.fast(`git-remote:${remote}`))
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a mock ChildProcessSpawner layer that intercepts git subcommands
|
* Creates a mock ChildProcessSpawner layer that intercepts git subcommands
|
||||||
* matching `failArg` and returns exit code 128, while delegating everything
|
* matching `failArg` and returns exit code 128, while delegating everything
|
||||||
|
|
@ -167,91 +154,28 @@ describe("Project.fromDirectory", () => {
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("prefers normalized origin remote over root commit", () =>
|
it.live("keeps root commit identity when origin exists", () =>
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tmp = yield* tmpdirScoped({ git: true })
|
const tmp = yield* tmpdirScoped({ git: true })
|
||||||
yield* Effect.promise(() => $`git remote add origin git@github.com:Test-Org/Test-Repo.git`.cwd(tmp).quiet())
|
yield* Effect.promise(() => $`git remote add origin git@github.com:Test-Org/Test-Repo.git`.cwd(tmp).quiet())
|
||||||
|
|
||||||
const { project } = yield* run((svc) => svc.fromDirectory(tmp))
|
const { project } = yield* run((svc) => svc.fromDirectory(tmp))
|
||||||
|
const root = (yield* Effect.promise(() => $`git rev-list --max-parents=0 HEAD`.cwd(tmp).text())).trim()
|
||||||
|
|
||||||
expect(project.id).toBe(remoteProjectID("github.com/Test-Org/Test-Repo"))
|
expect(project.id).toBe(ProjectID.make(root))
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
|
|
||||||
it.live("normalizes equivalent origin URL forms to the same project ID", () =>
|
it.live("keeps cached project identity when origin becomes available", () =>
|
||||||
Effect.gen(function* () {
|
|
||||||
const ssh = yield* tmpdirScoped({ git: true })
|
|
||||||
const https = yield* tmpdirScoped({ git: true })
|
|
||||||
yield* Effect.promise(() => $`git remote add origin git@github.com:owner/repo.git`.cwd(ssh).quiet())
|
|
||||||
yield* Effect.promise(() => $`git remote add origin https://github.com/owner/repo.git`.cwd(https).quiet())
|
|
||||||
|
|
||||||
const { project: a } = yield* run((svc) => svc.fromDirectory(ssh))
|
|
||||||
const { project: b } = yield* run((svc) => svc.fromDirectory(https))
|
|
||||||
|
|
||||||
expect(a.id).toBe(remoteProjectID("github.com/owner/repo"))
|
|
||||||
expect(b.id).toBe(a.id)
|
|
||||||
}),
|
|
||||||
)
|
|
||||||
|
|
||||||
it.live("migrates cached root project data when origin becomes available", () =>
|
|
||||||
Effect.gen(function* () {
|
Effect.gen(function* () {
|
||||||
const tmp = yield* tmpdirScoped({ git: true })
|
const tmp = yield* tmpdirScoped({ git: true })
|
||||||
const projects = yield* Project.Service
|
const projects = yield* Project.Service
|
||||||
const { project: rootProject } = yield* projects.fromDirectory(tmp)
|
const { project: rootProject } = yield* projects.fromDirectory(tmp)
|
||||||
const remoteID = remoteProjectID("github.com/acme/app")
|
|
||||||
const sessionID = crypto.randomUUID() as SessionID
|
|
||||||
const workspaceID = WorkspaceID.ascending()
|
|
||||||
|
|
||||||
yield* Effect.sync(() => {
|
|
||||||
Database.use((db) => {
|
|
||||||
db.insert(SessionTable)
|
|
||||||
.values({
|
|
||||||
id: sessionID,
|
|
||||||
project_id: rootProject.id,
|
|
||||||
slug: sessionID,
|
|
||||||
directory: tmp,
|
|
||||||
title: "test",
|
|
||||||
version: "0.0.0-test",
|
|
||||||
time_created: Date.now(),
|
|
||||||
time_updated: Date.now(),
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
db.insert(PermissionTable)
|
|
||||||
.values({
|
|
||||||
project_id: rootProject.id,
|
|
||||||
data: [{ permission: "edit", pattern: "*", action: "allow" }],
|
|
||||||
time_created: Date.now(),
|
|
||||||
time_updated: Date.now(),
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
db.insert(WorkspaceTable)
|
|
||||||
.values({
|
|
||||||
id: workspaceID,
|
|
||||||
type: "local",
|
|
||||||
name: "test",
|
|
||||||
project_id: rootProject.id,
|
|
||||||
})
|
|
||||||
.run()
|
|
||||||
})
|
|
||||||
})
|
|
||||||
yield* Effect.promise(() => $`git remote add origin git@github.com:acme/app.git`.cwd(tmp).quiet())
|
yield* Effect.promise(() => $`git remote add origin git@github.com:acme/app.git`.cwd(tmp).quiet())
|
||||||
|
|
||||||
const { project } = yield* projects.fromDirectory(tmp)
|
const { project } = yield* projects.fromDirectory(tmp)
|
||||||
|
|
||||||
expect(project.id).toBe(remoteID)
|
expect(project.id).toBe(rootProject.id)
|
||||||
expect(
|
|
||||||
Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, rootProject.id)).get()),
|
|
||||||
).toBeUndefined()
|
|
||||||
expect(
|
|
||||||
Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, sessionID)).get())?.project_id,
|
|
||||||
).toBe(remoteID)
|
|
||||||
expect(
|
|
||||||
Database.use((db) => db.select().from(PermissionTable).where(eq(PermissionTable.project_id, remoteID)).get()),
|
|
||||||
).toBeDefined()
|
|
||||||
expect(
|
|
||||||
Database.use((db) => db.select().from(WorkspaceTable).where(eq(WorkspaceTable.id, workspaceID)).get())
|
|
||||||
?.project_id,
|
|
||||||
).toBe(remoteID)
|
|
||||||
}),
|
}),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue