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