auq-mcp-server 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 (66) hide show
  1. package/LICENSE +25 -0
  2. package/README.md +176 -0
  3. package/dist/__tests__/schema-validation.test.js +137 -0
  4. package/dist/__tests__/server.integration.test.js +263 -0
  5. package/dist/add.js +1 -0
  6. package/dist/add.test.js +5 -0
  7. package/dist/bin/auq.js +245 -0
  8. package/dist/bin/test-session-menu.js +28 -0
  9. package/dist/bin/test-tabbar.js +42 -0
  10. package/dist/file-utils.js +59 -0
  11. package/dist/format/ResponseFormatter.js +206 -0
  12. package/dist/format/__tests__/ResponseFormatter.test.js +380 -0
  13. package/dist/package.json +74 -0
  14. package/dist/server.js +107 -0
  15. package/dist/session/ResponseFormatter.js +130 -0
  16. package/dist/session/SessionManager.js +474 -0
  17. package/dist/session/__tests__/ResponseFormatter.test.js +417 -0
  18. package/dist/session/__tests__/SessionManager.test.js +553 -0
  19. package/dist/session/__tests__/atomic-operations.test.js +345 -0
  20. package/dist/session/__tests__/file-watcher.test.js +311 -0
  21. package/dist/session/__tests__/workflow.integration.test.js +334 -0
  22. package/dist/session/atomic-operations.js +307 -0
  23. package/dist/session/file-watcher.js +218 -0
  24. package/dist/session/index.js +7 -0
  25. package/dist/session/types.js +20 -0
  26. package/dist/session/utils.js +125 -0
  27. package/dist/session-manager.js +171 -0
  28. package/dist/session-watcher.js +110 -0
  29. package/dist/src/__tests__/schema-validation.test.js +170 -0
  30. package/dist/src/__tests__/server.integration.test.js +274 -0
  31. package/dist/src/add.js +1 -0
  32. package/dist/src/add.test.js +5 -0
  33. package/dist/src/server.js +163 -0
  34. package/dist/src/session/ResponseFormatter.js +163 -0
  35. package/dist/src/session/SessionManager.js +572 -0
  36. package/dist/src/session/__tests__/ResponseFormatter.test.js +741 -0
  37. package/dist/src/session/__tests__/SessionManager.test.js +593 -0
  38. package/dist/src/session/__tests__/atomic-operations.test.js +346 -0
  39. package/dist/src/session/__tests__/file-watcher.test.js +311 -0
  40. package/dist/src/session/atomic-operations.js +307 -0
  41. package/dist/src/session/file-watcher.js +227 -0
  42. package/dist/src/session/index.js +7 -0
  43. package/dist/src/session/types.js +20 -0
  44. package/dist/src/session/utils.js +180 -0
  45. package/dist/src/tui/__tests__/session-watcher.test.js +368 -0
  46. package/dist/src/tui/components/AnimatedGradient.js +45 -0
  47. package/dist/src/tui/components/ConfirmationDialog.js +89 -0
  48. package/dist/src/tui/components/CustomInput.js +14 -0
  49. package/dist/src/tui/components/Footer.js +55 -0
  50. package/dist/src/tui/components/Header.js +35 -0
  51. package/dist/src/tui/components/MultiLineTextInput.js +65 -0
  52. package/dist/src/tui/components/OptionsList.js +115 -0
  53. package/dist/src/tui/components/QuestionDisplay.js +36 -0
  54. package/dist/src/tui/components/ReviewScreen.js +57 -0
  55. package/dist/src/tui/components/SessionSelectionMenu.js +151 -0
  56. package/dist/src/tui/components/StepperView.js +166 -0
  57. package/dist/src/tui/components/TabBar.js +42 -0
  58. package/dist/src/tui/components/Toast.js +19 -0
  59. package/dist/src/tui/components/WaitingScreen.js +20 -0
  60. package/dist/src/tui/session-watcher.js +195 -0
  61. package/dist/src/tui/theme.js +114 -0
  62. package/dist/src/tui/utils/gradientText.js +24 -0
  63. package/dist/tui/__tests__/session-watcher.test.js +368 -0
  64. package/dist/tui/session-watcher.js +183 -0
  65. package/package.json +78 -0
  66. package/scripts/postinstall.cjs +51 -0
