@vladimirven/openswe 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (117) hide show
  1. package/AGENTS.md +203 -0
  2. package/CLAUDE.md +203 -0
  3. package/README.md +166 -0
  4. package/bun.lock +447 -0
  5. package/bunfig.toml +4 -0
  6. package/package.json +42 -0
  7. package/src/app.tsx +84 -0
  8. package/src/components/App.tsx +526 -0
  9. package/src/components/ConfirmDialog.tsx +88 -0
  10. package/src/components/Footer.tsx +50 -0
  11. package/src/components/HelpModal.tsx +136 -0
  12. package/src/components/IssueSelectorModal.tsx +701 -0
  13. package/src/components/ManualSessionModal.tsx +191 -0
  14. package/src/components/PhaseProgress.tsx +45 -0
  15. package/src/components/Preview.tsx +249 -0
  16. package/src/components/ProviderSwitcherModal.tsx +156 -0
  17. package/src/components/ScrollableText.tsx +120 -0
  18. package/src/components/SessionCard.tsx +60 -0
  19. package/src/components/SessionList.tsx +79 -0
  20. package/src/components/SessionTerminal.tsx +89 -0
  21. package/src/components/StatusBar.tsx +84 -0
  22. package/src/components/ThemeSwitcherModal.tsx +237 -0
  23. package/src/components/index.ts +58 -0
  24. package/src/components/session-utils.ts +337 -0
  25. package/src/components/theme.ts +206 -0
  26. package/src/components/types.ts +215 -0
  27. package/src/config/defaults.ts +44 -0
  28. package/src/config/env.ts +67 -0
  29. package/src/config/global.ts +252 -0
  30. package/src/config/index.ts +171 -0
  31. package/src/config/types.ts +131 -0
  32. package/src/core/.gitkeep +0 -0
  33. package/src/core/index.ts +5 -0
  34. package/src/core/parser.ts +62 -0
  35. package/src/core/process-manager.ts +52 -0
  36. package/src/core/session.ts +423 -0
  37. package/src/core/tmux.ts +206 -0
  38. package/src/git/.gitkeep +0 -0
  39. package/src/git/index.ts +8 -0
  40. package/src/git/repo.ts +443 -0
  41. package/src/git/worktree.ts +317 -0
  42. package/src/github/.gitkeep +0 -0
  43. package/src/github/client.ts +208 -0
  44. package/src/github/index.ts +8 -0
  45. package/src/github/issues.ts +351 -0
  46. package/src/index.ts +369 -0
  47. package/src/prompts/.gitkeep +0 -0
  48. package/src/prompts/index.ts +1 -0
  49. package/src/prompts/swe-system.ts +22 -0
  50. package/src/providers/claude.ts +103 -0
  51. package/src/providers/index.ts +21 -0
  52. package/src/providers/opencode.ts +98 -0
  53. package/src/providers/registry.ts +53 -0
  54. package/src/providers/types.ts +117 -0
  55. package/src/store/buffers.ts +234 -0
  56. package/src/store/db.test.ts +579 -0
  57. package/src/store/db.ts +249 -0
  58. package/src/store/index.ts +101 -0
  59. package/src/store/project.ts +119 -0
  60. package/src/store/schema.sql +71 -0
  61. package/src/store/sessions.ts +454 -0
  62. package/src/store/types.ts +194 -0
  63. package/src/theme/context.tsx +170 -0
  64. package/src/theme/custom.ts +134 -0
  65. package/src/theme/index.ts +58 -0
  66. package/src/theme/loader.ts +264 -0
  67. package/src/theme/themes/aura.json +69 -0
  68. package/src/theme/themes/ayu.json +80 -0
  69. package/src/theme/themes/carbonfox.json +248 -0
  70. package/src/theme/themes/catppuccin-frappe.json +233 -0
  71. package/src/theme/themes/catppuccin-macchiato.json +233 -0
  72. package/src/theme/themes/catppuccin.json +112 -0
  73. package/src/theme/themes/cobalt2.json +228 -0
  74. package/src/theme/themes/cursor.json +249 -0
  75. package/src/theme/themes/dracula.json +219 -0
  76. package/src/theme/themes/everforest.json +241 -0
  77. package/src/theme/themes/flexoki.json +237 -0
  78. package/src/theme/themes/github.json +233 -0
  79. package/src/theme/themes/gruvbox.json +242 -0
  80. package/src/theme/themes/kanagawa.json +77 -0
  81. package/src/theme/themes/lucent-orng.json +237 -0
  82. package/src/theme/themes/material.json +235 -0
  83. package/src/theme/themes/matrix.json +77 -0
  84. package/src/theme/themes/mercury.json +252 -0
  85. package/src/theme/themes/monokai.json +221 -0
  86. package/src/theme/themes/nightowl.json +221 -0
  87. package/src/theme/themes/nord.json +223 -0
  88. package/src/theme/themes/one-dark.json +84 -0
  89. package/src/theme/themes/opencode.json +245 -0
  90. package/src/theme/themes/orng.json +249 -0
  91. package/src/theme/themes/osaka-jade.json +93 -0
  92. package/src/theme/themes/palenight.json +222 -0
  93. package/src/theme/themes/rosepine.json +234 -0
  94. package/src/theme/themes/solarized.json +223 -0
  95. package/src/theme/themes/synthwave84.json +226 -0
  96. package/src/theme/themes/tokyonight.json +243 -0
  97. package/src/theme/themes/vercel.json +245 -0
  98. package/src/theme/themes/vesper.json +218 -0
  99. package/src/theme/themes/zenburn.json +223 -0
  100. package/src/theme/types.ts +225 -0
  101. package/src/types/sql.d.ts +4 -0
  102. package/src/utils/ansi-parser.ts +225 -0
  103. package/src/utils/format.ts +46 -0
  104. package/src/utils/id.ts +15 -0
  105. package/src/utils/logger.ts +112 -0
  106. package/src/utils/prerequisites.ts +118 -0
  107. package/src/utils/shell.ts +9 -0
  108. package/src/wizard/flows.ts +419 -0
  109. package/src/wizard/index.ts +37 -0
  110. package/src/wizard/prompts.ts +190 -0
  111. package/src/workspace/detect.test.ts +51 -0
  112. package/src/workspace/detect.ts +223 -0
  113. package/src/workspace/index.ts +71 -0
  114. package/src/workspace/init.ts +131 -0
  115. package/src/workspace/paths.ts +143 -0
  116. package/src/workspace/project.ts +164 -0
  117. package/tsconfig.json +22 -0
