migrate-global.test.ts 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151
  1. import { describe, expect, test } from "bun:test"
  2. import { Project } from "../../src/project"
  3. import { Database, eq } from "../../src/storage"
  4. import { SessionTable } from "../../src/session/session.sql"
  5. import { ProjectTable } from "../../src/project/project.sql"
  6. import { ProjectID } from "../../src/project/schema"
  7. import { SessionID } from "../../src/session/schema"
  8. import { Log } from "../../src/util"
  9. import { $ } from "bun"
  10. import { tmpdir } from "../fixture/fixture"
  11. import { Effect } from "effect"
  12. Log.init({ print: false })
  13. function run<A>(fn: (svc: Project.Interface) => Effect.Effect<A>) {
  14. return Effect.runPromise(
  15. Effect.gen(function* () {
  16. const svc = yield* Project.Service
  17. return yield* fn(svc)
  18. }).pipe(Effect.provide(Project.defaultLayer)),
  19. )
  20. }
  21. function uid() {
  22. return SessionID.make(crypto.randomUUID())
  23. }
  24. function seed(opts: { id: SessionID; dir: string; project: ProjectID }) {
  25. const now = Date.now()
  26. Database.use((db) =>
  27. db
  28. .insert(SessionTable)
  29. .values({
  30. id: opts.id,
  31. project_id: opts.project,
  32. slug: opts.id,
  33. directory: opts.dir,
  34. title: "test",
  35. version: "0.0.0-test",
  36. time_created: now,
  37. time_updated: now,
  38. })
  39. .run(),
  40. )
  41. }
  42. function ensureGlobal() {
  43. Database.use((db) =>
  44. db
  45. .insert(ProjectTable)
  46. .values({
  47. id: ProjectID.global,
  48. worktree: "/",
  49. time_created: Date.now(),
  50. time_updated: Date.now(),
  51. sandboxes: [],
  52. })
  53. .onConflictDoNothing()
  54. .run(),
  55. )
  56. }
  57. describe("migrateFromGlobal", () => {
  58. test("migrates global sessions on first project creation", async () => {
  59. // 1. Start with git init but no commits — creates "global" project row
  60. await using tmp = await tmpdir()
  61. await $`git init`.cwd(tmp.path).quiet()
  62. await $`git config user.name "Test"`.cwd(tmp.path).quiet()
  63. await $`git config user.email "[email protected]"`.cwd(tmp.path).quiet()
  64. await $`git config commit.gpgsign false`.cwd(tmp.path).quiet()
  65. const { project: pre } = await run((svc) => svc.fromDirectory(tmp.path))
  66. expect(pre.id).toBe(ProjectID.global)
  67. // 2. Seed a session under "global" with matching directory
  68. const id = uid()
  69. seed({ id, dir: tmp.path, project: ProjectID.global })
  70. // 3. Make a commit so the project gets a real ID
  71. await $`git commit --allow-empty -m "root"`.cwd(tmp.path).quiet()
  72. const { project: real } = await run((svc) => svc.fromDirectory(tmp.path))
  73. expect(real.id).not.toBe(ProjectID.global)
  74. // 4. The session should have been migrated to the real project ID
  75. const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
  76. expect(row).toBeDefined()
  77. expect(row!.project_id).toBe(real.id)
  78. })
  79. test("migrates global sessions even when project row already exists", async () => {
  80. // 1. Create a repo with a commit — real project ID created immediately
  81. await using tmp = await tmpdir({ git: true })
  82. const { project } = await run((svc) => svc.fromDirectory(tmp.path))
  83. expect(project.id).not.toBe(ProjectID.global)
  84. // 2. Ensure "global" project row exists (as it would from a prior no-git session)
  85. ensureGlobal()
  86. // 3. Seed a session under "global" with matching directory.
  87. // This simulates a session created before git init that wasn't
  88. // present when the real project row was first created.
  89. const id = uid()
  90. seed({ id, dir: tmp.path, project: ProjectID.global })
  91. // 4. Call fromDirectory again — project row already exists,
  92. // so the current code skips migration entirely. This is the bug.
  93. await run((svc) => svc.fromDirectory(tmp.path))
  94. const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
  95. expect(row).toBeDefined()
  96. expect(row!.project_id).toBe(project.id)
  97. })
  98. test("does not claim sessions with empty directory", async () => {
  99. await using tmp = await tmpdir({ git: true })
  100. const { project } = await run((svc) => svc.fromDirectory(tmp.path))
  101. expect(project.id).not.toBe(ProjectID.global)
  102. ensureGlobal()
  103. // Legacy sessions may lack a directory value.
  104. // Without a matching origin directory, they should remain global.
  105. const id = uid()
  106. seed({ id, dir: "", project: ProjectID.global })
  107. await run((svc) => svc.fromDirectory(tmp.path))
  108. const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
  109. expect(row).toBeDefined()
  110. expect(row!.project_id).toBe(ProjectID.global)
  111. })
  112. test("does not steal sessions from unrelated directories", async () => {
  113. await using tmp = await tmpdir({ git: true })
  114. const { project } = await run((svc) => svc.fromDirectory(tmp.path))
  115. expect(project.id).not.toBe(ProjectID.global)
  116. ensureGlobal()
  117. // Seed a session under "global" but for a DIFFERENT directory
  118. const id = uid()
  119. seed({ id, dir: "/some/other/dir", project: ProjectID.global })
  120. await run((svc) => svc.fromDirectory(tmp.path))
  121. const row = Database.use((db) => db.select().from(SessionTable).where(eq(SessionTable.id, id)).get())
  122. expect(row).toBeDefined()
  123. // Should remain under "global" — not stolen
  124. expect(row!.project_id).toBe(ProjectID.global)
  125. })
  126. })