@@ -0,0 +1,593 @@
1
+ /**
2
+ * Unit tests for SessionManager class
3
+ */
4
+ import { promises as fs } from "fs";
5
+ import { join } from "path";
6
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
7
+ import { SessionManager } from "../SessionManager.js";
8
+ import { getCurrentTimestamp } from "../utils.js";
9
+ describe("SessionManager", () => {
10
+ let sessionManager;
11
+ const testBaseDir = "/tmp/auq-test-sessions";
12
+ beforeEach(async () => {
13
+ // Clean up test directory before each test
14
+ await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
15
+ sessionManager = new SessionManager({
16
+ baseDir: testBaseDir,
17
+ maxSessions: 10,
18
+ sessionTimeout: 1000, // 1 second for testing
19
+ });
20
+ await sessionManager.initialize();
21
+ });
22
+ afterEach(async () => {
23
+ // Clean up test directory after each test
24
+ await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
25
+ });
26
+ describe("initialization", () => {
27
+ it("should create session directory with proper permissions", async () => {
28
+ const isValid = await fs
29
+ .access(testBaseDir)
30
+ .then(() => true)
31
+ .catch(() => false);
32
+ expect(isValid).toBe(true);
33
+ });
34
+ it("should throw error for invalid directory", async () => {
35
+ const invalidManager = new SessionManager({
36
+ baseDir: "/invalid/path/that/cannot/be/created",
37
+ });
38
+ await expect(invalidManager.initialize()).rejects.toThrow();
39
+ });
40
+ });
41
+ describe("createSession", () => {
42
+ const sampleQuestions = [
43
+ {
44
+ options: [
45
+ { description: "Dynamic scripting language", label: "JavaScript" },
46
+ { description: "Typed superset of JavaScript", label: "TypeScript" },
47
+ { description: "High-level interpreted language", label: "Python" },
48
+ ],
49
+ prompt: "Which programming language do you prefer?",
50
+ title: "Language",
51
+ },
52
+ ];
53
+ it("should create a new session with unique ID", async () => {
54
+ const sessionId = await sessionManager.createSession(sampleQuestions);
55
+ expect(sessionId).toBeDefined();
56
+ expect(typeof sessionId).toBe("string");
57
+ // Verify session directory exists
58
+ const sessionDir = `${testBaseDir}/${sessionId}`;
59
+ const stat = await fs.stat(sessionDir);
60
+ expect(stat.isDirectory()).toBe(true);
61
+ // Verify session files exist
62
+ const requestExists = await fs
63
+ .access(`${sessionDir}/request.json`)
64
+ .then(() => true)
65
+ .catch(() => false);
66
+ const statusExists = await fs
67
+ .access(`${sessionDir}/status.json`)
68
+ .then(() => true)
69
+ .catch(() => false);
70
+ expect(requestExists).toBe(true);
71
+ expect(statusExists).toBe(true);
72
+ });
73
+ it("should create sessions with different IDs", async () => {
74
+ const sessionId1 = await sessionManager.createSession(sampleQuestions);
75
+ const sessionId2 = await sessionManager.createSession(sampleQuestions);
76
+ expect(sessionId1).not.toBe(sessionId2);
77
+ });
78
+ it("should throw error for empty questions array", async () => {
79
+ await expect(sessionManager.createSession([])).rejects.toThrow("At least one question is required to create a session");
80
+ });
81
+ it("should throw error for null questions", async () => {
82
+ await expect(sessionManager.createSession(null)).rejects.toThrow("At least one question is required to create a session");
83
+ });
84
+ it("should store correct session data", async () => {
85
+ const sessionId = await sessionManager.createSession(sampleQuestions);
86
+ const request = await sessionManager.getSessionRequest(sessionId);
87
+ const status = await sessionManager.getSessionStatus(sessionId);
88
+ expect(request?.sessionId).toBe(sessionId);
89
+ expect(request?.questions).toEqual(sampleQuestions);
90
+ expect(request?.status).toBe("pending");
91
+ expect(status?.sessionId).toBe(sessionId);
92
+ expect(status?.status).toBe("pending");
93
+ expect(status?.totalQuestions).toBe(sampleQuestions.length);
94
+ expect(status?.currentQuestionIndex).toBeUndefined();
95
+ });
96
+ });
97
+ describe("sessionExists", () => {
98
+ it("should return true for existing session", async () => {
99
+ const questions = [
100
+ { options: [{ label: "Option" }], prompt: "Test", title: "Test" },
101
+ ];
102
+ const sessionId = await sessionManager.createSession(questions);
103
+ const exists = await sessionManager.sessionExists(sessionId);
104
+ expect(exists).toBe(true);
105
+ });
106
+ it("should return false for non-existing session", async () => {
107
+ const exists = await sessionManager.sessionExists("non-existent-id");
108
+ expect(exists).toBe(false);
109
+ });
110
+ });
111
+ describe("getSessionStatus", () => {
112
+ it("should return session status for existing session", async () => {
113
+ const questions = [
114
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
115
+ ];
116
+ const sessionId = await sessionManager.createSession(questions);
117
+ const status = await sessionManager.getSessionStatus(sessionId);
118
+ expect(status).toBeDefined();
119
+ expect(status?.sessionId).toBe(sessionId);
120
+ expect(status?.status).toBe("pending");
121
+ expect(status?.totalQuestions).toBe(1);
122
+ });
123
+ it("should return null for non-existing session", async () => {
124
+ const status = await sessionManager.getSessionStatus("non-existent-id");
125
+ expect(status).toBe(null);
126
+ });
127
+ });
128
+ describe("getSessionRequest", () => {
129
+ it("should return session request for existing session", async () => {
130
+ const questions = [
131
+ {
132
+ options: [{ description: "Test description", label: "Test option" }],
133
+ prompt: "Test question",
134
+ title: "Test",
135
+ },
136
+ ];
137
+ const sessionId = await sessionManager.createSession(questions);
138
+ const request = await sessionManager.getSessionRequest(sessionId);
139
+ expect(request).toBeDefined();
140
+ expect(request?.sessionId).toBe(sessionId);
141
+ expect(request?.questions).toEqual(questions);
142
+ expect(request?.status).toBe("pending");
143
+ });
144
+ it("should return null for non-existing session", async () => {
145
+ const request = await sessionManager.getSessionRequest("non-existent-id");
146
+ expect(request).toBe(null);
147
+ });
148
+ });
149
+ describe("updateSessionStatus", () => {
150
+ it("should update session status successfully", async () => {
151
+ const questions = [
152
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
153
+ ];
154
+ const sessionId = await sessionManager.createSession(questions);
155
+ // Add small delay to ensure different timestamps
156
+ await new Promise((resolve) => setTimeout(resolve, 1));
157
+ await sessionManager.updateSessionStatus(sessionId, "in-progress", {
158
+ currentQuestionIndex: 0,
159
+ });
160
+ const status = await sessionManager.getSessionStatus(sessionId);
161
+ expect(status?.status).toBe("in-progress");
162
+ expect(status?.currentQuestionIndex).toBe(0);
163
+ expect(status?.lastModified).not.toBe(status?.createdAt);
164
+ });
165
+ it("should throw error for non-existing session", async () => {
166
+ await expect(sessionManager.updateSessionStatus("non-existent-id", "completed")).rejects.toThrow("Session not found: non-existent-id");
167
+ });
168
+ });
169
+ describe("getSessionCount", () => {
170
+ it("should return 0 for no sessions", async () => {
171
+ const count = await sessionManager.getSessionCount();
172
+ expect(count).toBe(0);
173
+ });
174
+ it("should return correct count for multiple sessions", async () => {
175
+ const questions = [
176
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
177
+ ];
178
+ await sessionManager.createSession(questions);
179
+ await sessionManager.createSession(questions);
180
+ await sessionManager.createSession(questions);
181
+ const count = await sessionManager.getSessionCount();
182
+ expect(count).toBe(3);
183
+ });
184
+ });
185
+ describe("getAllSessionIds", () => {
186
+ it("should return empty array for no sessions", async () => {
187
+ const ids = await sessionManager.getAllSessionIds();
188
+ expect(ids).toEqual([]);
189
+ });
190
+ it("should return all session IDs", async () => {
191
+ const questions = [
192
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
193
+ ];
194
+ const id1 = await sessionManager.createSession(questions);
195
+ const id2 = await sessionManager.createSession(questions);
196
+ const ids = await sessionManager.getAllSessionIds();
197
+ expect(ids).toContain(id1);
198
+ expect(ids).toContain(id2);
199
+ expect(ids.length).toBe(2);
200
+ });
201
+ });
202
+ describe("isSessionLimitReached", () => {
203
+ it("should return false when under limit", async () => {
204
+ const questions = [
205
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
206
+ ];
207
+ await sessionManager.createSession(questions);
208
+ const isLimitReached = await sessionManager.isSessionLimitReached();
209
+ expect(isLimitReached).toBe(false);
210
+ });
211
+ it("should return true when at limit", async () => {
212
+ const questions = [
213
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
214
+ ];
215
+ // Create sessions up to the limit (10)
216
+ for (let i = 0; i < 10; i++) {
217
+ await sessionManager.createSession(questions);
218
+ }
219
+ const isLimitReached = await sessionManager.isSessionLimitReached();
220
+ expect(isLimitReached).toBe(true);
221
+ });
222
+ });
223
+ describe("deleteSession", () => {
224
+ it("should delete session and all files", async () => {
225
+ const questions = [
226
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
227
+ ];
228
+ const sessionId = await sessionManager.createSession(questions);
229
+ // Verify session exists
230
+ expect(await sessionManager.sessionExists(sessionId)).toBe(true);
231
+ // Delete session
232
+ await sessionManager.deleteSession(sessionId);
233
+ // Verify session is deleted
234
+ expect(await sessionManager.sessionExists(sessionId)).toBe(false);
235
+ });
236
+ });
237
+ describe("cleanupExpiredSessions", () => {
238
+ it("should clean up expired sessions based on retention period", async () => {
239
+ // Create a manager with short retention period for testing
240
+ const shortRetentionManager = new SessionManager({
241
+ baseDir: testBaseDir,
242
+ retentionPeriod: 500, // 500ms retention
243
+ });
244
+ await shortRetentionManager.initialize();
245
+ const questions = [
246
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
247
+ ];
248
+ const sessionId = await shortRetentionManager.createSession(questions);
249
+ // Wait for retention period to expire
250
+ await new Promise((resolve) => setTimeout(resolve, 600));
251
+ const cleanedCount = await shortRetentionManager.cleanupExpiredSessions();
252
+ expect(cleanedCount).toBe(1);
253
+ expect(await shortRetentionManager.sessionExists(sessionId)).toBe(false);
254
+ });
255
+ it("should not clean up active sessions", async () => {
256
+ const questions = [
257
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
258
+ ];
259
+ const sessionId = await sessionManager.createSession(questions);
260
+ // Clean up immediately (session should not be expired yet)
261
+ const cleanedCount = await sessionManager.cleanupExpiredSessions();
262
+ expect(cleanedCount).toBe(0);
263
+ expect(await sessionManager.sessionExists(sessionId)).toBe(true);
264
+ });
265
+ it("should respect retention period independently from session timeout", async () => {
266
+ // Create a new manager with short retention period (500ms) but no session timeout
267
+ const retentionManager = new SessionManager({
268
+ baseDir: testBaseDir,
269
+ retentionPeriod: 500, // 500ms retention
270
+ sessionTimeout: 0, // No timeout (infinite wait)
271
+ });
272
+ await retentionManager.initialize();
273
+ const questions = [
274
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
275
+ ];
276
+ const sessionId = await retentionManager.createSession(questions);
277
+ // Wait for retention period to expire
278
+ await new Promise((resolve) => setTimeout(resolve, 600));
279
+ const cleanedCount = await retentionManager.cleanupExpiredSessions();
280
+ expect(cleanedCount).toBe(1);
281
+ expect(await retentionManager.sessionExists(sessionId)).toBe(false);
282
+ });
283
+ it("should preserve sessions within retention period", async () => {
284
+ // Create manager with 7-day retention period (default)
285
+ const retentionManager = new SessionManager({
286
+ baseDir: testBaseDir,
287
+ retentionPeriod: 604800000, // 7 days
288
+ });
289
+ await retentionManager.initialize();
290
+ const questions = [
291
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
292
+ ];
293
+ const sessionId = await retentionManager.createSession(questions);
294
+ // Clean up immediately - session should be preserved
295
+ const cleanedCount = await retentionManager.cleanupExpiredSessions();
296
+ expect(cleanedCount).toBe(0);
297
+ expect(await retentionManager.sessionExists(sessionId)).toBe(true);
298
+ });
299
+ it("should not cleanup when retentionPeriod is 0", async () => {
300
+ // Create manager with retention disabled
301
+ const noRetentionManager = new SessionManager({
302
+ baseDir: testBaseDir,
303
+ retentionPeriod: 0, // Disabled
304
+ });
305
+ await noRetentionManager.initialize();
306
+ const questions = [
307
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
308
+ ];
309
+ const sessionId = await noRetentionManager.createSession(questions);
310
+ // Wait a bit
311
+ await new Promise((resolve) => setTimeout(resolve, 100));
312
+ // Should not clean up even old sessions
313
+ const cleanedCount = await noRetentionManager.cleanupExpiredSessions();
314
+ expect(cleanedCount).toBe(0);
315
+ expect(await noRetentionManager.sessionExists(sessionId)).toBe(true);
316
+ });
317
+ it("should use default 7-day retention period when not specified", async () => {
318
+ // Create manager without specifying retention period
319
+ const defaultManager = new SessionManager({
320
+ baseDir: testBaseDir,
321
+ });
322
+ await defaultManager.initialize();
323
+ const questions = [
324
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
325
+ ];
326
+ const sessionId = await defaultManager.createSession(questions);
327
+ // Clean up immediately - should preserve with default 7-day retention
328
+ const cleanedCount = await defaultManager.cleanupExpiredSessions();
329
+ expect(cleanedCount).toBe(0);
330
+ expect(await defaultManager.sessionExists(sessionId)).toBe(true);
331
+ });
332
+ });
333
+ describe("validateSession", () => {
334
+ it("should validate a correct session", async () => {
335
+ const questions = [
336
+ { options: [{ label: "Opt" }], prompt: "Test", title: "Test" },
337
+ ];
338
+ const sessionId = await sessionManager.createSession(questions);
339
+ const validation = await sessionManager.validateSession(sessionId);
340
+ expect(validation.isValid).toBe(true);
341
+ expect(validation.issues).toEqual([]);
342
+ });
343
+ it("should detect issues with invalid session", async () => {
344
+ const validation = await sessionManager.validateSession("non-existent");
345
+ expect(validation.isValid).toBe(false);
346
+ expect(validation.issues).toContain("Session directory does not exist");
347
+ });
348
+ });
349
+ describe("getConfig", () => {
350
+ it("should return configuration", () => {
351
+ const config = sessionManager.getConfig();
352
+ expect(config.baseDir).toBe(testBaseDir);
353
+ expect(config.maxSessions).toBe(10);
354
+ expect(config.sessionTimeout).toBe(1000);
355
+ });
356
+ });
357
+ describe("startSession - Complete Lifecycle", () => {
358
+ it("should complete full lifecycle successfully", async () => {
359
+ const questions = [
360
+ {
361
+ options: [
362
+ { description: "Dynamic web language", label: "JavaScript" },
363
+ { description: "Type-safe JavaScript", label: "TypeScript" },
364
+ ],
365
+ prompt: "What is your favorite programming language?",
366
+ title: "Language",
367
+ },
368
+ {
369
+ options: [
370
+ { description: "Web application", label: "Web" },
371
+ { description: "Command-line tool", label: "CLI" },
372
+ ],
373
+ prompt: "What type of application are you building?",
374
+ title: "App Type",
375
+ },
376
+ ];
377
+ // Start session in background
378
+ const sessionPromise = sessionManager.startSession(questions);
379
+ // Wait a bit for session files to be created
380
+ await new Promise((resolve) => setTimeout(resolve, 100));
381
+ // Get the session ID from directory listing
382
+ const entries = await fs.readdir(testBaseDir);
383
+ expect(entries.length).toBe(1);
384
+ const sessionId = entries[0];
385
+ // Simulate user submitting answers
386
+ const answers = {
387
+ answers: [
388
+ {
389
+ questionIndex: 0,
390
+ selectedOption: "TypeScript",
391
+ timestamp: getCurrentTimestamp(),
392
+ },
393
+ {
394
+ customText: "Desktop app with Electron",
395
+ questionIndex: 1,
396
+ timestamp: getCurrentTimestamp(),
397
+ },
398
+ ],
399
+ sessionId,
400
+ timestamp: getCurrentTimestamp(),
401
+ };
402
+ await sessionManager.saveSessionAnswers(sessionId, answers);
403
+ // Wait for session to complete
404
+ const result = await sessionPromise;
405
+ expect(result.sessionId).toBe(sessionId);
406
+ expect(result.formattedResponse).toContain("Here are the user's answers:");
407
+ expect(result.formattedResponse).toContain("TypeScript");
408
+ expect(result.formattedResponse).toContain("Desktop app with Electron");
409
+ // Verify final status is completed
410
+ const status = await sessionManager.getSessionStatus(sessionId);
411
+ expect(status?.status).toBe("completed");
412
+ });
413
+ it("should timeout and set status to timed_out", async () => {
414
+ // Create session manager with short timeout for testing
415
+ const shortTimeoutManager = new SessionManager({
416
+ baseDir: testBaseDir,
417
+ sessionTimeout: 500, // 500ms
418
+ });
419
+ await shortTimeoutManager.initialize();
420
+ const questions = [
421
+ {
422
+ options: [{ label: "Option 1" }],
423
+ prompt: "Test question",
424
+ title: "Test",
425
+ },
426
+ ];
427
+ // Start session but don't provide answers
428
+ await expect(shortTimeoutManager.startSession(questions)).rejects.toThrow("timed out");
429
+ // Wait a bit to ensure the status is updated
430
+ await new Promise((resolve) => setTimeout(resolve, 100));
431
+ // Verify status was set to timed_out
432
+ const entries = await fs.readdir(testBaseDir);
433
+ const sessionId = entries[entries.length - 1]; // Get the last created session
434
+ const status = await shortTimeoutManager.getSessionStatus(sessionId);
435
+ expect(status?.status).toBe("timed_out");
436
+ });
437
+ it("should handle invalid answers file", async () => {
438
+ const questions = [
439
+ {
440
+ options: [{ label: "Option 1" }],
441
+ prompt: "Test question",
442
+ title: "Test",
443
+ },
444
+ ];
445
+ // Start session in background
446
+ const sessionPromise = sessionManager.startSession(questions);
447
+ // Wait for session files to be created
448
+ await new Promise((resolve) => setTimeout(resolve, 100));
449
+ // Get session ID
450
+ const entries = await fs.readdir(testBaseDir);
451
+ const sessionId = entries[entries.length - 1];
452
+ // Write invalid answers file
453
+ const answersPath = join(testBaseDir, sessionId, "answers.json");
454
+ await fs.writeFile(answersPath, "invalid json");
455
+ // Should reject with validation error
456
+ await expect(sessionPromise).rejects.toThrow("Failed to parse JSON");
457
+ // Verify status was set to abandoned
458
+ const status = await sessionManager.getSessionStatus(sessionId);
459
+ expect(status?.status).toBe("abandoned");
460
+ });
461
+ it("should handle answer validation errors", async () => {
462
+ const questions = [
463
+ {
464
+ options: [{ label: "Option 1" }, { label: "Option 2" }],
465
+ prompt: "Test question",
466
+ title: "Test",
467
+ },
468
+ ];
469
+ // Start session in background
470
+ const sessionPromise = sessionManager.startSession(questions);
471
+ // Wait for session files to be created
472
+ await new Promise((resolve) => setTimeout(resolve, 100));
473
+ // Get session ID
474
+ const entries = await fs.readdir(testBaseDir);
475
+ const sessionId = entries[entries.length - 1];
476
+ // Submit answer with invalid option
477
+ const answers = {
478
+ answers: [
479
+ {
480
+ questionIndex: 0,
481
+ selectedOption: "Invalid Option", // Not in the options list
482
+ timestamp: getCurrentTimestamp(),
483
+ },
484
+ ],
485
+ sessionId,
486
+ timestamp: getCurrentTimestamp(),
487
+ };
488
+ await sessionManager.saveSessionAnswers(sessionId, answers);
489
+ // Should reject with validation error
490
+ await expect(sessionPromise).rejects.toThrow("Answer validation failed");
491
+ // Verify status was set to abandoned
492
+ const status = await sessionManager.getSessionStatus(sessionId);
493
+ expect(status?.status).toBe("abandoned");
494
+ });
495
+ it("should format response according to PRD specification", async () => {
496
+ const questions = [
497
+ {
498
+ options: [
499
+ { description: "The color of fire", label: "Red" },
500
+ { description: "The color of sky", label: "Blue" },
501
+ ],
502
+ prompt: "What is your favorite color?",
503
+ title: "Color",
504
+ },
505
+ ];
506
+ // Start session in background
507
+ const sessionPromise = sessionManager.startSession(questions);
508
+ // Wait for session files to be created
509
+ await new Promise((resolve) => setTimeout(resolve, 100));
510
+ // Get session ID
511
+ const entries = await fs.readdir(testBaseDir);
512
+ const sessionId = entries[entries.length - 1];
513
+ // Submit answer
514
+ const answers = {
515
+ answers: [
516
+ {
517
+ questionIndex: 0,
518
+ selectedOption: "Blue",
519
+ timestamp: getCurrentTimestamp(),
520
+ },
521
+ ],
522
+ sessionId,
523
+ timestamp: getCurrentTimestamp(),
524
+ };
525
+ await sessionManager.saveSessionAnswers(sessionId, answers);
526
+ // Wait for completion
527
+ const result = await sessionPromise;
528
+ // Verify PRD-compliant format
529
+ expect(result.formattedResponse).toBe("Here are the user's answers:\n\n" +
530
+ "1. What is your favorite color?\n" +
531
+ "→ Blue — The color of sky");
532
+ });
533
+ it.skip("should handle concurrent sessions independently", async () => {
534
+ const questions1 = [
535
+ {
536
+ options: [{ label: "Option 1" }],
537
+ prompt: "Question 1",
538
+ title: "Test 1",
539
+ },
540
+ ];
541
+ const questions2 = [
542
+ {
543
+ options: [{ label: "Option 2" }],
544
+ prompt: "Question 2",
545
+ title: "Test 2",
546
+ },
547
+ ];
548
+ // Start two sessions concurrently
549
+ const session1Promise = sessionManager.startSession(questions1);
550
+ const session2Promise = sessionManager.startSession(questions2);
551
+ // Wait for both sessions to be created
552
+ await new Promise((resolve) => setTimeout(resolve, 100));
553
+ // Get both session IDs
554
+ const entries = await fs.readdir(testBaseDir);
555
+ expect(entries.length).toBeGreaterThanOrEqual(2);
556
+ const sessionId1 = entries[entries.length - 2];
557
+ const sessionId2 = entries[entries.length - 1];
558
+ // Submit answers for both sessions
559
+ await sessionManager.saveSessionAnswers(sessionId1, {
560
+ answers: [
561
+ {
562
+ questionIndex: 0,
563
+ selectedOption: "Option 1",
564
+ timestamp: getCurrentTimestamp(),
565
+ },
566
+ ],
567
+ sessionId: sessionId1,
568
+ timestamp: getCurrentTimestamp(),
569
+ });
570
+ await sessionManager.saveSessionAnswers(sessionId2, {
571
+ answers: [
572
+ {
573
+ questionIndex: 0,
574
+ selectedOption: "Option 2",
575
+ timestamp: getCurrentTimestamp(),
576
+ },
577
+ ],
578
+ sessionId: sessionId2,
579
+ timestamp: getCurrentTimestamp(),
580
+ });
581
+ // Wait for both to complete
582
+ const [result1, result2] = await Promise.all([
583
+ session1Promise,
584
+ session2Promise,
585
+ ]);
586
+ // Verify both completed independently
587
+ expect(result1.sessionId).toBe(sessionId1);
588
+ expect(result1.formattedResponse).toContain("Option 1");
589
+ expect(result2.sessionId).toBe(sessionId2);
590
+ expect(result2.formattedResponse).toContain("Option 2");
591
+ });
592
+ });
593
+ });