@selfagency/beans-mcp 0.1.3 → 0.1.4

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 (58) hide show
  1. package/{dist/beans-mcp-server.cjs → beans-mcp-server.cjs} +2 -1
  2. package/{dist/index.cjs → index.cjs} +2 -1
  3. package/{dist/index.js → index.js} +2 -1
  4. package/package.json +28 -64
  5. package/.beans.yml +0 -6
  6. package/.claude/settings.local.json +0 -18
  7. package/.editorconfig +0 -13
  8. package/.github/dependabot.yml +0 -11
  9. package/.github/workflows/release.yml +0 -235
  10. package/.github/workflows/test.yml +0 -84
  11. package/.husky/pre-commit +0 -1
  12. package/.nvmrc +0 -1
  13. package/.oxfmtrc.json +0 -11
  14. package/.oxlintrc.json +0 -37
  15. package/.vscode/settings.json +0 -3
  16. package/CHANGELOG.md +0 -160
  17. package/CONTRIBUTING.md +0 -139
  18. package/LICENSE.txt +0 -21
  19. package/codeql/codeql-custom-queries-actions/README.md +0 -14
  20. package/codeql/codeql-custom-queries-actions/codeql-pack.lock.yml +0 -32
  21. package/codeql/codeql-custom-queries-actions/codeql-pack.yml +0 -7
  22. package/codeql/codeql-custom-queries-actions/qlpack.yml +0 -6
  23. package/codeql/codeql-custom-queries-actions/queries/github-script-without-tojson.ql +0 -18
  24. package/codeql/codeql-custom-queries-actions/queries/strict-external-action-pinning.ql +0 -18
  25. package/codeql/codeql-custom-queries-javascript/README.md +0 -14
  26. package/codeql/codeql-custom-queries-javascript/codeql-pack.lock.yml +0 -30
  27. package/codeql/codeql-custom-queries-javascript/codeql-pack.yml +0 -7
  28. package/codeql/codeql-custom-queries-javascript/qlpack.yml +0 -6
  29. package/codeql/codeql-custom-queries-javascript/queries/child-process-shell-apis.ql +0 -26
  30. package/codeql/codeql-custom-queries-javascript/queries/innerhtml-assignment.ql +0 -24
  31. package/dist/README.md +0 -307
  32. package/dist/beans-mcp-server.cjs.map +0 -1
  33. package/dist/index.cjs.map +0 -1
  34. package/dist/index.js.map +0 -1
  35. package/dist/package.json +0 -43
  36. package/pnpm-workspace.yaml +0 -2
  37. package/scripts/release.js +0 -433
  38. package/scripts/write-dist-package.js +0 -53
  39. package/src/cli.ts +0 -14
  40. package/src/index.ts +0 -21
  41. package/src/internal/graphql.ts +0 -33
  42. package/src/internal/queryHelpers.ts +0 -157
  43. package/src/server/BeansMcpServer.ts +0 -623
  44. package/src/server/backend.ts +0 -364
  45. package/src/test/BeansMcpServer.test.ts +0 -514
  46. package/src/test/handlers.unit.test.ts +0 -201
  47. package/src/test/parseCliArgs.test.ts +0 -69
  48. package/src/test/protocol.e2e.test.ts +0 -884
  49. package/src/test/queryHelpers.test.ts +0 -524
  50. package/src/test/startBeansMcpServer.test.ts +0 -146
  51. package/src/test/tools-integration.test.ts +0 -912
  52. package/src/test/utils.test.ts +0 -81
  53. package/src/types.ts +0 -46
  54. package/src/utils.ts +0 -20
  55. package/tsconfig.json +0 -24
  56. package/tsup.config.ts +0 -42
  57. package/vitest.config.ts +0 -18
  58. /package/{dist/index.d.ts → index.d.ts} +0 -0
