auq-mcp-server 0.1.9 → 0.1.24
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/README.md +24 -0
- package/dist/bin/auq.js +127 -7
- package/dist/package.json +15 -5
- package/dist/src/__tests__/schema-validation.test.js +32 -24
- package/dist/src/core/ask-user-questions.js +74 -0
- package/dist/src/server.js +11 -74
- package/dist/src/tui/components/Header.js +9 -1
- package/dist/src/tui/components/QuestionDisplay.js +10 -6
- package/dist/src/tui/components/ReviewScreen.js +6 -2
- package/dist/src/tui/components/StepperView.js +25 -3
- package/dist/src/tui/components/WaitingScreen.js +31 -4
- package/package.json +7 -1
- package/dist/__tests__/schema-validation.test.js +0 -137
- package/dist/__tests__/server.integration.test.js +0 -263
- package/dist/add.js +0 -1
- package/dist/add.test.js +0 -5
- package/dist/bin/test-session-menu.js +0 -28
- package/dist/bin/test-tabbar.js +0 -42
- package/dist/file-utils.js +0 -59
- package/dist/format/ResponseFormatter.js +0 -206
- package/dist/format/__tests__/ResponseFormatter.test.js +0 -380
- package/dist/server.js +0 -107
- package/dist/session/ResponseFormatter.js +0 -130
- package/dist/session/SessionManager.js +0 -474
- package/dist/session/__tests__/ResponseFormatter.test.js +0 -417
- package/dist/session/__tests__/SessionManager.test.js +0 -553
- package/dist/session/__tests__/atomic-operations.test.js +0 -345
- package/dist/session/__tests__/file-watcher.test.js +0 -311
- package/dist/session/__tests__/workflow.integration.test.js +0 -334
- package/dist/session/atomic-operations.js +0 -307
- package/dist/session/file-watcher.js +0 -218
- package/dist/session/index.js +0 -7
- package/dist/session/types.js +0 -20
- package/dist/session/utils.js +0 -125
- package/dist/session-manager.js +0 -171
- package/dist/session-watcher.js +0 -110
- package/dist/src/tui/components/SessionSelectionMenu.js +0 -151
- package/dist/tui/__tests__/session-watcher.test.js +0 -368
- package/dist/tui/session-watcher.js +0 -183
|
@@ -1,151 +0,0 @@
|
|
|
1
|
-
import React, { useState, useEffect } from "react";
|
|
2
|
-
import { Box, Text, useInput, useApp } from "ink";
|
|
3
|
-
import { createTUIWatcher } from "../session-watcher.js";
|
|
4
|
-
/**
|
|
5
|
-
* SessionSelectionMenu displays a list of pending question sets and allows user to select one
|
|
6
|
-
* Uses ↑↓ for navigation, Enter to select, q to quit
|
|
7
|
-
*/
|
|
8
|
-
export const SessionSelectionMenu = ({ onSessionSelect, }) => {
|
|
9
|
-
const { exit } = useApp();
|
|
10
|
-
const [sessions, setSessions] = useState([]);
|
|
11
|
-
const [selectedIndex, setSelectedIndex] = useState(0);
|
|
12
|
-
const [isLoading, setIsLoading] = useState(true);
|
|
13
|
-
const [error, setError] = useState(null);
|
|
14
|
-
// Load pending sessions on mount and start persistent watcher
|
|
15
|
-
useEffect(() => {
|
|
16
|
-
let watcherInstance = null;
|
|
17
|
-
const initialize = async () => {
|
|
18
|
-
try {
|
|
19
|
-
setIsLoading(true);
|
|
20
|
-
// Step 1: Load existing pending sessions
|
|
21
|
-
const watcher = createTUIWatcher();
|
|
22
|
-
const sessionIds = await watcher.getPendingSessions();
|
|
23
|
-
const sessionData = await Promise.all(sessionIds.map(async (sessionId) => {
|
|
24
|
-
const sessionRequest = await watcher.getSessionRequest(sessionId);
|
|
25
|
-
if (!sessionRequest)
|
|
26
|
-
return null;
|
|
27
|
-
return {
|
|
28
|
-
sessionId,
|
|
29
|
-
sessionRequest,
|
|
30
|
-
timestamp: new Date(sessionRequest.timestamp),
|
|
31
|
-
};
|
|
32
|
-
}));
|
|
33
|
-
// Filter out null entries and sort by timestamp (newest first)
|
|
34
|
-
const validSessions = sessionData
|
|
35
|
-
.filter((s) => s !== null)
|
|
36
|
-
.sort((a, b) => b.timestamp.getTime() - a.timestamp.getTime());
|
|
37
|
-
setSessions(validSessions);
|
|
38
|
-
setIsLoading(false);
|
|
39
|
-
// Step 2: Start persistent watcher for new sessions
|
|
40
|
-
watcherInstance = createTUIWatcher({ autoLoadData: true });
|
|
41
|
-
watcherInstance.startEnhancedWatching((event) => {
|
|
42
|
-
// Add new session to queue (FIFO - append to end)
|
|
43
|
-
setSessions((prev) => {
|
|
44
|
-
// Check for duplicates
|
|
45
|
-
if (prev.some((s) => s.sessionId === event.sessionId)) {
|
|
46
|
-
return prev;
|
|
47
|
-
}
|
|
48
|
-
// Add to end of queue
|
|
49
|
-
return [
|
|
50
|
-
...prev,
|
|
51
|
-
{
|
|
52
|
-
sessionId: event.sessionId,
|
|
53
|
-
sessionRequest: event.sessionRequest,
|
|
54
|
-
timestamp: new Date(event.timestamp),
|
|
55
|
-
},
|
|
56
|
-
];
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
}
|
|
60
|
-
catch (err) {
|
|
61
|
-
setError(err instanceof Error ? err.message : "Failed to load question sets");
|
|
62
|
-
setIsLoading(false);
|
|
63
|
-
}
|
|
64
|
-
};
|
|
65
|
-
initialize();
|
|
66
|
-
// Cleanup: stop watcher on unmount
|
|
67
|
-
return () => {
|
|
68
|
-
if (watcherInstance) {
|
|
69
|
-
watcherInstance.stop();
|
|
70
|
-
}
|
|
71
|
-
};
|
|
72
|
-
}, []);
|
|
73
|
-
// Handle keyboard input
|
|
74
|
-
useInput((input, key) => {
|
|
75
|
-
if (isLoading)
|
|
76
|
-
return;
|
|
77
|
-
if (key.upArrow && sessions.length > 0) {
|
|
78
|
-
setSelectedIndex((prev) => Math.max(0, prev - 1));
|
|
79
|
-
}
|
|
80
|
-
if (key.downArrow && sessions.length > 0) {
|
|
81
|
-
setSelectedIndex((prev) => Math.min(sessions.length - 1, prev + 1));
|
|
82
|
-
}
|
|
83
|
-
if (key.return && sessions[selectedIndex]) {
|
|
84
|
-
const { sessionId, sessionRequest } = sessions[selectedIndex];
|
|
85
|
-
onSessionSelect(sessionId, sessionRequest);
|
|
86
|
-
}
|
|
87
|
-
if (input === "q") {
|
|
88
|
-
exit();
|
|
89
|
-
}
|
|
90
|
-
});
|
|
91
|
-
// Loading state
|
|
92
|
-
if (isLoading) {
|
|
93
|
-
return (React.createElement(Box, { padding: 1 },
|
|
94
|
-
React.createElement(Text, null, "Loading question sets...")));
|
|
95
|
-
}
|
|
96
|
-
// Error state
|
|
97
|
-
if (error) {
|
|
98
|
-
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
99
|
-
React.createElement(Text, { color: "red" },
|
|
100
|
-
"Error: ",
|
|
101
|
-
error),
|
|
102
|
-
React.createElement(Text, { dimColor: true }, "Press q to quit")));
|
|
103
|
-
}
|
|
104
|
-
// Zero sessions state
|
|
105
|
-
if (sessions.length === 0) {
|
|
106
|
-
return (React.createElement(Box, { flexDirection: "column", padding: 1 },
|
|
107
|
-
React.createElement(Text, { color: "yellow" }, "No pending question sets found."),
|
|
108
|
-
React.createElement(Text, { dimColor: true }, "Waiting for AI to ask questions..."),
|
|
109
|
-
React.createElement(Text, { dimColor: true }, "Press q to quit")));
|
|
110
|
-
}
|
|
111
|
-
// Session selection menu
|
|
112
|
-
return (React.createElement(Box, { flexDirection: "column", padding: 1, borderStyle: "round", borderColor: "cyan" },
|
|
113
|
-
React.createElement(Text, { bold: true }, "Select a pending question set:"),
|
|
114
|
-
React.createElement(Box, { marginTop: 1 }),
|
|
115
|
-
sessions.map((session, idx) => {
|
|
116
|
-
const isSelected = idx === selectedIndex;
|
|
117
|
-
const indicator = isSelected ? "→" : " ";
|
|
118
|
-
const questionCount = session.sessionRequest.questions.length;
|
|
119
|
-
const relativeTime = formatRelativeTime(session.sessionRequest.timestamp);
|
|
120
|
-
return (React.createElement(Text, { key: session.sessionId, color: isSelected ? "cyan" : "white" },
|
|
121
|
-
indicator,
|
|
122
|
-
" Question Set ",
|
|
123
|
-
idx + 1,
|
|
124
|
-
" (",
|
|
125
|
-
questionCount,
|
|
126
|
-
" ",
|
|
127
|
-
questionCount === 1 ? "question" : "questions",
|
|
128
|
-
") - ",
|
|
129
|
-
relativeTime));
|
|
130
|
-
}),
|
|
131
|
-
React.createElement(Box, { marginTop: 1 }),
|
|
132
|
-
React.createElement(Text, { dimColor: true }, "\u2191\u2193 Navigate | Enter Select | q Quit")));
|
|
133
|
-
};
|
|
134
|
-
/**
|
|
135
|
-
* Format timestamp as relative time (e.g., "5m ago", "2h ago")
|
|
136
|
-
*/
|
|
137
|
-
function formatRelativeTime(timestamp) {
|
|
138
|
-
const now = Date.now();
|
|
139
|
-
const then = new Date(timestamp).getTime();
|
|
140
|
-
const diffMs = now - then;
|
|
141
|
-
const diffMins = Math.floor(diffMs / 60000);
|
|
142
|
-
if (diffMins < 1)
|
|
143
|
-
return "just now";
|
|
144
|
-
if (diffMins < 60)
|
|
145
|
-
return `${diffMins}m ago`;
|
|
146
|
-
const diffHours = Math.floor(diffMins / 60);
|
|
147
|
-
if (diffHours < 24)
|
|
148
|
-
return `${diffHours}h ago`;
|
|
149
|
-
const diffDays = Math.floor(diffHours / 24);
|
|
150
|
-
return `${diffDays}d ago`;
|
|
151
|
-
}
|
|
@@ -1,368 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unit tests for TUI session watcher functionality
|
|
3
|
-
*/
|
|
4
|
-
import { promises as fs } from "fs";
|
|
5
|
-
import { join } from "path";
|
|
6
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
7
|
-
import { SESSION_FILES } from "../../session/types.js";
|
|
8
|
-
import { createTUIWatcher, EnhancedTUISessionWatcher, getNextPendingSession, } from "../session-watcher.js";
|
|
9
|
-
describe("TUI Session Watcher", () => {
|
|
10
|
-
const testDir = "/tmp/auq-tui-watcher-test";
|
|
11
|
-
const sessionDir = join(testDir, "sessions");
|
|
12
|
-
const testSessionId = "test-session-123";
|
|
13
|
-
const mockSessionRequest = {
|
|
14
|
-
questions: [
|
|
15
|
-
{
|
|
16
|
-
title: "Language",
|
|
17
|
-
options: [
|
|
18
|
-
{ description: "Dynamic web language", label: "JavaScript" },
|
|
19
|
-
{ description: "Typed JavaScript", label: "TypeScript" },
|
|
20
|
-
{ description: "Versatile and readable", label: "Python" },
|
|
21
|
-
],
|
|
22
|
-
prompt: "What is your favorite programming language?",
|
|
23
|
-
},
|
|
24
|
-
],
|
|
25
|
-
sessionId: testSessionId,
|
|
26
|
-
status: "pending",
|
|
27
|
-
timestamp: new Date().toISOString(),
|
|
28
|
-
};
|
|
29
|
-
beforeEach(async () => {
|
|
30
|
-
// Clean up test directory before each test
|
|
31
|
-
await fs.rm(testDir, { force: true, recursive: true }).catch(() => { });
|
|
32
|
-
await fs.mkdir(sessionDir, { recursive: true });
|
|
33
|
-
});
|
|
34
|
-
afterEach(async () => {
|
|
35
|
-
// Clean up test directory after each test
|
|
36
|
-
await fs.rm(testDir, { force: true, recursive: true }).catch(() => { });
|
|
37
|
-
});
|
|
38
|
-
describe("EnhancedTUISessionWatcher", () => {
|
|
39
|
-
describe("session event detection", () => {
|
|
40
|
-
it("should detect new sessions with loaded data", async () => {
|
|
41
|
-
const watcher = new EnhancedTUISessionWatcher({
|
|
42
|
-
autoLoadData: true,
|
|
43
|
-
debounceMs: 100,
|
|
44
|
-
sessionDir,
|
|
45
|
-
});
|
|
46
|
-
const events = [];
|
|
47
|
-
watcher.startEnhancedWatching((event) => {
|
|
48
|
-
events.push(event);
|
|
49
|
-
});
|
|
50
|
-
// Give watcher time to initialize
|
|
51
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
52
|
-
// Create a new session directory
|
|
53
|
-
const newSessionDir = join(sessionDir, testSessionId);
|
|
54
|
-
await fs.mkdir(newSessionDir);
|
|
55
|
-
// Create required session files
|
|
56
|
-
const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
|
|
57
|
-
const statusFile = join(newSessionDir, SESSION_FILES.STATUS);
|
|
58
|
-
await Promise.all([
|
|
59
|
-
fs.writeFile(requestFile, JSON.stringify(mockSessionRequest, null, 2)),
|
|
60
|
-
fs.writeFile(statusFile, JSON.stringify({
|
|
61
|
-
createdAt: new Date().toISOString(),
|
|
62
|
-
lastModified: new Date().toISOString(),
|
|
63
|
-
sessionId: testSessionId,
|
|
64
|
-
status: "pending",
|
|
65
|
-
totalQuestions: 1,
|
|
66
|
-
})),
|
|
67
|
-
]);
|
|
68
|
-
// Wait for debounce (100ms) + processing time
|
|
69
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
70
|
-
expect(events).toHaveLength(1);
|
|
71
|
-
expect(events[0].type).toBe("session-created");
|
|
72
|
-
expect(events[0].sessionId).toBe(testSessionId);
|
|
73
|
-
expect(events[0].sessionRequest).toEqual(mockSessionRequest);
|
|
74
|
-
watcher.stop();
|
|
75
|
-
});
|
|
76
|
-
it("should handle autoLoadData disabled", async () => {
|
|
77
|
-
const watcher = new EnhancedTUISessionWatcher({
|
|
78
|
-
autoLoadData: false,
|
|
79
|
-
debounceMs: 100,
|
|
80
|
-
sessionDir,
|
|
81
|
-
});
|
|
82
|
-
const events = [];
|
|
83
|
-
watcher.startEnhancedWatching((event) => {
|
|
84
|
-
events.push(event);
|
|
85
|
-
});
|
|
86
|
-
// Give watcher time to initialize
|
|
87
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
88
|
-
// Create session directory and files
|
|
89
|
-
const newSessionDir = join(sessionDir, testSessionId);
|
|
90
|
-
await fs.mkdir(newSessionDir);
|
|
91
|
-
const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
|
|
92
|
-
await fs.writeFile(requestFile, JSON.stringify(mockSessionRequest, null, 2));
|
|
93
|
-
// Wait for debounce + processing
|
|
94
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
95
|
-
expect(events).toHaveLength(1);
|
|
96
|
-
expect(events[0].type).toBe("session-created");
|
|
97
|
-
expect(events[0].sessionId).toBe(testSessionId);
|
|
98
|
-
expect(events[0].sessionRequest).toBeUndefined(); // Should not be loaded
|
|
99
|
-
watcher.stop();
|
|
100
|
-
});
|
|
101
|
-
it("should handle corrupted session files gracefully", async () => {
|
|
102
|
-
const watcher = new EnhancedTUISessionWatcher({
|
|
103
|
-
autoLoadData: true,
|
|
104
|
-
debounceMs: 100,
|
|
105
|
-
sessionDir,
|
|
106
|
-
});
|
|
107
|
-
const events = [];
|
|
108
|
-
const consoleSpy = vi
|
|
109
|
-
.spyOn(console, "warn")
|
|
110
|
-
.mockImplementation(() => { });
|
|
111
|
-
watcher.startEnhancedWatching((event) => {
|
|
112
|
-
events.push(event);
|
|
113
|
-
});
|
|
114
|
-
// Create session with corrupted request.json
|
|
115
|
-
const newSessionDir = join(sessionDir, testSessionId);
|
|
116
|
-
await fs.mkdir(newSessionDir);
|
|
117
|
-
const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
|
|
118
|
-
const statusFile = join(newSessionDir, SESSION_FILES.STATUS);
|
|
119
|
-
// Write corrupted request.json and valid status.json
|
|
120
|
-
await Promise.all([
|
|
121
|
-
fs.writeFile(requestFile, "invalid json content"),
|
|
122
|
-
fs.writeFile(statusFile, JSON.stringify({
|
|
123
|
-
createdAt: new Date().toISOString(),
|
|
124
|
-
lastModified: new Date().toISOString(),
|
|
125
|
-
sessionId: testSessionId,
|
|
126
|
-
status: "pending",
|
|
127
|
-
totalQuestions: 1,
|
|
128
|
-
})),
|
|
129
|
-
]);
|
|
130
|
-
// Wait for debounce + processing
|
|
131
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
132
|
-
expect(events).toHaveLength(1);
|
|
133
|
-
expect(events[0].type).toBe("session-created");
|
|
134
|
-
expect(events[0].sessionId).toBe(testSessionId);
|
|
135
|
-
expect(events[0].sessionRequest).toBeUndefined();
|
|
136
|
-
// Should have logged a warning about corrupted data
|
|
137
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to load session request"), expect.any(Error));
|
|
138
|
-
consoleSpy.mockRestore();
|
|
139
|
-
watcher.stop();
|
|
140
|
-
});
|
|
141
|
-
});
|
|
142
|
-
describe("event handlers", () => {
|
|
143
|
-
it("should support multiple event handlers", async () => {
|
|
144
|
-
const watcher = new EnhancedTUISessionWatcher({
|
|
145
|
-
debounceMs: 100,
|
|
146
|
-
sessionDir,
|
|
147
|
-
});
|
|
148
|
-
const mainEvents = [];
|
|
149
|
-
const customEvents = [];
|
|
150
|
-
watcher.addEventHandler("custom", (event) => {
|
|
151
|
-
customEvents.push(event);
|
|
152
|
-
});
|
|
153
|
-
watcher.startEnhancedWatching((event) => {
|
|
154
|
-
mainEvents.push(event);
|
|
155
|
-
});
|
|
156
|
-
// Give watcher time to initialize
|
|
157
|
-
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
158
|
-
// Create session
|
|
159
|
-
const newSessionDir = join(sessionDir, testSessionId);
|
|
160
|
-
await fs.mkdir(newSessionDir);
|
|
161
|
-
const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
|
|
162
|
-
await fs.writeFile(requestFile, JSON.stringify(mockSessionRequest, null, 2));
|
|
163
|
-
// Wait for debounce + processing
|
|
164
|
-
await new Promise((resolve) => setTimeout(resolve, 300));
|
|
165
|
-
expect(mainEvents).toHaveLength(1);
|
|
166
|
-
expect(customEvents).toHaveLength(1);
|
|
167
|
-
expect(customEvents[0]).toEqual(mainEvents[0]);
|
|
168
|
-
watcher.removeEventHandler("custom");
|
|
169
|
-
watcher.stop();
|
|
170
|
-
});
|
|
171
|
-
it("should handle event handler removal", async () => {
|
|
172
|
-
const watcher = new EnhancedTUISessionWatcher({ sessionDir });
|
|
173
|
-
const customEvents = [];
|
|
174
|
-
watcher.addEventHandler("custom", (event) => {
|
|
175
|
-
customEvents.push(event);
|
|
176
|
-
});
|
|
177
|
-
watcher.removeEventHandler("custom");
|
|
178
|
-
watcher.startEnhancedWatching(() => { });
|
|
179
|
-
// Create session
|
|
180
|
-
const newSessionDir = join(sessionDir, testSessionId);
|
|
181
|
-
await fs.mkdir(newSessionDir);
|
|
182
|
-
const requestFile = join(newSessionDir, SESSION_FILES.REQUEST);
|
|
183
|
-
await fs.writeFile(requestFile, JSON.stringify(mockSessionRequest, null, 2));
|
|
184
|
-
// Wait for processing
|
|
185
|
-
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
186
|
-
expect(customEvents).toHaveLength(0); // Should not have triggered
|
|
187
|
-
watcher.stop();
|
|
188
|
-
});
|
|
189
|
-
});
|
|
190
|
-
describe("session management", () => {
|
|
191
|
-
beforeEach(async () => {
|
|
192
|
-
// Create multiple test sessions
|
|
193
|
-
const sessions = [
|
|
194
|
-
{ completed: false, id: "session-1" },
|
|
195
|
-
{ completed: true, id: "session-2" },
|
|
196
|
-
{ completed: false, id: "session-3" },
|
|
197
|
-
];
|
|
198
|
-
for (const session of sessions) {
|
|
199
|
-
const sessionDir = join(testDir, "sessions", session.id);
|
|
200
|
-
await fs.mkdir(sessionDir, { recursive: true });
|
|
201
|
-
// Create request.json
|
|
202
|
-
const requestFile = join(sessionDir, SESSION_FILES.REQUEST);
|
|
203
|
-
await fs.writeFile(requestFile, JSON.stringify({
|
|
204
|
-
...mockSessionRequest,
|
|
205
|
-
sessionId: session.id,
|
|
206
|
-
}));
|
|
207
|
-
// Create status.json
|
|
208
|
-
const statusFile = join(sessionDir, SESSION_FILES.STATUS);
|
|
209
|
-
await fs.writeFile(statusFile, JSON.stringify({
|
|
210
|
-
createdAt: new Date().toISOString(),
|
|
211
|
-
lastModified: new Date().toISOString(),
|
|
212
|
-
sessionId: session.id,
|
|
213
|
-
status: session.completed ? "completed" : "pending",
|
|
214
|
-
totalQuestions: 1,
|
|
215
|
-
}));
|
|
216
|
-
// Create answers.json for completed sessions
|
|
217
|
-
if (session.completed) {
|
|
218
|
-
const answersFile = join(sessionDir, SESSION_FILES.ANSWERS);
|
|
219
|
-
await fs.writeFile(answersFile, JSON.stringify({
|
|
220
|
-
answers: [
|
|
221
|
-
{
|
|
222
|
-
questionIndex: 0,
|
|
223
|
-
selectedOption: "JavaScript",
|
|
224
|
-
timestamp: new Date().toISOString(),
|
|
225
|
-
},
|
|
226
|
-
],
|
|
227
|
-
sessionId: session.id,
|
|
228
|
-
timestamp: new Date().toISOString(),
|
|
229
|
-
}));
|
|
230
|
-
}
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
it("should get pending sessions correctly", async () => {
|
|
234
|
-
const watcher = new EnhancedTUISessionWatcher({ sessionDir });
|
|
235
|
-
const pendingSessions = await watcher.getPendingSessions();
|
|
236
|
-
expect(pendingSessions).toHaveLength(2);
|
|
237
|
-
expect(pendingSessions).toContain("session-1");
|
|
238
|
-
expect(pendingSessions).toContain("session-3");
|
|
239
|
-
expect(pendingSessions).not.toContain("session-2");
|
|
240
|
-
// Should be sorted
|
|
241
|
-
expect(pendingSessions[0]).toBe("session-1");
|
|
242
|
-
expect(pendingSessions[1]).toBe("session-3");
|
|
243
|
-
watcher.stop();
|
|
244
|
-
});
|
|
245
|
-
it("should get session request data", async () => {
|
|
246
|
-
const watcher = new EnhancedTUISessionWatcher({ sessionDir });
|
|
247
|
-
const sessionRequest = await watcher.getSessionRequest("session-1");
|
|
248
|
-
expect(sessionRequest).toBeTruthy();
|
|
249
|
-
expect(sessionRequest?.sessionId).toBe("session-1");
|
|
250
|
-
expect(sessionRequest?.questions).toHaveLength(1);
|
|
251
|
-
watcher.stop();
|
|
252
|
-
});
|
|
253
|
-
it("should handle non-existent session request", async () => {
|
|
254
|
-
const watcher = new EnhancedTUISessionWatcher({ sessionDir });
|
|
255
|
-
const consoleSpy = vi
|
|
256
|
-
.spyOn(console, "warn")
|
|
257
|
-
.mockImplementation(() => { });
|
|
258
|
-
const sessionRequest = await watcher.getSessionRequest("non-existent");
|
|
259
|
-
expect(sessionRequest).toBeNull();
|
|
260
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to load session request"), expect.any(Error));
|
|
261
|
-
consoleSpy.mockRestore();
|
|
262
|
-
watcher.stop();
|
|
263
|
-
});
|
|
264
|
-
it("should check session pending status correctly", async () => {
|
|
265
|
-
const watcher = new EnhancedTUISessionWatcher({ sessionDir });
|
|
266
|
-
expect(await watcher.isSessionPending("session-1")).toBe(true);
|
|
267
|
-
expect(await watcher.isSessionPending("session-2")).toBe(false);
|
|
268
|
-
expect(await watcher.isSessionPending("session-3")).toBe(true);
|
|
269
|
-
expect(await watcher.isSessionPending("non-existent")).toBe(false);
|
|
270
|
-
watcher.stop();
|
|
271
|
-
});
|
|
272
|
-
});
|
|
273
|
-
});
|
|
274
|
-
describe("Utility Functions", () => {
|
|
275
|
-
describe("createTUIWatcher", () => {
|
|
276
|
-
it("should create an enhanced TUI session watcher", () => {
|
|
277
|
-
const watcher = createTUIWatcher({ sessionDir });
|
|
278
|
-
expect(watcher).toBeInstanceOf(EnhancedTUISessionWatcher);
|
|
279
|
-
expect(watcher.watchedPath).toBe(sessionDir);
|
|
280
|
-
watcher.stop();
|
|
281
|
-
});
|
|
282
|
-
});
|
|
283
|
-
describe("getNextPendingSession", () => {
|
|
284
|
-
beforeEach(async () => {
|
|
285
|
-
// Create test sessions
|
|
286
|
-
const sessions = [
|
|
287
|
-
{ id: "first-session", pending: true },
|
|
288
|
-
{ id: "second-session", pending: true },
|
|
289
|
-
];
|
|
290
|
-
for (const session of sessions) {
|
|
291
|
-
const sessionDir = join(testDir, "sessions", session.id);
|
|
292
|
-
await fs.mkdir(sessionDir, { recursive: true });
|
|
293
|
-
// Create required files
|
|
294
|
-
const requestFile = join(sessionDir, SESSION_FILES.REQUEST);
|
|
295
|
-
const statusFile = join(sessionDir, SESSION_FILES.STATUS);
|
|
296
|
-
await Promise.all([
|
|
297
|
-
fs.writeFile(requestFile, JSON.stringify({
|
|
298
|
-
...mockSessionRequest,
|
|
299
|
-
sessionId: session.id,
|
|
300
|
-
})),
|
|
301
|
-
fs.writeFile(statusFile, JSON.stringify({
|
|
302
|
-
createdAt: new Date().toISOString(),
|
|
303
|
-
lastModified: new Date().toISOString(),
|
|
304
|
-
sessionId: session.id,
|
|
305
|
-
status: "pending",
|
|
306
|
-
totalQuestions: 1,
|
|
307
|
-
})),
|
|
308
|
-
]);
|
|
309
|
-
}
|
|
310
|
-
});
|
|
311
|
-
it("should get the next pending session", async () => {
|
|
312
|
-
const result = await getNextPendingSession({ sessionDir });
|
|
313
|
-
expect(result).toBeTruthy();
|
|
314
|
-
expect(result?.sessionId).toBe("first-session");
|
|
315
|
-
expect(result?.sessionRequest).toBeTruthy();
|
|
316
|
-
expect(result?.sessionRequest.sessionId).toBe("first-session");
|
|
317
|
-
});
|
|
318
|
-
it("should return null when no pending sessions", async () => {
|
|
319
|
-
// Clear all sessions
|
|
320
|
-
await fs.rm(sessionDir, { recursive: true });
|
|
321
|
-
await fs.mkdir(sessionDir);
|
|
322
|
-
const result = await getNextPendingSession({ sessionDir });
|
|
323
|
-
expect(result).toBeNull();
|
|
324
|
-
});
|
|
325
|
-
it("should handle sessions with corrupted data", async () => {
|
|
326
|
-
// Create session with corrupted request.json
|
|
327
|
-
const corruptedSessionDir = join(sessionDir, "corrupted-session");
|
|
328
|
-
await fs.mkdir(corruptedSessionDir);
|
|
329
|
-
const statusFile = join(corruptedSessionDir, SESSION_FILES.STATUS);
|
|
330
|
-
await fs.writeFile(statusFile, JSON.stringify({
|
|
331
|
-
createdAt: new Date().toISOString(),
|
|
332
|
-
lastModified: new Date().toISOString(),
|
|
333
|
-
sessionId: "corrupted-session",
|
|
334
|
-
status: "pending",
|
|
335
|
-
totalQuestions: 1,
|
|
336
|
-
}));
|
|
337
|
-
// Create corrupted request.json
|
|
338
|
-
const requestFile = join(corruptedSessionDir, SESSION_FILES.REQUEST);
|
|
339
|
-
await fs.writeFile(requestFile, "invalid json");
|
|
340
|
-
const result = await getNextPendingSession({ sessionDir });
|
|
341
|
-
// Should skip corrupted session and return valid one
|
|
342
|
-
expect(result).toBeTruthy();
|
|
343
|
-
expect(result?.sessionId).toBe("first-session");
|
|
344
|
-
});
|
|
345
|
-
});
|
|
346
|
-
});
|
|
347
|
-
describe("Error Handling", () => {
|
|
348
|
-
it("should handle directory access errors", async () => {
|
|
349
|
-
const watcher = new EnhancedTUISessionWatcher({
|
|
350
|
-
sessionDir: "/invalid/directory/path",
|
|
351
|
-
});
|
|
352
|
-
const consoleSpy = vi.spyOn(console, "warn").mockImplementation(() => { });
|
|
353
|
-
const pendingSessions = await watcher.getPendingSessions();
|
|
354
|
-
expect(pendingSessions).toEqual([]);
|
|
355
|
-
expect(consoleSpy).toHaveBeenCalledWith(expect.stringContaining("Failed to scan for pending sessions"), expect.any(Error));
|
|
356
|
-
consoleSpy.mockRestore();
|
|
357
|
-
watcher.stop();
|
|
358
|
-
});
|
|
359
|
-
it("should handle missing watched path gracefully", async () => {
|
|
360
|
-
const watcher = new EnhancedTUISessionWatcher({
|
|
361
|
-
sessionDir: "/non/existent/path",
|
|
362
|
-
});
|
|
363
|
-
const sessionRequest = await watcher.getSessionRequest("any-id");
|
|
364
|
-
expect(sessionRequest).toBeNull();
|
|
365
|
-
watcher.stop();
|
|
366
|
-
});
|
|
367
|
-
});
|
|
368
|
-
});
|