@@ -0,0 +1,579 @@
1
+ /**
2
+ * Database layer tests
3
+ *
4
+ * Tests for all store module functionality.
5
+ */
6
+
7
+ import { test, expect, beforeEach, afterEach, describe } from "bun:test"
8
+ import {
9
+ initDatabaseWithPath,
10
+ closeDatabase,
11
+ getDatabase,
12
+ isDatabaseInitialized,
13
+ getSchemaVersion,
14
+ withTransaction,
15
+ } from "./db"
16
+ import {
17
+ getProject,
18
+ createProject,
19
+ updateLastOpened,
20
+ projectExists,
21
+ deleteProject,
22
+ } from "./project"
23
+ import {
24
+ getAllSessions,
25
+ getSession,
26
+ getSessionsByStatus,
27
+ getActiveSessionCount,
28
+ getSessionCount,
29
+ createSession,
30
+ updateSession,
31
+ updateSessionPhase,
32
+ updateSessionStatus,
33
+ incrementRetryCount,
34
+ updateTokensUsed,
35
+ setPid,
36
+ deleteSession,
37
+ } from "./sessions"
38
+ import {
39
+ getBuffer,
40
+ getRecentLines,
41
+ createBuffer,
42
+ appendLines,
43
+ appendLine,
44
+ setLines,
45
+ clearBuffer,
46
+ MAX_BUFFER_LINES,
47
+ } from "./buffers"
48
+
49
+ // ============================================================================
50
+ // Test Setup
51
+ // ============================================================================
52
+
53
+ beforeEach(async () => {
54
+ // Use in-memory database for tests
55
+ await initDatabaseWithPath(":memory:")
56
+ })
57
+
58
+ afterEach(() => {
59
+ closeDatabase()
60
+ })
61
+
62
+ // ============================================================================
63
+ // Database Initialization Tests
64
+ // ============================================================================
65
+
66
+ describe("Database Initialization", () => {
67
+ test("initializes database successfully", () => {
68
+ expect(isDatabaseInitialized()).toBe(true)
69
+ })
70
+
71
+ test("getDatabase returns database instance", () => {
72
+ const db = getDatabase()
73
+ expect(db).toBeDefined()
74
+ })
75
+
76
+ test("schema version is 2", () => {
77
+ const version = getSchemaVersion()
78
+ expect(version).toBe(2)
79
+ })
80
+
81
+ test("tables are created", () => {
82
+ const db = getDatabase()
83
+ const tables = db
84
+ .query<{ name: string }, []>(
85
+ "SELECT name FROM sqlite_master WHERE type='table' ORDER BY name"
86
+ )
87
+ .all()
88
+
89
+ const tableNames = tables.map((t) => t.name)
90
+ expect(tableNames).toContain("project")
91
+ expect(tableNames).toContain("sessions")
92
+ expect(tableNames).toContain("output_buffers")
93
+ expect(tableNames).toContain("schema_version")
94
+ })
95
+
96
+ test("closeDatabase closes connection", () => {
97
+ closeDatabase()
98
+ expect(isDatabaseInitialized()).toBe(false)
99
+ })
100
+ })
101
+
102
+ // ============================================================================
103
+ // Project CRUD Tests
104
+ // ============================================================================
105
+
106
+ describe("Project Operations", () => {
107
+ test("project does not exist initially", () => {
108
+ expect(projectExists()).toBe(false)
109
+ expect(getProject()).toBeNull()
110
+ })
111
+
112
+ test("createProject creates project", () => {
113
+ const project = createProject({
114
+ repoFullName: "owner/repo",
115
+ repoUrl: "git@github.com:owner/repo.git",
116
+ })
117
+
118
+ expect(project.id).toBe(1)
119
+ expect(project.repoFullName).toBe("owner/repo")
120
+ expect(project.repoUrl).toBe("git@github.com:owner/repo.git")
121
+ expect(project.createdAt).toBeDefined()
122
+ expect(project.lastOpenedAt).toBeDefined()
123
+ })
124
+
125
+ test("getProject retrieves created project", () => {
126
+ createProject({
127
+ repoFullName: "owner/repo",
128
+ repoUrl: "git@github.com:owner/repo.git",
129
+ })
130
+
131
+ const project = getProject()
132
+ expect(project).not.toBeNull()
133
+ expect(project!.repoFullName).toBe("owner/repo")
134
+ })
135
+
136
+ test("createProject throws if project exists", () => {
137
+ createProject({
138
+ repoFullName: "owner/repo",
139
+ repoUrl: "git@github.com:owner/repo.git",
140
+ })
141
+
142
+ expect(() =>
143
+ createProject({
144
+ repoFullName: "other/repo",
145
+ repoUrl: "git@github.com:other/repo.git",
146
+ })
147
+ ).toThrow("Project already exists")
148
+ })
149
+
150
+ test("updateLastOpened updates timestamp", async () => {
151
+ createProject({
152
+ repoFullName: "owner/repo",
153
+ repoUrl: "git@github.com:owner/repo.git",
154
+ })
155
+
156
+ const before = getProject()!.lastOpenedAt
157
+
158
+ // Small delay to ensure timestamp changes
159
+ await new Promise((resolve) => setTimeout(resolve, 10))
160
+
161
+ updateLastOpened()
162
+
163
+ const after = getProject()!.lastOpenedAt
164
+ expect(after).not.toBe(before)
165
+ })
166
+
167
+ test("deleteProject removes project", () => {
168
+ createProject({
169
+ repoFullName: "owner/repo",
170
+ repoUrl: "git@github.com:owner/repo.git",
171
+ })
172
+
173
+ expect(projectExists()).toBe(true)
174
+ deleteProject()
175
+ expect(projectExists()).toBe(false)
176
+ })
177
+ })
178
+
179
+ // ============================================================================
180
+ // Session CRUD Tests
181
+ // ============================================================================
182
+
183
+ describe("Session Operations", () => {
184
+ test("getAllSessions returns empty initially", () => {
185
+ expect(getAllSessions()).toEqual([])
186
+ })
187
+
188
+ test("getSessionCount returns 0 initially", () => {
189
+ expect(getSessionCount()).toBe(0)
190
+ })
191
+
192
+ test("createSession creates session with defaults", () => {
193
+ const session = createSession({
194
+ name: "test-session",
195
+ worktreePath: "/path/to/worktree",
196
+ branchName: "openswe/test-session",
197
+ })
198
+
199
+ expect(session.id).toBeDefined()
200
+ expect(session.name).toBe("test-session")
201
+ expect(session.worktreePath).toBe("/path/to/worktree")
202
+ expect(session.branchName).toBe("openswe/test-session")
203
+ expect(session.phase).toBe("pending")
204
+ expect(session.status).toBe("queued")
205
+ expect(session.issueNumber).toBeNull()
206
+ expect(session.retryCount).toBe(0)
207
+ expect(session.tokensUsed).toBe(0)
208
+ expect(session.pid).toBeNull()
209
+ })
210
+
211
+ test("createSession with issue data", () => {
212
+ const session = createSession({
213
+ name: "issue-123",
214
+ issueNumber: 123,
215
+ issueTitle: "Fix bug",
216
+ issueBody: "Description of bug",
217
+ issueUrl: "https://github.com/owner/repo/issues/123",
218
+ worktreePath: "/path/to/worktree",
219
+ branchName: "openswe/issue-123",
220
+ })
221
+
222
+ expect(session.issueNumber).toBe(123)
223
+ expect(session.issueTitle).toBe("Fix bug")
224
+ expect(session.issueBody).toBe("Description of bug")
225
+ expect(session.issueUrl).toBe("https://github.com/owner/repo/issues/123")
226
+ })
227
+
228
+ test("getSession retrieves by ID", () => {
229
+ const created = createSession({
230
+ name: "test-session",
231
+ worktreePath: "/path/to/worktree",
232
+ branchName: "openswe/test-session",
233
+ })
234
+
235
+ const retrieved = getSession(created.id)
236
+ expect(retrieved).not.toBeNull()
237
+ expect(retrieved!.id).toBe(created.id)
238
+ expect(retrieved!.name).toBe("test-session")
239
+ })
240
+
241
+ test("getSession returns null for non-existent ID", () => {
242
+ expect(getSession("non-existent-id")).toBeNull()
243
+ })
244
+
245
+ test("getAllSessions returns all sessions", () => {
246
+ createSession({
247
+ name: "session-1",
248
+ worktreePath: "/path/1",
249
+ branchName: "openswe/session-1",
250
+ })
251
+ createSession({
252
+ name: "session-2",
253
+ worktreePath: "/path/2",
254
+ branchName: "openswe/session-2",
255
+ })
256
+
257
+ const sessions = getAllSessions()
258
+ expect(sessions.length).toBe(2)
259
+ })
260
+
261
+ test("getSessionsByStatus filters correctly", () => {
262
+ const session1 = createSession({
263
+ name: "session-1",
264
+ worktreePath: "/path/1",
265
+ branchName: "openswe/session-1",
266
+ })
267
+ createSession({
268
+ name: "session-2",
269
+ worktreePath: "/path/2",
270
+ branchName: "openswe/session-2",
271
+ })
272
+
273
+ updateSessionStatus(session1.id, "active")
274
+
275
+ const active = getSessionsByStatus("active")
276
+ expect(active.length).toBe(1)
277
+ expect(active[0]?.name).toBe("session-1")
278
+
279
+ const queued = getSessionsByStatus("queued")
280
+ expect(queued.length).toBe(1)
281
+ expect(queued[0]?.name).toBe("session-2")
282
+ })
283
+
284
+ test("getActiveSessionCount counts active sessions", () => {
285
+ const session1 = createSession({
286
+ name: "session-1",
287
+ worktreePath: "/path/1",
288
+ branchName: "openswe/session-1",
289
+ })
290
+ createSession({
291
+ name: "session-2",
292
+ worktreePath: "/path/2",
293
+ branchName: "openswe/session-2",
294
+ })
295
+
296
+ expect(getActiveSessionCount()).toBe(0)
297
+
298
+ updateSessionStatus(session1.id, "active")
299
+ expect(getActiveSessionCount()).toBe(1)
300
+ })
301
+
302
+ test("updateSession updates multiple fields", () => {
303
+ const session = createSession({
304
+ name: "test-session",
305
+ worktreePath: "/path/to/worktree",
306
+ branchName: "openswe/test-session",
307
+ })
308
+
309
+ const updated = updateSession(session.id, {
310
+ name: "new-name",
311
+ phase: "working",
312
+ status: "active",
313
+ tokensUsed: 1000,
314
+ })
315
+
316
+ expect(updated.name).toBe("new-name")
317
+ expect(updated.phase).toBe("working")
318
+ expect(updated.status).toBe("active")
319
+ expect(updated.tokensUsed).toBe(1000)
320
+ })
321
+
322
+ test("updateSessionPhase updates phase", () => {
323
+ const session = createSession({
324
+ name: "test-session",
325
+ worktreePath: "/path/to/worktree",
326
+ branchName: "openswe/test-session",
327
+ })
328
+
329
+ updateSessionPhase(session.id, "planning")
330
+ expect(getSession(session.id)!.phase).toBe("planning")
331
+
332
+ updateSessionPhase(session.id, "working")
333
+ expect(getSession(session.id)!.phase).toBe("working")
334
+ })
335
+
336
+ test("updateSessionStatus with attention reason", () => {
337
+ const session = createSession({
338
+ name: "test-session",
339
+ worktreePath: "/path/to/worktree",
340
+ branchName: "openswe/test-session",
341
+ })
342
+
343
+ updateSessionStatus(session.id, "needs_attention", "AI needs clarification")
344
+
345
+ const updated = getSession(session.id)!
346
+ expect(updated.status).toBe("needs_attention")
347
+ expect(updated.attentionReason).toBe("AI needs clarification")
348
+ })
349
+
350
+ test("updateSessionStatus clears attention reason for other statuses", () => {
351
+ const session = createSession({
352
+ name: "test-session",
353
+ worktreePath: "/path/to/worktree",
354
+ branchName: "openswe/test-session",
355
+ })
356
+
357
+ updateSessionStatus(session.id, "needs_attention", "Needs help")
358
+ updateSessionStatus(session.id, "active")
359
+
360
+ const updated = getSession(session.id)!
361
+ expect(updated.status).toBe("active")
362
+ expect(updated.attentionReason).toBeNull()
363
+ })
364
+
365
+ test("incrementRetryCount increments and returns new value", () => {
366
+ const session = createSession({
367
+ name: "test-session",
368
+ worktreePath: "/path/to/worktree",
369
+ branchName: "openswe/test-session",
370
+ })
371
+
372
+ expect(incrementRetryCount(session.id)).toBe(1)
373
+ expect(incrementRetryCount(session.id)).toBe(2)
374
+ expect(incrementRetryCount(session.id)).toBe(3)
375
+ })
376
+
377
+ test("updateTokensUsed sets tokens", () => {
378
+ const session = createSession({
379
+ name: "test-session",
380
+ worktreePath: "/path/to/worktree",
381
+ branchName: "openswe/test-session",
382
+ })
383
+
384
+ updateTokensUsed(session.id, 5000)
385
+ expect(getSession(session.id)!.tokensUsed).toBe(5000)
386
+ })
387
+
388
+ test("setPid sets and clears PID", () => {
389
+ const session = createSession({
390
+ name: "test-session",
391
+ worktreePath: "/path/to/worktree",
392
+ branchName: "openswe/test-session",
393
+ })
394
+
395
+ setPid(session.id, 12345)
396
+ expect(getSession(session.id)!.pid).toBe(12345)
397
+
398
+ setPid(session.id, null)
399
+ expect(getSession(session.id)!.pid).toBeNull()
400
+ })
401
+
402
+ test("deleteSession removes session", () => {
403
+ const session = createSession({
404
+ name: "test-session",
405
+ worktreePath: "/path/to/worktree",
406
+ branchName: "openswe/test-session",
407
+ })
408
+
409
+ expect(getSession(session.id)).not.toBeNull()
410
+ deleteSession(session.id)
411
+ expect(getSession(session.id)).toBeNull()
412
+ })
413
+ })
414
+
415
+ // ============================================================================
416
+ // Human Task CRUD Tests - REMOVED
417
+ // ============================================================================
418
+
419
+ // ============================================================================
420
+ // Output Buffer Tests
421
+ // ============================================================================
422
+
423
+ describe("Output Buffer Operations", () => {
424
+ let sessionId: string
425
+
426
+ beforeEach(() => {
427
+ const session = createSession({
428
+ name: "test-session",
429
+ worktreePath: "/path/to/worktree",
430
+ branchName: "openswe/test-session",
431
+ })
432
+ sessionId = session.id
433
+ })
434
+
435
+ test("getBuffer returns null for non-existent buffer", () => {
436
+ expect(getBuffer(sessionId)).toBeNull()
437
+ })
438
+
439
+ test("createBuffer creates empty buffer", () => {
440
+ const buffer = createBuffer(sessionId)
441
+
442
+ expect(buffer.sessionId).toBe(sessionId)
443
+ expect(buffer.lines).toEqual([])
444
+ expect(buffer.lastUpdated).toBeDefined()
445
+ })
446
+
447
+ test("getBuffer retrieves created buffer", () => {
448
+ createBuffer(sessionId)
449
+
450
+ const buffer = getBuffer(sessionId)
451
+ expect(buffer).not.toBeNull()
452
+ expect(buffer!.sessionId).toBe(sessionId)
453
+ })
454
+
455
+ test("appendLines adds lines", () => {
456
+ createBuffer(sessionId)
457
+
458
+ appendLines(sessionId, ["line 1", "line 2"])
459
+
460
+ const buffer = getBuffer(sessionId)!
461
+ expect(buffer.lines).toEqual(["line 1", "line 2"])
462
+ })
463
+
464
+ test("appendLines creates buffer if needed", () => {
465
+ appendLines(sessionId, ["line 1"])
466
+
467
+ const buffer = getBuffer(sessionId)
468
+ expect(buffer).not.toBeNull()
469
+ expect(buffer!.lines).toEqual(["line 1"])
470
+ })
471
+
472
+ test("appendLine appends single line", () => {
473
+ appendLine(sessionId, "line 1")
474
+ appendLine(sessionId, "line 2")
475
+
476
+ const buffer = getBuffer(sessionId)!
477
+ expect(buffer.lines).toEqual(["line 1", "line 2"])
478
+ })
479
+
480
+ test("getRecentLines returns last N lines", () => {
481
+ appendLines(sessionId, ["line 1", "line 2", "line 3", "line 4", "line 5"])
482
+
483
+ expect(getRecentLines(sessionId, 3)).toEqual(["line 3", "line 4", "line 5"])
484
+ expect(getRecentLines(sessionId, 10)).toEqual([
485
+ "line 1",
486
+ "line 2",
487
+ "line 3",
488
+ "line 4",
489
+ "line 5",
490
+ ])
491
+ })
492
+
493
+ test("getRecentLines returns empty for non-existent buffer", () => {
494
+ expect(getRecentLines(sessionId, 10)).toEqual([])
495
+ })
496
+
497
+ test("circular buffer drops oldest lines", () => {
498
+ // Create lines that exceed MAX_BUFFER_LINES
499
+ const manyLines = Array.from({ length: MAX_BUFFER_LINES + 100 }, (_, i) => `line ${i}`)
500
+
501
+ appendLines(sessionId, manyLines)
502
+
503
+ const buffer = getBuffer(sessionId)!
504
+ expect(buffer.lines.length).toBe(MAX_BUFFER_LINES)
505
+ expect(buffer.lines[0]).toBe("line 100")
506
+ expect(buffer.lines[MAX_BUFFER_LINES - 1]).toBe(`line ${MAX_BUFFER_LINES + 99}`)
507
+ })
508
+
509
+ test("setLines replaces all lines", () => {
510
+ appendLines(sessionId, ["old 1", "old 2"])
511
+
512
+ setLines(sessionId, ["new 1", "new 2", "new 3"])
513
+
514
+ const buffer = getBuffer(sessionId)!
515
+ expect(buffer.lines).toEqual(["new 1", "new 2", "new 3"])
516
+ })
517
+
518
+ test("clearBuffer empties buffer", () => {
519
+ appendLines(sessionId, ["line 1", "line 2"])
520
+
521
+ clearBuffer(sessionId)
522
+
523
+ const buffer = getBuffer(sessionId)!
524
+ expect(buffer.lines).toEqual([])
525
+ })
526
+
527
+ test("cascade delete removes buffer when session deleted", () => {
528
+ createBuffer(sessionId)
529
+ appendLines(sessionId, ["line 1"])
530
+
531
+ expect(getBuffer(sessionId)).not.toBeNull()
532
+
533
+ deleteSession(sessionId)
534
+
535
+ expect(getBuffer(sessionId)).toBeNull()
536
+ })
537
+ })
538
+
539
+ // ============================================================================
540
+ // Transaction Tests
541
+ // ============================================================================
542
+
543
+ describe("Transactions", () => {
544
+ test("withTransaction commits on success", () => {
545
+ const result = withTransaction(() => {
546
+ createSession({
547
+ name: "session-1",
548
+ worktreePath: "/path/1",
549
+ branchName: "openswe/session-1",
550
+ })
551
+ createSession({
552
+ name: "session-2",
553
+ worktreePath: "/path/2",
554
+ branchName: "openswe/session-2",
555
+ })
556
+ return "done"
557
+ })
558
+
559
+ expect(result).toBe("done")
560
+ expect(getAllSessions().length).toBe(2)
561
+ })
562
+
563
+ test("withTransaction rolls back on error", () => {
564
+ try {
565
+ withTransaction(() => {
566
+ createSession({
567
+ name: "session-1",
568
+ worktreePath: "/path/1",
569
+ branchName: "openswe/session-1",
570
+ })
571
+ throw new Error("Rollback!")
572
+ })
573
+ } catch {
574
+ // Expected
575
+ }
576
+
577
+ expect(getAllSessions().length).toBe(0)
578
+ })
579
+ })