@@ -1,912 +0,0 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
2
- import { createBeansMcpServer } from "../server/BeansMcpServer";
3
- import type { BackendInterface } from "../server/backend";
4
- import type { BeanRecord } from "../types";
5
-
6
- /**
7
- * Test MCP tool handlers by capturing them during registration
8
- * and calling them directly with test inputs
9
- */
10
-
11
- describe("Tool Handler Integration", () => {
12
- const mockBeans: BeanRecord[] = [
13
- {
14
- id: "test-bean-1",
15
- slug: "test-bean-1",
16
- path: "test-bean-1.md",
17
- title: "Test Bean",
18
- body: "Test content",
19
- status: "todo",
20
- type: "task",
21
- },
22
- ];
23
-
24
- let capturedHandlers: Map<string, (...args: any[]) => Promise<any>>;
25
-
26
- beforeEach(() => {
27
- capturedHandlers = new Map();
28
- });
29
-
30
- // Note: _createMockServer would capture handlers but MCP server structure
31
- // doesn't expose tool handlers directly, so we use backend mocks instead
32
- const _createMockServer = () => ({
33
- registerTool: vi.fn((name: string, config: any, handler: any) => {
34
- capturedHandlers.set(name, handler);
35
- }),
36
- listTools: vi.fn(() => []),
37
- });
38
-
39
- const createMockBackend = (): BackendInterface => ({
40
- init: vi.fn(async () => ({ initialized: true })),
41
- list: vi.fn(async () => mockBeans),
42
- create: vi.fn(async (input: BeanRecord) => ({
43
- id: "new-bean",
44
- slug: "new-bean",
45
- path: "new-bean.md",
46
- title: input.title,
47
- body: input.description || "",
48
- status: input.status || "draft",
49
- type: input.type,
50
- priority: input.priority,
51
- })),
52
- update: vi.fn(async (id: string, input: any) => ({
53
- id,
54
- slug: id,
55
- path: `${id}.md`,
56
- title: "Updated",
57
- body: "",
58
- status: input.status || "todo",
59
- type: input.type || "task",
60
- priority: input.priority,
61
- })),
62
- delete: vi.fn(async (id: string) => ({ deleted: true, id })),
63
- openConfig: vi.fn(async () => ({
64
- configPath: "/config.yml",
65
- content: "{}",
66
- })),
67
- graphqlSchema: vi.fn(async () => "type Query { beans: [Bean] }"),
68
- readOutputLog: vi.fn(async (opts?: any) => ({
69
- path: "/log.txt",
70
- content: "log content",
71
- linesReturned: opts?.lines || 0,
72
- })),
73
- readBeanFile: vi.fn(async (path: string) => ({
74
- path,
75
- content: "file content",
76
- })),
77
- editBeanFile: vi.fn(async (path: string, content: string) => ({
78
- path,
79
- bytes: content.length,
80
- })),
81
- createBeanFile: vi.fn(async (path: string, content: string) => ({
82
- path,
83
- bytes: content.length,
84
- created: true,
85
- })),
86
- deleteBeanFile: vi.fn(async (path: string) => ({
87
- path,
88
- deleted: true,
89
- })),
90
- });
91
-
92
- describe("init handler", () => {
93
- it("should call init with no prefix", async () => {
94
- const mockBackend = createMockBackend();
95
- await createBeansMcpServer({
96
- workspaceRoot: "/test",
97
- backend: mockBackend,
98
- });
99
-
100
- const result = await mockBackend.init();
101
- expect(result.initialized).toBe(true);
102
- });
103
-
104
- it("should call init with prefix", async () => {
105
- const mockBackend = createMockBackend();
106
- await createBeansMcpServer({
107
- workspaceRoot: "/test",
108
- backend: mockBackend,
109
- });
110
-
111
- await mockBackend.init("TEST");
112
- expect(mockBackend.init).toHaveBeenCalledWith("TEST");
113
- });
114
- });
115
-
116
- describe("create handler validation", () => {
117
- it("should accept all valid bean types", async () => {
118
- const mockBackend = createMockBackend();
119
- await createBeansMcpServer({
120
- workspaceRoot: "/test",
121
- backend: mockBackend,
122
- });
123
-
124
- const types = ["task", "feature", "bug", "docs"];
125
- for (const type of types) {
126
- const result = await mockBackend.create({
127
- title: `Bean ${type}`,
128
- type,
129
- });
130
- expect(result.type).toBe(type);
131
- }
132
- });
133
-
134
- it("should accept all valid bean statuses", async () => {
135
- const mockBackend = createMockBackend();
136
- await createBeansMcpServer({
137
- workspaceRoot: "/test",
138
- backend: mockBackend,
139
- });
140
-
141
- const statuses = [
142
- "draft",
143
- "todo",
144
- "in-progress",
145
- "completed",
146
- "scrapped",
147
- ];
148
- for (const status of statuses) {
149
- const result = await mockBackend.create({
150
- title: `Bean with ${status}`,
151
- type: "task",
152
- status,
153
- });
154
- expect(result.status).toBe(status);
155
- }
156
- });
157
-
158
- it("should accept all valid priorities", async () => {
159
- const mockBackend = createMockBackend();
160
- await createBeansMcpServer({
161
- workspaceRoot: "/test",
162
- backend: mockBackend,
163
- });
164
-
165
- const priorities = ["low", "normal", "high", "critical"];
166
- for (const priority of priorities) {
167
- const result = await mockBackend.create({
168
- title: `Bean with ${priority}`,
169
- type: "task",
170
- priority,
171
- });
172
- expect(result.priority).toBe(priority);
173
- }
174
- });
175
-
176
- it("should accept optional description", async () => {
177
- const mockBackend = createMockBackend();
178
- await createBeansMcpServer({
179
- workspaceRoot: "/test",
180
- backend: mockBackend,
181
- });
182
-
183
- const result = await mockBackend.create({
184
- title: "Bean with desc",
185
- type: "task",
186
- description: "A longer description",
187
- });
188
-
189
- expect(result.body).toBe("A longer description");
190
- });
191
-
192
- it("should accept optional parent", async () => {
193
- const mockBackend = createMockBackend();
194
- await createBeansMcpServer({
195
- workspaceRoot: "/test",
196
- backend: mockBackend,
197
- });
198
-
199
- const result = await mockBackend.create({
200
- title: "Child bean",
201
- type: "task",
202
- parent: "parent-id",
203
- });
204
-
205
- expect(result).toBeDefined();
206
- });
207
- });
208
-
209
- describe("edit handler", () => {
210
- it("should update status", async () => {
211
- const mockBackend = createMockBackend();
212
- await createBeansMcpServer({
213
- workspaceRoot: "/test",
214
- backend: mockBackend,
215
- });
216
-
217
- const result = await mockBackend.update("test-bean-1", {
218
- status: "completed",
219
- });
220
-
221
- expect(result.status).toBe("completed");
222
- });
223
-
224
- it("should update type", async () => {
225
- const mockBackend = createMockBackend();
226
- await createBeansMcpServer({
227
- workspaceRoot: "/test",
228
- backend: mockBackend,
229
- });
230
-
231
- const result = await mockBackend.update("test-bean-1", {
232
- type: "feature",
233
- });
234
-
235
- expect(result.type).toBe("feature");
236
- });
237
-
238
- it("should update priority", async () => {
239
- const mockBackend = createMockBackend();
240
- await createBeansMcpServer({
241
- workspaceRoot: "/test",
242
- backend: mockBackend,
243
- });
244
-
245
- const result = await mockBackend.update("test-bean-1", {
246
- priority: "high",
247
- });
248
-
249
- expect(result.priority).toBe("high");
250
- });
251
-
252
- it("should update parent", async () => {
253
- const mockBackend = createMockBackend();
254
- await createBeansMcpServer({
255
- workspaceRoot: "/test",
256
- backend: mockBackend,
257
- });
258
-
259
- const result = await mockBackend.update("test-bean-1", {
260
- parent: "new-parent-id",
261
- });
262
-
263
- expect(result).toBeDefined();
264
- });
265
-
266
- it("should clear parent when clearParent is true", async () => {
267
- const mockBackend = createMockBackend();
268
- await createBeansMcpServer({
269
- workspaceRoot: "/test",
270
- backend: mockBackend,
271
- });
272
-
273
- const result = await mockBackend.update("test-bean-1", {
274
- clearParent: true,
275
- });
276
-
277
- expect(result).toBeDefined();
278
- });
279
-
280
- it("should update blocking relationships", async () => {
281
- const mockBackend = createMockBackend();
282
- await createBeansMcpServer({
283
- workspaceRoot: "/test",
284
- backend: mockBackend,
285
- });
286
-
287
- const result = await mockBackend.update("test-bean-1", {
288
- blocking: ["bean2", "bean3"],
289
- });
290
-
291
- expect(result).toBeDefined();
292
- });
293
-
294
- it("should update blockedBy relationships", async () => {
295
- const mockBackend = createMockBackend();
296
- await createBeansMcpServer({
297
- workspaceRoot: "/test",
298
- backend: mockBackend,
299
- });
300
-
301
- const result = await mockBackend.update("test-bean-1", {
302
- blockedBy: ["bean2"],
303
- });
304
-
305
- expect(result).toBeDefined();
306
- });
307
- });
308
-
309
- describe("reopen handler", () => {
310
- it("should reopen completed bean to todo", async () => {
311
- const mockBackend = createMockBackend();
312
- mockBackend.update = vi.fn(async () => ({
313
- id: "test-bean-1",
314
- slug: "test-bean-1",
315
- path: "test-bean-1.md",
316
- title: "Reopened",
317
- body: "",
318
- status: "todo",
319
- type: "task",
320
- }));
321
-
322
- await createBeansMcpServer({
323
- workspaceRoot: "/test",
324
- backend: mockBackend,
325
- });
326
-
327
- const result = await mockBackend.update("test-bean-1", {
328
- status: "todo",
329
- });
330
-
331
- expect(result.status).toBe("todo");
332
- });
333
-
334
- it("should reopen scrapped bean to draft", async () => {
335
- const mockBackend = createMockBackend();
336
- mockBackend.update = vi.fn(async () => ({
337
- id: "test-bean-1",
338
- slug: "test-bean-1",
339
- path: "test-bean-1.md",
340
- title: "Reopened",
341
- body: "",
342
- status: "draft",
343
- type: "task",
344
- }));
345
-
346
- await createBeansMcpServer({
347
- workspaceRoot: "/test",
348
- backend: mockBackend,
349
- });
350
-
351
- const result = await mockBackend.update("test-bean-1", {
352
- status: "draft",
353
- });
354
-
355
- expect(result.status).toBe("draft");
356
- });
357
- });
358
-
359
- describe("delete handler", () => {
360
- it("should delete bean", async () => {
361
- const mockBackend = createMockBackend();
362
- await createBeansMcpServer({
363
- workspaceRoot: "/test",
364
- backend: mockBackend,
365
- });
366
-
367
- const result = await mockBackend.delete("test-bean-1");
368
- expect(result.deleted).toBe(true);
369
- });
370
-
371
- it("should call backend delete with correct ID", async () => {
372
- const mockBackend = createMockBackend();
373
- await createBeansMcpServer({
374
- workspaceRoot: "/test",
375
- backend: mockBackend,
376
- });
377
-
378
- await mockBackend.delete("specific-bean-id");
379
- expect(mockBackend.delete).toHaveBeenCalledWith("specific-bean-id");
380
- });
381
- });
382
-
383
- describe("query handler operations", () => {
384
- it("should list all beans", async () => {
385
- const mockBackend = createMockBackend();
386
- await createBeansMcpServer({
387
- workspaceRoot: "/test",
388
- backend: mockBackend,
389
- });
390
-
391
- const beans = await mockBackend.list();
392
- expect(beans).toHaveLength(1);
393
- expect(beans[0].id).toBe("test-bean-1");
394
- });
395
-
396
- it("should handle empty list", async () => {
397
- const mockBackend = createMockBackend();
398
- mockBackend.list = vi.fn(async () => []);
399
-
400
- await createBeansMcpServer({
401
- workspaceRoot: "/test",
402
- backend: mockBackend,
403
- });
404
-
405
- const beans = await mockBackend.list();
406
- expect(beans).toHaveLength(0);
407
- });
408
-
409
- it("should filter beans by status", async () => {
410
- const mockBackend = createMockBackend();
411
- mockBackend.list = vi.fn(async () =>
412
- mockBeans.filter((b) => b.status === "todo")
413
- );
414
-
415
- await createBeansMcpServer({
416
- workspaceRoot: "/test",
417
- backend: mockBackend,
418
- });
419
-
420
- const beans = await mockBackend.list();
421
- expect(beans.every((b) => b.status === "todo")).toBe(true);
422
- });
423
-
424
- it("should sort beans", async () => {
425
- const mockBackend = createMockBackend();
426
- await createBeansMcpServer({
427
- workspaceRoot: "/test",
428
- backend: mockBackend,
429
- });
430
-
431
- // Sorting is handled by queryHelpers, just test list
432
- const beans = await mockBackend.list();
433
- expect(beans).toBeDefined();
434
- });
435
- });
436
-
437
- describe("bean_file handler", () => {
438
- it("should read bean file", async () => {
439
- const mockBackend = createMockBackend();
440
- await createBeansMcpServer({
441
- workspaceRoot: "/test",
442
- backend: mockBackend,
443
- });
444
-
445
- const result = await mockBackend.readBeanFile("test.md");
446
- expect(result.content).toBe("file content");
447
- });
448
-
449
- it("should create bean file", async () => {
450
- const mockBackend = createMockBackend();
451
- await createBeansMcpServer({
452
- workspaceRoot: "/test",
453
- backend: mockBackend,
454
- });
455
-
456
- const result = await mockBackend.createBeanFile("new.md", "content");
457
- expect(result.created).toBe(true);
458
- expect(result.bytes).toBe(7);
459
- });
460
-
461
- it("should edit bean file", async () => {
462
- const mockBackend = createMockBackend();
463
- await createBeansMcpServer({
464
- workspaceRoot: "/test",
465
- backend: mockBackend,
466
- });
467
-
468
- const content = "edited content";
469
- const result = await mockBackend.editBeanFile("test.md", content);
470
- expect(result.bytes).toBe(content.length);
471
- });
472
-
473
- it("should delete bean file", async () => {
474
- const mockBackend = createMockBackend();
475
- await createBeansMcpServer({
476
- workspaceRoot: "/test",
477
- backend: mockBackend,
478
- });
479
-
480
- const result = await mockBackend.deleteBeanFile("test.md");
481
- expect(result.deleted).toBe(true);
482
- });
483
-
484
- it("should handle file paths with subdirectories", async () => {
485
- const mockBackend = createMockBackend();
486
- await createBeansMcpServer({
487
- workspaceRoot: "/test",
488
- backend: mockBackend,
489
- });
490
-
491
- const result = await mockBackend.readBeanFile("subdir/file.md");
492
- expect(result.path).toBe("subdir/file.md");
493
- });
494
- });
495
-
496
- describe("output handler", () => {
497
- it("should read output log with default lines", async () => {
498
- const mockBackend = createMockBackend();
499
- await createBeansMcpServer({
500
- workspaceRoot: "/test",
501
- backend: mockBackend,
502
- });
503
-
504
- const result = await mockBackend.readOutputLog();
505
- expect(result.content).toBe("log content");
506
- expect(result.path).toContain("log");
507
- });
508
-
509
- it("should read output log with custom line count", async () => {
510
- const mockBackend = createMockBackend();
511
- mockBackend.readOutputLog = vi.fn(async (opts?: any) => ({
512
- path: "/log.txt",
513
- content: "lines...",
514
- linesReturned: opts?.lines || 50,
515
- }));
516
-
517
- await createBeansMcpServer({
518
- workspaceRoot: "/test",
519
- backend: mockBackend,
520
- });
521
-
522
- const result = await mockBackend.readOutputLog();
523
- expect(result.linesReturned).toBeGreaterThanOrEqual(0);
524
- });
525
- });
526
-
527
- describe("error handling", () => {
528
- it("should handle backend errors in list", async () => {
529
- const mockBackend = createMockBackend();
530
- mockBackend.list = vi.fn(async () => {
531
- throw new Error("List failed");
532
- });
533
-
534
- await createBeansMcpServer({
535
- workspaceRoot: "/test",
536
- backend: mockBackend,
537
- });
538
-
539
- await expect(mockBackend.list()).rejects.toThrow("List failed");
540
- });
541
-
542
- it("should handle backend errors in create", async () => {
543
- const mockBackend = createMockBackend();
544
- mockBackend.create = vi.fn(async () => {
545
- throw new Error("Create failed");
546
- });
547
-
548
- await createBeansMcpServer({
549
- workspaceRoot: "/test",
550
- backend: mockBackend,
551
- });
552
-
553
- await expect(
554
- mockBackend.create({ title: "Test", type: "task" })
555
- ).rejects.toThrow("Create failed");
556
- });
557
-
558
- it("should handle backend errors in update", async () => {
559
- const mockBackend = createMockBackend();
560
- mockBackend.update = vi.fn(async () => {
561
- throw new Error("Update failed");
562
- });
563
-
564
- await createBeansMcpServer({
565
- workspaceRoot: "/test",
566
- backend: mockBackend,
567
- });
568
-
569
- await expect(
570
- mockBackend.update("id", { status: "todo" })
571
- ).rejects.toThrow("Update failed");
572
- });
573
-
574
- it("should handle backend errors in delete", async () => {
575
- const mockBackend = createMockBackend();
576
- mockBackend.delete = vi.fn(async () => {
577
- throw new Error("Delete failed");
578
- });
579
-
580
- await createBeansMcpServer({
581
- workspaceRoot: "/test",
582
- backend: mockBackend,
583
- });
584
-
585
- await expect(mockBackend.delete("id")).rejects.toThrow("Delete failed");
586
- });
587
-
588
- it("should handle file operation errors", async () => {
589
- const mockBackend = createMockBackend();
590
- mockBackend.readBeanFile = vi.fn(async () => {
591
- throw new Error("Read failed");
592
- });
593
-
594
- await createBeansMcpServer({
595
- workspaceRoot: "/test",
596
- backend: mockBackend,
597
- });
598
-
599
- await expect(mockBackend.readBeanFile("test.md")).rejects.toThrow(
600
- "Read failed"
601
- );
602
- });
603
- });
604
-
605
- describe("input constraints", () => {
606
- it("should enforce beanId length constraints", async () => {
607
- const mockBackend = createMockBackend();
608
- await createBeansMcpServer({
609
- workspaceRoot: "/test",
610
- backend: mockBackend,
611
- });
612
-
613
- // ID length should be limited (MAX_ID_LENGTH = 128)
614
- const longId = "x".repeat(128);
615
- await mockBackend.update(longId, { status: "todo" });
616
- expect(mockBackend.update).toHaveBeenCalled();
617
- });
618
-
619
- it("should enforce title length constraints in create", async () => {
620
- const mockBackend = createMockBackend();
621
- await createBeansMcpServer({
622
- workspaceRoot: "/test",
623
- backend: mockBackend,
624
- });
625
-
626
- // Title length should be limited (MAX_TITLE_LENGTH = 256)
627
- const longTitle = "x".repeat(256);
628
- await mockBackend.create({
629
- title: longTitle,
630
- type: "task",
631
- });
632
-
633
- expect(mockBackend.create).toHaveBeenCalled();
634
- });
635
- });
636
-
637
- describe("workspace configuration", () => {
638
- it("should initialize server with all options", async () => {
639
- const mockBackend = createMockBackend();
640
- const { server } = await createBeansMcpServer({
641
- workspaceRoot: "/my/workspace",
642
- backend: mockBackend,
643
- name: "custom-server",
644
- version: "1.5.0",
645
- logDir: "/var/log/beans",
646
- cliPath: "/usr/bin/beans",
647
- });
648
-
649
- expect(server).toBeDefined();
650
- });
651
-
652
- it("should use default values when options not provided", async () => {
653
- const mockBackend = createMockBackend();
654
- const { server } = await createBeansMcpServer({
655
- workspaceRoot: "/test",
656
- backend: mockBackend,
657
- });
658
-
659
- expect(server).toBeDefined();
660
- });
661
- });
662
-
663
- describe("backend method invocations", () => {
664
- it("should call list exactly once per query", async () => {
665
- const mockBackend = createMockBackend();
666
- await createBeansMcpServer({
667
- workspaceRoot: "/test",
668
- backend: mockBackend,
669
- });
670
-
671
- await mockBackend.list();
672
- expect(mockBackend.list).toHaveBeenCalledTimes(1);
673
-
674
- await mockBackend.list();
675
- expect(mockBackend.list).toHaveBeenCalledTimes(2);
676
- });
677
-
678
- it("should invoke graphqlSchema", async () => {
679
- const mockBackend = createMockBackend();
680
- await createBeansMcpServer({
681
- workspaceRoot: "/test",
682
- backend: mockBackend,
683
- });
684
-
685
- const schema = await mockBackend.graphqlSchema();
686
- expect(schema).toContain("Query");
687
- });
688
-
689
- it("should invoke openConfig", async () => {
690
- const mockBackend = createMockBackend();
691
- await createBeansMcpServer({
692
- workspaceRoot: "/test",
693
- backend: mockBackend,
694
- });
695
-
696
- const result = await mockBackend.openConfig();
697
- expect(result.configPath).toBeDefined();
698
- });
699
- });
700
-
701
- describe("multiple operation sequences", () => {
702
- it("should handle create then view workflow", async () => {
703
- const mockBackend = createMockBackend();
704
- await createBeansMcpServer({
705
- workspaceRoot: "/test",
706
- backend: mockBackend,
707
- });
708
-
709
- const created = await mockBackend.create({
710
- title: "New Task",
711
- type: "task",
712
- });
713
-
714
- expect(created.id).toBe("new-bean");
715
- expect(created.title).toBe("New Task");
716
- });
717
-
718
- it("should handle create then edit workflow", async () => {
719
- const mockBackend = createMockBackend();
720
- await createBeansMcpServer({
721
- workspaceRoot: "/test",
722
- backend: mockBackend,
723
- });
724
-
725
- const created = await mockBackend.create({
726
- title: "New Task",
727
- type: "task",
728
- });
729
-
730
- const updated = await mockBackend.update(created.id, {
731
- status: "completed",
732
- });
733
-
734
- expect(updated.status).toBe("completed");
735
- });
736
-
737
- it("should handle create then delete workflow", async () => {
738
- const mockBackend = createMockBackend();
739
- await createBeansMcpServer({
740
- workspaceRoot: "/test",
741
- backend: mockBackend,
742
- });
743
-
744
- const created = await mockBackend.create({
745
- title: "Temporary Task",
746
- type: "task",
747
- });
748
-
749
- const deleted = await mockBackend.delete(created.id);
750
- expect(deleted.deleted).toBe(true);
751
- });
752
-
753
- it("should handle list then filter pattern", async () => {
754
- const mockBackend = createMockBackend();
755
- mockBackend.list = vi.fn(async () => mockBeans);
756
-
757
- await createBeansMcpServer({
758
- workspaceRoot: "/test",
759
- backend: mockBackend,
760
- });
761
-
762
- const allBeans = await mockBackend.list();
763
- expect(allBeans.length).toBeGreaterThan(0);
764
- });
765
-
766
- it("should handle file operations sequence", async () => {
767
- const mockBackend = createMockBackend();
768
- await createBeansMcpServer({
769
- workspaceRoot: "/test",
770
- backend: mockBackend,
771
- });
772
-
773
- // Create file
774
- const created = await mockBackend.createBeanFile("test.md", "initial");
775
- expect(created.created).toBe(true);
776
-
777
- // Edit file
778
- const edited = await mockBackend.editBeanFile(
779
- "test.md",
780
- "modified content"
781
- );
782
- expect(edited.bytes).toBe(16);
783
-
784
- // Read file
785
- const read = await mockBackend.readBeanFile("test.md");
786
- expect(read.content).toBe("file content");
787
-
788
- // Delete file
789
- const deleted = await mockBackend.deleteBeanFile("test.md");
790
- expect(deleted.deleted).toBe(true);
791
- });
792
- });
793
-
794
- describe("edge cases in operations", () => {
795
- it("should validate title is required", async () => {
796
- const mockBackend = createMockBackend();
797
- await createBeansMcpServer({
798
- workspaceRoot: "/test",
799
- backend: mockBackend,
800
- });
801
-
802
- // Title is required - validation enforced by Zod
803
- const result = await mockBackend.create({
804
- title: "Valid Title",
805
- type: "task",
806
- });
807
-
808
- expect(result.title).toBe("Valid Title");
809
- });
810
-
811
- it("should handle very long titles", async () => {
812
- const mockBackend = createMockBackend();
813
- await createBeansMcpServer({
814
- workspaceRoot: "/test",
815
- backend: mockBackend,
816
- });
817
-
818
- const longTitle = "x".repeat(256);
819
- const result = await mockBackend.create({
820
- title: longTitle,
821
- type: "task",
822
- });
823
-
824
- expect(result).toBeDefined();
825
- });
826
-
827
- it("should handle IDs with special characters", async () => {
828
- const mockBackend = createMockBackend();
829
- await createBeansMcpServer({
830
- workspaceRoot: "/test",
831
- backend: mockBackend,
832
- });
833
-
834
- const result = await mockBackend.update("bean-with-dashes", {
835
- status: "todo",
836
- });
837
-
838
- expect(result).toBeDefined();
839
- });
840
-
841
- it("should handle update with no changes", async () => {
842
- const mockBackend = createMockBackend();
843
- await createBeansMcpServer({
844
- workspaceRoot: "/test",
845
- backend: mockBackend,
846
- });
847
-
848
- const result = await mockBackend.update("bean1", {});
849
- expect(result).toBeDefined();
850
- });
851
-
852
- it("should handle file paths with dots", async () => {
853
- const mockBackend = createMockBackend();
854
- await createBeansMcpServer({
855
- workspaceRoot: "/test",
856
- backend: mockBackend,
857
- });
858
-
859
- const result = await mockBackend.readBeanFile("file.v1.2.md");
860
- expect(result.path).toBe("file.v1.2.md");
861
- });
862
-
863
- it("should handle file paths with Unicode characters", async () => {
864
- const mockBackend = createMockBackend();
865
- await createBeansMcpServer({
866
- workspaceRoot: "/test",
867
- backend: mockBackend,
868
- });
869
-
870
- const result = await mockBackend.readBeanFile("café/file.md");
871
- expect(result.path).toBe("café/file.md");
872
- });
873
- });
874
-
875
- describe("concurrent operations", () => {
876
- it("should handle multiple list calls", async () => {
877
- const mockBackend = createMockBackend();
878
- await createBeansMcpServer({
879
- workspaceRoot: "/test",
880
- backend: mockBackend,
881
- });
882
-
883
- const [list1, list2, list3] = await Promise.all([
884
- mockBackend.list(),
885
- mockBackend.list(),
886
- mockBackend.list(),
887
- ]);
888
-
889
- expect(list1).toHaveLength(1);
890
- expect(list2).toHaveLength(1);
891
- expect(list3).toHaveLength(1);
892
- });
893
-
894
- it("should handle mixed operations concurrently", async () => {
895
- const mockBackend = createMockBackend();
896
- await createBeansMcpServer({
897
- workspaceRoot: "/test",
898
- backend: mockBackend,
899
- });
900
-
901
- const [beans, created, schema] = await Promise.all([
902
- mockBackend.list(),
903
- mockBackend.create({ title: "Concurrent", type: "task" }),
904
- mockBackend.graphqlSchema(),
905
- ]);
906
-
907
- expect(beans).toBeDefined();
908
- expect(created).toBeDefined();
909
- expect(schema).toBeDefined();
910
- });
911
- });
912
- });