@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.
- package/AGENTS.md +203 -0
- package/CLAUDE.md +203 -0
- package/README.md +166 -0
- package/bun.lock +447 -0
- package/bunfig.toml +4 -0
- package/package.json +42 -0
- package/src/app.tsx +84 -0
- package/src/components/App.tsx +526 -0
- package/src/components/ConfirmDialog.tsx +88 -0
- package/src/components/Footer.tsx +50 -0
- package/src/components/HelpModal.tsx +136 -0
- package/src/components/IssueSelectorModal.tsx +701 -0
- package/src/components/ManualSessionModal.tsx +191 -0
- package/src/components/PhaseProgress.tsx +45 -0
- package/src/components/Preview.tsx +249 -0
- package/src/components/ProviderSwitcherModal.tsx +156 -0
- package/src/components/ScrollableText.tsx +120 -0
- package/src/components/SessionCard.tsx +60 -0
- package/src/components/SessionList.tsx +79 -0
- package/src/components/SessionTerminal.tsx +89 -0
- package/src/components/StatusBar.tsx +84 -0
- package/src/components/ThemeSwitcherModal.tsx +237 -0
- package/src/components/index.ts +58 -0
- package/src/components/session-utils.ts +337 -0
- package/src/components/theme.ts +206 -0
- package/src/components/types.ts +215 -0
- package/src/config/defaults.ts +44 -0
- package/src/config/env.ts +67 -0
- package/src/config/global.ts +252 -0
- package/src/config/index.ts +171 -0
- package/src/config/types.ts +131 -0
- package/src/core/.gitkeep +0 -0
- package/src/core/index.ts +5 -0
- package/src/core/parser.ts +62 -0
- package/src/core/process-manager.ts +52 -0
- package/src/core/session.ts +423 -0
- package/src/core/tmux.ts +206 -0
- package/src/git/.gitkeep +0 -0
- package/src/git/index.ts +8 -0
- package/src/git/repo.ts +443 -0
- package/src/git/worktree.ts +317 -0
- package/src/github/.gitkeep +0 -0
- package/src/github/client.ts +208 -0
- package/src/github/index.ts +8 -0
- package/src/github/issues.ts +351 -0
- package/src/index.ts +369 -0
- package/src/prompts/.gitkeep +0 -0
- package/src/prompts/index.ts +1 -0
- package/src/prompts/swe-system.ts +22 -0
- package/src/providers/claude.ts +103 -0
- package/src/providers/index.ts +21 -0
- package/src/providers/opencode.ts +98 -0
- package/src/providers/registry.ts +53 -0
- package/src/providers/types.ts +117 -0
- package/src/store/buffers.ts +234 -0
- package/src/store/db.test.ts +579 -0
- package/src/store/db.ts +249 -0
- package/src/store/index.ts +101 -0
- package/src/store/project.ts +119 -0
- package/src/store/schema.sql +71 -0
- package/src/store/sessions.ts +454 -0
- package/src/store/types.ts +194 -0
- package/src/theme/context.tsx +170 -0
- package/src/theme/custom.ts +134 -0
- package/src/theme/index.ts +58 -0
- package/src/theme/loader.ts +264 -0
- package/src/theme/themes/aura.json +69 -0
- package/src/theme/themes/ayu.json +80 -0
- package/src/theme/themes/carbonfox.json +248 -0
- package/src/theme/themes/catppuccin-frappe.json +233 -0
- package/src/theme/themes/catppuccin-macchiato.json +233 -0
- package/src/theme/themes/catppuccin.json +112 -0
- package/src/theme/themes/cobalt2.json +228 -0
- package/src/theme/themes/cursor.json +249 -0
- package/src/theme/themes/dracula.json +219 -0
- package/src/theme/themes/everforest.json +241 -0
- package/src/theme/themes/flexoki.json +237 -0
- package/src/theme/themes/github.json +233 -0
- package/src/theme/themes/gruvbox.json +242 -0
- package/src/theme/themes/kanagawa.json +77 -0
- package/src/theme/themes/lucent-orng.json +237 -0
- package/src/theme/themes/material.json +235 -0
- package/src/theme/themes/matrix.json +77 -0
- package/src/theme/themes/mercury.json +252 -0
- package/src/theme/themes/monokai.json +221 -0
- package/src/theme/themes/nightowl.json +221 -0
- package/src/theme/themes/nord.json +223 -0
- package/src/theme/themes/one-dark.json +84 -0
- package/src/theme/themes/opencode.json +245 -0
- package/src/theme/themes/orng.json +249 -0
- package/src/theme/themes/osaka-jade.json +93 -0
- package/src/theme/themes/palenight.json +222 -0
- package/src/theme/themes/rosepine.json +234 -0
- package/src/theme/themes/solarized.json +223 -0
- package/src/theme/themes/synthwave84.json +226 -0
- package/src/theme/themes/tokyonight.json +243 -0
- package/src/theme/themes/vercel.json +245 -0
- package/src/theme/themes/vesper.json +218 -0
- package/src/theme/themes/zenburn.json +223 -0
- package/src/theme/types.ts +225 -0
- package/src/types/sql.d.ts +4 -0
- package/src/utils/ansi-parser.ts +225 -0
- package/src/utils/format.ts +46 -0
- package/src/utils/id.ts +15 -0
- package/src/utils/logger.ts +112 -0
- package/src/utils/prerequisites.ts +118 -0
- package/src/utils/shell.ts +9 -0
- package/src/wizard/flows.ts +419 -0
- package/src/wizard/index.ts +37 -0
- package/src/wizard/prompts.ts +190 -0
- package/src/workspace/detect.test.ts +51 -0
- package/src/workspace/detect.ts +223 -0
- package/src/workspace/index.ts +71 -0
- package/src/workspace/init.ts +131 -0
- package/src/workspace/paths.ts +143 -0
- package/src/workspace/project.ts +164 -0
- 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
|
+
})
|