feat(id): brand ProjectID through Drizzle and Zod schemas (#16948)

This commit is contained in:
Kit Langton
2026-03-11 16:44:26 -04:00
committed by GitHub
parent c37f7b9d99
commit dbc00aa8e0
15 changed files with 77 additions and 44 deletions

View File

@@ -86,7 +86,7 @@ export const ImportCommand = cmd({
await bootstrap(process.cwd(), async () => { await bootstrap(process.cwd(), async () => {
let exportData: let exportData:
| { | {
info: Session.Info info: SDKSession
messages: Array<{ messages: Array<{
info: Message info: Message
parts: Part[] parts: Part[]
@@ -152,7 +152,7 @@ export const ImportCommand = cmd({
return return
} }
const row = { ...Session.toRow(exportData.info), project_id: Instance.project.id } const row = Session.toRow({ ...exportData.info, projectID: Instance.project.id })
Database.use((db) => Database.use((db) =>
db db
.insert(SessionTable) .insert(SessionTable)

View File

@@ -1,5 +1,6 @@
import z from "zod" import z from "zod"
import { Identifier } from "@/id/id" import { Identifier } from "@/id/id"
import { ProjectID } from "@/project/schema"
export const WorkspaceInfo = z.object({ export const WorkspaceInfo = z.object({
id: Identifier.schema("workspace"), id: Identifier.schema("workspace"),
@@ -8,7 +9,7 @@ export const WorkspaceInfo = z.object({
name: z.string().nullable(), name: z.string().nullable(),
directory: z.string().nullable(), directory: z.string().nullable(),
extra: z.unknown().nullable(), extra: z.unknown().nullable(),
projectID: z.string(), projectID: ProjectID.zod,
}) })
export type WorkspaceInfo = z.infer<typeof WorkspaceInfo> export type WorkspaceInfo = z.infer<typeof WorkspaceInfo>

View File

@@ -1,5 +1,6 @@
import { sqliteTable, text } from "drizzle-orm/sqlite-core" import { sqliteTable, text } from "drizzle-orm/sqlite-core"
import { ProjectTable } from "../project/project.sql" import { ProjectTable } from "../project/project.sql"
import type { ProjectID } from "../project/schema"
export const WorkspaceTable = sqliteTable("workspace", { export const WorkspaceTable = sqliteTable("workspace", {
id: text().primaryKey(), id: text().primaryKey(),
@@ -9,6 +10,7 @@ export const WorkspaceTable = sqliteTable("workspace", {
directory: text(), directory: text(),
extra: text({ mode: "json" }), extra: text({ mode: "json" }),
project_id: text() project_id: text()
.$type<ProjectID>()
.notNull() .notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }), .references(() => ProjectTable.id, { onDelete: "cascade" }),
}) })

View File

@@ -6,6 +6,7 @@ import { Project } from "@/project/project"
import { BusEvent } from "@/bus/bus-event" import { BusEvent } from "@/bus/bus-event"
import { GlobalBus } from "@/bus/global" import { GlobalBus } from "@/bus/global"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { ProjectID } from "@/project/schema"
import { WorkspaceTable } from "./workspace.sql" import { WorkspaceTable } from "./workspace.sql"
import { getAdaptor } from "./adaptors" import { getAdaptor } from "./adaptors"
import { WorkspaceInfo } from "./types" import { WorkspaceInfo } from "./types"
@@ -48,7 +49,7 @@ export namespace Workspace {
id: Identifier.schema("workspace").optional(), id: Identifier.schema("workspace").optional(),
type: Info.shape.type, type: Info.shape.type,
branch: Info.shape.branch, branch: Info.shape.branch,
projectID: Info.shape.projectID, projectID: ProjectID.zod,
extra: Info.shape.extra, extra: Info.shape.extra,
}) })

View File

@@ -7,6 +7,7 @@ import { Database, eq } from "@/storage/db"
import { PermissionTable } from "@/session/session.sql" import { PermissionTable } from "@/session/session.sql"
import { fn } from "@/util/fn" import { fn } from "@/util/fn"
import { Log } from "@/util/log" import { Log } from "@/util/log"
import { ProjectID } from "@/project/schema"
import { Wildcard } from "@/util/wildcard" import { Wildcard } from "@/util/wildcard"
import os from "os" import os from "os"
import z from "zod" import z from "zod"
@@ -90,7 +91,7 @@ export namespace PermissionNext {
export type Reply = z.infer<typeof Reply> export type Reply = z.infer<typeof Reply>
export const Approval = z.object({ export const Approval = z.object({
projectID: z.string(), projectID: ProjectID.zod,
patterns: z.string().array(), patterns: z.string().array(),
}) })

View File

@@ -1,8 +1,9 @@
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core" import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core"
import { Timestamps } from "../storage/schema.sql" import { Timestamps } from "../storage/schema.sql"
import type { ProjectID } from "./schema"
export const ProjectTable = sqliteTable("project", { export const ProjectTable = sqliteTable("project", {
id: text().primaryKey(), id: text().$type<ProjectID>().primaryKey(),
worktree: text().notNull(), worktree: text().notNull(),
vcs: text(), vcs: text(),
name: text(), name: text(),

View File

@@ -15,6 +15,7 @@ import { existsSync } from "fs"
import { git } from "../util/git" import { git } from "../util/git"
import { Glob } from "../util/glob" import { Glob } from "../util/glob"
import { which } from "../util/which" import { which } from "../util/which"
import { ProjectID } from "./schema"
export namespace Project { export namespace Project {
const log = Log.create({ service: "project" }) const log = Log.create({ service: "project" })
@@ -33,7 +34,7 @@ export namespace Project {
export const Info = z export const Info = z
.object({ .object({
id: z.string(), id: ProjectID.zod,
worktree: z.string(), worktree: z.string(),
vcs: z.literal("git").optional(), vcs: z.literal("git").optional(),
name: z.string().optional(), name: z.string().optional(),
@@ -73,7 +74,7 @@ export namespace Project {
? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined } ? { url: row.icon_url ?? undefined, color: row.icon_color ?? undefined }
: undefined : undefined
return { return {
id: row.id, id: ProjectID.make(row.id),
worktree: row.worktree, worktree: row.worktree,
vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined, vcs: row.vcs ? Info.shape.vcs.parse(row.vcs) : undefined,
name: row.name ?? undefined, name: row.name ?? undefined,
@@ -91,6 +92,7 @@ export namespace Project {
function readCachedId(dir: string) { function readCachedId(dir: string) {
return Filesystem.readText(path.join(dir, "opencode")) return Filesystem.readText(path.join(dir, "opencode"))
.then((x) => x.trim()) .then((x) => x.trim())
.then(ProjectID.make)
.catch(() => undefined) .catch(() => undefined)
} }
@@ -111,7 +113,7 @@ export namespace Project {
if (!gitBinary) { if (!gitBinary) {
return { return {
id: id ?? "global", id: id ?? ProjectID.global,
worktree: sandbox, worktree: sandbox,
sandbox, sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
@@ -130,7 +132,7 @@ export namespace Project {
if (!worktree) { if (!worktree) {
return { return {
id: id ?? "global", id: id ?? ProjectID.global,
worktree: sandbox, worktree: sandbox,
sandbox, sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
@@ -160,14 +162,14 @@ export namespace Project {
if (!roots) { if (!roots) {
return { return {
id: "global", id: ProjectID.global,
worktree: sandbox, worktree: sandbox,
sandbox, sandbox,
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
} }
} }
id = roots[0] id = roots[0] ? ProjectID.make(roots[0]) : undefined
if (id) { if (id) {
await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined) await Filesystem.write(path.join(dotgit, "opencode"), id).catch(() => undefined)
} }
@@ -175,7 +177,7 @@ export namespace Project {
if (!id) { if (!id) {
return { return {
id: "global", id: ProjectID.global,
worktree: sandbox, worktree: sandbox,
sandbox, sandbox,
vcs: "git", vcs: "git",
@@ -208,7 +210,7 @@ export namespace Project {
} }
return { return {
id: "global", id: ProjectID.global,
worktree: "/", worktree: "/",
sandbox: "/", sandbox: "/",
vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS), vcs: Info.shape.vcs.parse(Flag.OPENCODE_FAKE_VCS),
@@ -228,7 +230,7 @@ export namespace Project {
updated: Date.now(), updated: Date.now(),
}, },
} }
if (data.id !== "global") { if (data.id !== ProjectID.global) {
await migrateFromGlobal(data.id, data.worktree) await migrateFromGlobal(data.id, data.worktree)
} }
return fresh return fresh
@@ -308,12 +310,12 @@ export namespace Project {
return return
} }
async function migrateFromGlobal(id: string, worktree: string) { async function migrateFromGlobal(id: ProjectID, worktree: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, "global")).get()) const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, ProjectID.global)).get())
if (!row) return if (!row) return
const sessions = Database.use((db) => const sessions = Database.use((db) =>
db.select().from(SessionTable).where(eq(SessionTable.project_id, "global")).all(), db.select().from(SessionTable).where(eq(SessionTable.project_id, ProjectID.global)).all(),
) )
if (sessions.length === 0) return if (sessions.length === 0) return
@@ -323,14 +325,14 @@ export namespace Project {
// Skip sessions that belong to a different directory // Skip sessions that belong to a different directory
if (row.directory && row.directory !== worktree) return if (row.directory && row.directory !== worktree) return
log.info("migrating session", { sessionID: row.id, from: "global", to: id }) log.info("migrating session", { sessionID: row.id, from: ProjectID.global, to: id })
Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run()) Database.use((db) => db.update(SessionTable).set({ project_id: id }).where(eq(SessionTable.id, row.id)).run())
}).catch((error) => { }).catch((error) => {
log.error("failed to migrate sessions from global to project", { error, projectId: id }) log.error("failed to migrate sessions from global to project", { error, projectId: id })
}) })
} }
export function setInitialized(id: string) { export function setInitialized(id: ProjectID) {
Database.use((db) => Database.use((db) =>
db db
.update(ProjectTable) .update(ProjectTable)
@@ -352,7 +354,7 @@ export namespace Project {
) )
} }
export function get(id: string): Info | undefined { export function get(id: ProjectID): Info | undefined {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) return undefined if (!row) return undefined
return fromRow(row) return fromRow(row)
@@ -375,12 +377,13 @@ export namespace Project {
export const update = fn( export const update = fn(
z.object({ z.object({
projectID: z.string(), projectID: ProjectID.zod,
name: z.string().optional(), name: z.string().optional(),
icon: Info.shape.icon.optional(), icon: Info.shape.icon.optional(),
commands: Info.shape.commands.optional(), commands: Info.shape.commands.optional(),
}), }),
async (input) => { async (input) => {
const id = ProjectID.make(input.projectID)
const result = Database.use((db) => const result = Database.use((db) =>
db db
.update(ProjectTable) .update(ProjectTable)
@@ -391,7 +394,7 @@ export namespace Project {
commands: input.commands, commands: input.commands,
time_updated: Date.now(), time_updated: Date.now(),
}) })
.where(eq(ProjectTable.id, input.projectID)) .where(eq(ProjectTable.id, id))
.returning() .returning()
.get(), .get(),
) )
@@ -407,7 +410,7 @@ export namespace Project {
}, },
) )
export async function sandboxes(id: string) { export async function sandboxes(id: ProjectID) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) return [] if (!row) return []
const data = fromRow(row) const data = fromRow(row)
@@ -419,7 +422,7 @@ export namespace Project {
return valid return valid
} }
export async function addSandbox(id: string, directory: string) { export async function addSandbox(id: ProjectID, directory: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) throw new Error(`Project not found: ${id}`) if (!row) throw new Error(`Project not found: ${id}`)
const sandboxes = [...row.sandboxes] const sandboxes = [...row.sandboxes]
@@ -443,7 +446,7 @@ export namespace Project {
return data return data
} }
export async function removeSandbox(id: string, directory: string) { export async function removeSandbox(id: ProjectID, directory: string) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get()) const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, id)).get())
if (!row) throw new Error(`Project not found: ${id}`) if (!row) throw new Error(`Project not found: ${id}`)
const sandboxes = row.sandboxes.filter((s) => s !== directory) const sandboxes = row.sandboxes.filter((s) => s !== directory)

View File

@@ -0,0 +1,16 @@
import { Schema } from "effect"
import z from "zod"
import { withStatics } from "@/util/schema"
const projectIdSchema = Schema.String.pipe(Schema.brand("ProjectId"))
export type ProjectID = typeof projectIdSchema.Type
export const ProjectID = projectIdSchema.pipe(
withStatics((schema: typeof projectIdSchema) => ({
global: schema.makeUnsafe("global"),
make: (id: string) => schema.makeUnsafe(id),
zod: z.string().pipe(z.custom<ProjectID>()),
})),
)

View File

@@ -4,6 +4,7 @@ import { resolver } from "hono-openapi"
import { Instance } from "../../project/instance" import { Instance } from "../../project/instance"
import { Project } from "../../project/project" import { Project } from "../../project/project"
import z from "zod" import z from "zod"
import { ProjectID } from "../../project/schema"
import { errors } from "../error" import { errors } from "../error"
import { lazy } from "../../util/lazy" import { lazy } from "../../util/lazy"
import { InstanceBootstrap } from "../../project/bootstrap" import { InstanceBootstrap } from "../../project/bootstrap"
@@ -105,7 +106,7 @@ export const ProjectRoutes = lazy(() =>
...errors(400, 404), ...errors(400, 404),
}, },
}), }),
validator("param", z.object({ projectID: z.string() })), validator("param", z.object({ projectID: ProjectID.zod })),
validator("json", Project.update.schema.omit({ projectID: true })), validator("json", Project.update.schema.omit({ projectID: true })),
async (c) => { async (c) => {
const projectID = c.req.valid("param").projectID const projectID = c.req.valid("param").projectID

View File

@@ -23,6 +23,7 @@ import { fn } from "@/util/fn"
import { Command } from "../command" import { Command } from "../command"
import { Snapshot } from "@/snapshot" import { Snapshot } from "@/snapshot"
import { WorkspaceContext } from "../control-plane/workspace-context" import { WorkspaceContext } from "../control-plane/workspace-context"
import { ProjectID } from "../project/schema"
import type { Provider } from "@/provider/provider" import type { Provider } from "@/provider/provider"
import { PermissionNext } from "@/permission/next" import { PermissionNext } from "@/permission/next"
@@ -120,7 +121,7 @@ export namespace Session {
.object({ .object({
id: Identifier.schema("session"), id: Identifier.schema("session"),
slug: z.string(), slug: z.string(),
projectID: z.string(), projectID: ProjectID.zod,
workspaceID: z.string().optional(), workspaceID: z.string().optional(),
directory: z.string(), directory: z.string(),
parentID: Identifier.schema("session").optional(), parentID: Identifier.schema("session").optional(),
@@ -162,7 +163,7 @@ export namespace Session {
export const ProjectInfo = z export const ProjectInfo = z
.object({ .object({
id: z.string(), id: ProjectID.zod,
name: z.string().optional(), name: z.string().optional(),
worktree: z.string(), worktree: z.string(),
}) })

View File

@@ -3,6 +3,7 @@ import { ProjectTable } from "../project/project.sql"
import type { MessageV2 } from "./message-v2" import type { MessageV2 } from "./message-v2"
import type { Snapshot } from "../snapshot" import type { Snapshot } from "../snapshot"
import type { PermissionNext } from "../permission/next" import type { PermissionNext } from "../permission/next"
import type { ProjectID } from "../project/schema"
import { Timestamps } from "../storage/schema.sql" import { Timestamps } from "../storage/schema.sql"
type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID"> type PartData = Omit<MessageV2.Part, "id" | "sessionID" | "messageID">
@@ -13,6 +14,7 @@ export const SessionTable = sqliteTable(
{ {
id: text().primaryKey(), id: text().primaryKey(),
project_id: text() project_id: text()
.$type<ProjectID>()
.notNull() .notNull()
.references(() => ProjectTable.id, { onDelete: "cascade" }), .references(() => ProjectTable.id, { onDelete: "cascade" }),
workspace_id: text(), workspace_id: text(),

View File

@@ -8,6 +8,7 @@ import { InstanceBootstrap } from "../project/bootstrap"
import { Project } from "../project/project" import { Project } from "../project/project"
import { Database, eq } from "../storage/db" import { Database, eq } from "../storage/db"
import { ProjectTable } from "../project/project.sql" import { ProjectTable } from "../project/project.sql"
import type { ProjectID } from "../project/schema"
import { fn } from "../util/fn" import { fn } from "../util/fn"
import { Log } from "../util/log" import { Log } from "../util/log"
import { Process } from "../util/process" import { Process } from "../util/process"
@@ -310,7 +311,7 @@ export namespace Worktree {
return false return false
} }
async function runStartScripts(directory: string, input: { projectID: string; extra?: string }) { async function runStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) {
const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get()) const row = Database.use((db) => db.select().from(ProjectTable).where(eq(ProjectTable.id, input.projectID)).get())
const project = row ? Project.fromRow(row) : undefined const project = row ? Project.fromRow(row) : undefined
const startup = project?.commands?.start?.trim() ?? "" const startup = project?.commands?.start?.trim() ?? ""
@@ -322,7 +323,7 @@ export namespace Worktree {
return true return true
} }
function queueStartScripts(directory: string, input: { projectID: string; extra?: string }) { function queueStartScripts(directory: string, input: { projectID: ProjectID; extra?: string }) {
setTimeout(() => { setTimeout(() => {
const start = async () => { const start = async () => {
await runStartScripts(directory, input) await runStartScripts(directory, input)

View File

@@ -8,6 +8,7 @@ import path from "path"
import fs from "fs/promises" import fs from "fs/promises"
import { pathToFileURL } from "url" import { pathToFileURL } from "url"
import { Global } from "../../src/global" import { Global } from "../../src/global"
import { ProjectID } from "../../src/project/schema"
import { Filesystem } from "../../src/util/filesystem" import { Filesystem } from "../../src/util/filesystem"
// Get managed config directory from environment (set in preload.ts) // Get managed config directory from environment (set in preload.ts)
@@ -44,7 +45,7 @@ async function check(map: (dir: string) => string) {
const cfg = await Config.get() const cfg = await Config.get()
expect(cfg.snapshot).toBe(true) expect(cfg.snapshot).toBe(true)
expect(Instance.directory).toBe(Filesystem.resolve(tmp.path)) expect(Instance.directory).toBe(Filesystem.resolve(tmp.path))
expect(Instance.project.id).not.toBe("global") expect(Instance.project.id).not.toBe(ProjectID.global)
}, },
}) })
} finally { } finally {

View File

@@ -6,6 +6,7 @@ import path from "path"
import { tmpdir } from "../fixture/fixture" import { tmpdir } from "../fixture/fixture"
import { Filesystem } from "../../src/util/filesystem" import { Filesystem } from "../../src/util/filesystem"
import { GlobalBus } from "../../src/bus/global" import { GlobalBus } from "../../src/bus/global"
import { ProjectID } from "../../src/project/schema"
Log.init({ print: false }) Log.init({ print: false })
@@ -74,7 +75,7 @@ describe("Project.fromDirectory", () => {
const { project } = await p.fromDirectory(tmp.path) const { project } = await p.fromDirectory(tmp.path)
expect(project).toBeDefined() expect(project).toBeDefined()
expect(project.id).toBe("global") expect(project.id).toBe(ProjectID.global)
expect(project.vcs).toBe("git") expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path) expect(project.worktree).toBe(tmp.path)
@@ -90,7 +91,7 @@ describe("Project.fromDirectory", () => {
const { project } = await p.fromDirectory(tmp.path) const { project } = await p.fromDirectory(tmp.path)
expect(project).toBeDefined() expect(project).toBeDefined()
expect(project.id).not.toBe("global") expect(project.id).not.toBe(ProjectID.global)
expect(project.vcs).toBe("git") expect(project.vcs).toBe("git")
expect(project.worktree).toBe(tmp.path) expect(project.worktree).toBe(tmp.path)
@@ -107,7 +108,7 @@ describe("Project.fromDirectory", () => {
await withMode("rev-list-fail", async () => { await withMode("rev-list-fail", async () => {
const { project } = await p.fromDirectory(tmp.path) const { project } = await p.fromDirectory(tmp.path)
expect(project.vcs).toBe("git") expect(project.vcs).toBe("git")
expect(project.id).toBe("global") expect(project.id).toBe(ProjectID.global)
expect(project.worktree).toBe(tmp.path) expect(project.worktree).toBe(tmp.path)
}) })
}) })
@@ -301,7 +302,7 @@ describe("Project.update", () => {
await expect( await expect(
Project.update({ Project.update({
projectID: "nonexistent-project-id", projectID: ProjectID.make("nonexistent-project-id"),
name: "Should Fail", name: "Should Fail",
}), }),
).rejects.toThrow("Project not found: nonexistent-project-id") ).rejects.toThrow("Project not found: nonexistent-project-id")

View File

@@ -8,6 +8,7 @@ import { readFileSync, readdirSync } from "fs"
import { JsonMigration } from "../../src/storage/json-migration" import { JsonMigration } from "../../src/storage/json-migration"
import { Global } from "../../src/global" import { Global } from "../../src/global"
import { ProjectTable } from "../../src/project/project.sql" import { ProjectTable } from "../../src/project/project.sql"
import { ProjectID } from "../../src/project/schema"
import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql" import { SessionTable, MessageTable, PartTable, TodoTable, PermissionTable } from "../../src/session/session.sql"
import { SessionShareTable } from "../../src/share/share.sql" import { SessionShareTable } from "../../src/share/share.sql"
@@ -123,7 +124,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite }) const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all() const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1) expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_test123abc") expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
expect(projects[0].worktree).toBe("/test/path") expect(projects[0].worktree).toBe("/test/path")
expect(projects[0].name).toBe("Test Project") expect(projects[0].name).toBe("Test Project")
expect(projects[0].sandboxes).toEqual(["/test/sandbox"]) expect(projects[0].sandboxes).toEqual(["/test/sandbox"])
@@ -148,7 +149,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite }) const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all() const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1) expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_filename") // Uses filename, not JSON id expect(projects[0].id).toBe(ProjectID.make("proj_filename")) // Uses filename, not JSON id
}) })
test("migrates project with commands", async () => { test("migrates project with commands", async () => {
@@ -169,7 +170,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite }) const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all() const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1) expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_with_commands") expect(projects[0].id).toBe(ProjectID.make("proj_with_commands"))
expect(projects[0].commands).toEqual({ start: "npm run dev" }) expect(projects[0].commands).toEqual({ start: "npm run dev" })
}) })
@@ -190,7 +191,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite }) const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all() const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1) expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_no_commands") expect(projects[0].id).toBe(ProjectID.make("proj_no_commands"))
expect(projects[0].commands).toBeNull() expect(projects[0].commands).toBeNull()
}) })
@@ -220,7 +221,7 @@ describe("JSON to SQLite migration", () => {
const sessions = db.select().from(SessionTable).all() const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1) expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_test456def") expect(sessions[0].id).toBe("ses_test456def")
expect(sessions[0].project_id).toBe("proj_test123abc") expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
expect(sessions[0].slug).toBe("test-session") expect(sessions[0].slug).toBe("test-session")
expect(sessions[0].title).toBe("Test Session Title") expect(sessions[0].title).toBe("Test Session Title")
expect(sessions[0].summary_additions).toBe(10) expect(sessions[0].summary_additions).toBe(10)
@@ -426,7 +427,7 @@ describe("JSON to SQLite migration", () => {
const sessions = db.select().from(SessionTable).all() const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1) expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_migrated") expect(sessions[0].id).toBe("ses_migrated")
expect(sessions[0].project_id).toBe(gitBasedProjectID) // Uses directory, not stale JSON expect(sessions[0].project_id).toBe(ProjectID.make(gitBasedProjectID)) // Uses directory, not stale JSON
}) })
test("uses filename for session id when JSON has different value", async () => { test("uses filename for session id when JSON has different value", async () => {
@@ -458,7 +459,7 @@ describe("JSON to SQLite migration", () => {
const sessions = db.select().from(SessionTable).all() const sessions = db.select().from(SessionTable).all()
expect(sessions.length).toBe(1) expect(sessions.length).toBe(1)
expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id expect(sessions[0].id).toBe("ses_from_filename") // Uses filename, not JSON id
expect(sessions[0].project_id).toBe("proj_test123abc") expect(sessions[0].project_id).toBe(ProjectID.make("proj_test123abc"))
}) })
test("is idempotent (running twice doesn't duplicate)", async () => { test("is idempotent (running twice doesn't duplicate)", async () => {
@@ -643,7 +644,7 @@ describe("JSON to SQLite migration", () => {
const db = drizzle({ client: sqlite }) const db = drizzle({ client: sqlite })
const projects = db.select().from(ProjectTable).all() const projects = db.select().from(ProjectTable).all()
expect(projects.length).toBe(1) expect(projects.length).toBe(1)
expect(projects[0].id).toBe("proj_test123abc") expect(projects[0].id).toBe(ProjectID.make("proj_test123abc"))
}) })
test("skips invalid todo entries while preserving source positions", async () => { test("skips invalid todo entries while preserving source positions", async () => {