migrate-global.test.ts 4.9 KB

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