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.
- package/LICENSE +25 -0
- package/README.md +176 -0
- package/dist/__tests__/schema-validation.test.js +137 -0
- package/dist/__tests__/server.integration.test.js +263 -0
- package/dist/add.js +1 -0
- package/dist/add.test.js +5 -0
- package/dist/bin/auq.js +245 -0
- package/dist/bin/test-session-menu.js +28 -0
- package/dist/bin/test-tabbar.js +42 -0
- package/dist/file-utils.js +59 -0
- package/dist/format/ResponseFormatter.js +206 -0
- package/dist/format/__tests__/ResponseFormatter.test.js +380 -0
- package/dist/package.json +74 -0
- package/dist/server.js +107 -0
- package/dist/session/ResponseFormatter.js +130 -0
- package/dist/session/SessionManager.js +474 -0
- package/dist/session/__tests__/ResponseFormatter.test.js +417 -0
- package/dist/session/__tests__/SessionManager.test.js +553 -0
- package/dist/session/__tests__/atomic-operations.test.js +345 -0
- package/dist/session/__tests__/file-watcher.test.js +311 -0
- package/dist/session/__tests__/workflow.integration.test.js +334 -0
- package/dist/session/atomic-operations.js +307 -0
- package/dist/session/file-watcher.js +218 -0
- package/dist/session/index.js +7 -0
- package/dist/session/types.js +20 -0
- package/dist/session/utils.js +125 -0
- package/dist/session-manager.js +171 -0
- package/dist/session-watcher.js +110 -0
- package/dist/src/__tests__/schema-validation.test.js +170 -0
- package/dist/src/__tests__/server.integration.test.js +274 -0
- package/dist/src/add.js +1 -0
- package/dist/src/add.test.js +5 -0
- package/dist/src/server.js +163 -0
- package/dist/src/session/ResponseFormatter.js +163 -0
- package/dist/src/session/SessionManager.js +572 -0
- package/dist/src/session/__tests__/ResponseFormatter.test.js +741 -0
- package/dist/src/session/__tests__/SessionManager.test.js +593 -0
- package/dist/src/session/__tests__/atomic-operations.test.js +346 -0
- package/dist/src/session/__tests__/file-watcher.test.js +311 -0
- package/dist/src/session/atomic-operations.js +307 -0
- package/dist/src/session/file-watcher.js +227 -0
- package/dist/src/session/index.js +7 -0
- package/dist/src/session/types.js +20 -0
- package/dist/src/session/utils.js +180 -0
- package/dist/src/tui/__tests__/session-watcher.test.js +368 -0
- package/dist/src/tui/components/AnimatedGradient.js +45 -0
- package/dist/src/tui/components/ConfirmationDialog.js +89 -0
- package/dist/src/tui/components/CustomInput.js +14 -0
- package/dist/src/tui/components/Footer.js +55 -0
- package/dist/src/tui/components/Header.js +35 -0
- package/dist/src/tui/components/MultiLineTextInput.js +65 -0
- package/dist/src/tui/components/OptionsList.js +115 -0
- package/dist/src/tui/components/QuestionDisplay.js +36 -0
- package/dist/src/tui/components/ReviewScreen.js +57 -0
- package/dist/src/tui/components/SessionSelectionMenu.js +151 -0
- package/dist/src/tui/components/StepperView.js +166 -0
- package/dist/src/tui/components/TabBar.js +42 -0
- package/dist/src/tui/components/Toast.js +19 -0
- package/dist/src/tui/components/WaitingScreen.js +20 -0
- package/dist/src/tui/session-watcher.js +195 -0
- package/dist/src/tui/theme.js +114 -0
- package/dist/src/tui/utils/gradientText.js +24 -0
- package/dist/tui/__tests__/session-watcher.test.js +368 -0
- package/dist/tui/session-watcher.js +183 -0
- package/package.json +78 -0
- 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
|
+
});
|