auq-mcp-server 2.6.3 → 2.7.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 (96) hide show
  1. package/README.md +56 -2
  2. package/dist/bin/auq.js +36 -3
  3. package/dist/bin/tui-app.js +30 -15
  4. package/dist/package.json +7 -2
  5. package/dist/src/__tests__/schema-validation.test.js +61 -1
  6. package/dist/src/cli/commands/__tests__/fetch-answers.test.js +310 -0
  7. package/dist/src/cli/commands/__tests__/history.test.js +211 -0
  8. package/dist/src/cli/commands/answer.js +11 -0
  9. package/dist/src/cli/commands/config.js +48 -0
  10. package/dist/src/cli/commands/fetch-answers.js +205 -0
  11. package/dist/src/cli/commands/history.js +375 -0
  12. package/dist/src/config/__tests__/ConfigLoader.test.js +38 -0
  13. package/dist/src/config/defaults.js +1 -0
  14. package/dist/src/config/types.js +1 -0
  15. package/dist/src/core/ask-user-questions.js +63 -0
  16. package/dist/src/i18n/locales/en.js +2 -2
  17. package/dist/src/server.js +59 -2
  18. package/dist/src/session/ResponseFormatter.js +79 -2
  19. package/dist/src/session/SessionManager.js +36 -0
  20. package/dist/src/session/__tests__/ResponseFormatter.test.js +86 -0
  21. package/dist/src/session/__tests__/SessionManager.test.js +129 -0
  22. package/dist/src/shared/schemas.js +8 -0
  23. package/dist/src/tui/ThemeProvider.js +3 -3
  24. package/dist/src/tui/components/Header.js +2 -1
  25. package/dist/src/tui/components/OptionsList.js +1 -1
  26. package/dist/src/tui/components/SessionPicker.js +1 -1
  27. package/dist/src/tui/components/StepperView.js +1 -1
  28. package/dist/src/tui/components/__tests__/ConfirmationDialog.test.js +1 -1
  29. package/dist/src/tui/components/__tests__/Footer.test.js +1 -1
  30. package/dist/src/tui/components/__tests__/MarkdownPrompt.test.js +1 -1
  31. package/dist/src/tui/components/__tests__/ReviewScreen.test.js +1 -1
  32. package/dist/src/tui/components/__tests__/SessionDots.test.js +1 -1
  33. package/dist/src/tui/components/__tests__/SessionPicker.test.js +1 -1
  34. package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +1 -1
  35. package/dist/src/tui/components/__tests__/StepperView.keyboard.test.js +1 -1
  36. package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -1
  37. package/dist/src/tui/components/__tests__/WaitingScreen.test.js +1 -1
  38. package/dist/src/tui/shared/session-events.js +4 -0
  39. package/dist/src/tui/shared/themes/catppuccin-latte.js +130 -0
  40. package/dist/src/tui/shared/themes/catppuccin-mocha.js +131 -0
  41. package/dist/src/tui/shared/themes/dark.js +131 -0
  42. package/dist/src/tui/shared/themes/dracula.js +131 -0
  43. package/dist/src/tui/shared/themes/github-dark.js +129 -0
  44. package/dist/src/tui/shared/themes/github-light.js +129 -0
  45. package/dist/src/tui/shared/themes/gruvbox-dark.js +130 -0
  46. package/dist/src/tui/shared/themes/gruvbox-light.js +130 -0
  47. package/dist/src/tui/shared/themes/index.js +70 -0
  48. package/dist/src/tui/shared/themes/light.js +130 -0
  49. package/dist/src/tui/shared/themes/loader.js +111 -0
  50. package/dist/src/tui/shared/themes/monokai.js +132 -0
  51. package/dist/src/tui/shared/themes/nord.js +130 -0
  52. package/dist/src/tui/shared/themes/one-dark.js +131 -0
  53. package/dist/src/tui/shared/themes/rose-pine.js +131 -0
  54. package/dist/src/tui/shared/themes/solarized-dark.js +130 -0
  55. package/dist/src/tui/shared/themes/solarized-light.js +130 -0
  56. package/dist/src/tui/shared/themes/tokyo-night.js +131 -0
  57. package/dist/src/tui/shared/themes/types.js +1 -0
  58. package/dist/src/tui/shared/types.js +1 -0
  59. package/dist/src/tui/shared/utils/config.js +80 -0
  60. package/dist/src/tui/shared/utils/detectTheme.js +33 -0
  61. package/dist/src/tui/shared/utils/index.js +6 -0
  62. package/dist/src/tui/shared/utils/recommended.js +52 -0
  63. package/dist/src/tui/shared/utils/relativeTime.js +24 -0
  64. package/dist/src/tui/shared/utils/sessionSwitching.js +56 -0
  65. package/dist/src/tui/shared/utils/staleDetection.js +51 -0
  66. package/dist/src/tui/themes/catppuccin-latte.js +2 -127
  67. package/dist/src/tui/themes/catppuccin-mocha.js +2 -127
  68. package/dist/src/tui/themes/dark.js +2 -128
  69. package/dist/src/tui/themes/dracula.js +2 -127
  70. package/dist/src/tui/themes/github-dark.js +2 -126
  71. package/dist/src/tui/themes/github-light.js +2 -126
  72. package/dist/src/tui/themes/gruvbox-dark.js +2 -127
  73. package/dist/src/tui/themes/gruvbox-light.js +2 -127
  74. package/dist/src/tui/themes/index.js +2 -70
  75. package/dist/src/tui/themes/light.js +2 -127
  76. package/dist/src/tui/themes/loader.js +2 -111
  77. package/dist/src/tui/themes/monokai.js +2 -128
  78. package/dist/src/tui/themes/nord.js +2 -127
  79. package/dist/src/tui/themes/one-dark.js +2 -127
  80. package/dist/src/tui/themes/rose-pine.js +2 -128
  81. package/dist/src/tui/themes/solarized-dark.js +2 -127
  82. package/dist/src/tui/themes/solarized-light.js +2 -127
  83. package/dist/src/tui/themes/tokyo-night.js +2 -127
  84. package/dist/src/tui/themes/types.js +2 -1
  85. package/dist/src/tui/types.js +1 -1
  86. package/dist/src/tui/utils/__tests__/recommended.test.js +1 -1
  87. package/dist/src/tui/utils/__tests__/relativeTime.test.js +1 -1
  88. package/dist/src/tui/utils/__tests__/sessionSwitching.test.js +1 -1
  89. package/dist/src/tui/utils/__tests__/staleDetection.test.js +1 -1
  90. package/dist/src/tui/utils/config.js +1 -80
  91. package/dist/src/tui/utils/detectTheme.js +1 -22
  92. package/dist/src/tui/utils/recommended.js +1 -52
  93. package/dist/src/tui/utils/relativeTime.js +1 -24
  94. package/dist/src/tui/utils/sessionSwitching.js +1 -56
  95. package/dist/src/tui/utils/staleDetection.js +1 -51
  96. package/package.json +7 -2
@@ -0,0 +1,310 @@
1
+ /**
2
+ * Integration tests for the `auq fetch-answers` CLI command.
3
+ */
4
+ import { promises as fs } from "fs";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ import { SessionManager } from "../../../session/SessionManager.js";
7
+ import { runFetchAnswersCommand } from "../fetch-answers.js";
8
+ // ── Helpers ────────────────────────────────────────────────────────────────
9
+ const testBaseDir = "/tmp/auq-test-cli-fetch-answers";
10
+ const sampleQuestions = [
11
+ {
12
+ title: "Language",
13
+ prompt: "Which language do you prefer?",
14
+ options: [
15
+ { label: "TypeScript", description: "Typed JS" },
16
+ { label: "Python", description: "Scripting" },
17
+ ],
18
+ },
19
+ {
20
+ title: "Framework",
21
+ prompt: "Pick a framework",
22
+ options: [
23
+ { label: "React" },
24
+ { label: "Vue" },
25
+ ],
26
+ },
27
+ ];
28
+ // Stub getSessionDirectory so the fetch-answers command always targets our temp dir.
29
+ vi.mock("../../../session/utils.js", async (importOriginal) => {
30
+ const actual = (await importOriginal());
31
+ return {
32
+ ...actual,
33
+ getSessionDirectory: () => testBaseDir,
34
+ };
35
+ });
36
+ /**
37
+ * Helper: create a completed session with answers saved.
38
+ */
39
+ async function createCompletedSession(sessionManager, questions = sampleQuestions) {
40
+ const sessionId = await sessionManager.createSession(questions);
41
+ const answersData = {
42
+ sessionId,
43
+ timestamp: new Date().toISOString(),
44
+ answers: questions.map((q, i) => ({
45
+ questionIndex: i,
46
+ timestamp: new Date().toISOString(),
47
+ selectedOption: q.options[0].label,
48
+ })),
49
+ };
50
+ await sessionManager.saveSessionAnswers(sessionId, answersData);
51
+ await sessionManager.updateSessionStatus(sessionId, "completed");
52
+ return sessionId;
53
+ }
54
+ // ── Test Suite ─────────────────────────────────────────────────────────────
55
+ describe("fetch-answers command", () => {
56
+ let sessionManager;
57
+ let consoleLogSpy;
58
+ let consoleErrorSpy;
59
+ beforeEach(async () => {
60
+ await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
61
+ sessionManager = new SessionManager({ baseDir: testBaseDir });
62
+ await sessionManager.initialize();
63
+ process.exitCode = undefined;
64
+ consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => { });
65
+ consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
66
+ });
67
+ afterEach(async () => {
68
+ consoleLogSpy.mockRestore();
69
+ consoleErrorSpy.mockRestore();
70
+ vi.restoreAllMocks();
71
+ process.exitCode = undefined;
72
+ await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
73
+ });
74
+ // ── Fetch specific session ──────────────────────────────────────────────
75
+ describe("fetch specific session", () => {
76
+ it("should display formatted answers for completed session", async () => {
77
+ const sessionId = await createCompletedSession(sessionManager);
78
+ await runFetchAnswersCommand([sessionId]);
79
+ expect(process.exitCode).toBeUndefined();
80
+ const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
81
+ // Should contain answer content from the session
82
+ expect(allOutput).toBeTruthy();
83
+ expect(allOutput.length).toBeGreaterThan(0);
84
+ });
85
+ it("should mark session as read after fetching", async () => {
86
+ const sessionId = await createCompletedSession(sessionManager);
87
+ // Should not be in unread before fetching (actually it IS unread)
88
+ const unreadBefore = await sessionManager.getUnreadSessions();
89
+ expect(unreadBefore).toContain(sessionId);
90
+ await runFetchAnswersCommand([sessionId]);
91
+ // After fetching, session should be marked as read
92
+ const unreadAfter = await sessionManager.getUnreadSessions();
93
+ expect(unreadAfter).not.toContain(sessionId);
94
+ });
95
+ it("should display pending status for pending session (non-blocking)", async () => {
96
+ const sessionId = await sessionManager.createSession(sampleQuestions);
97
+ await runFetchAnswersCommand([sessionId]);
98
+ expect(process.exitCode).toBeUndefined();
99
+ const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
100
+ // Should contain some output (pending status)
101
+ expect(allOutput).toBeTruthy();
102
+ });
103
+ it("should display rejected status", async () => {
104
+ const sessionId = await sessionManager.createSession(sampleQuestions);
105
+ await sessionManager.updateSessionStatus(sessionId, "rejected");
106
+ await runFetchAnswersCommand([sessionId]);
107
+ expect(process.exitCode).toBeUndefined();
108
+ const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
109
+ expect(allOutput).toBeTruthy();
110
+ });
111
+ it("should resolve short session ID (8-char prefix)", async () => {
112
+ const sessionId = await createCompletedSession(sessionManager);
113
+ const shortId = sessionId.slice(0, 8);
114
+ await runFetchAnswersCommand([shortId]);
115
+ expect(process.exitCode).toBeUndefined();
116
+ const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
117
+ expect(allOutput).toBeTruthy();
118
+ });
119
+ it("should error for non-existent full-UUID session", async () => {
120
+ const fakeId = "00000000-0000-4000-a000-000000000000";
121
+ await runFetchAnswersCommand([fakeId]);
122
+ expect(process.exitCode).toBe(1);
123
+ const errOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
124
+ const logOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
125
+ const combined = errOutput + logOutput;
126
+ expect(combined).toContain(fakeId);
127
+ });
128
+ it("should error for non-existent 8-char prefix", async () => {
129
+ const fakeShortId = "deadbeef";
130
+ await runFetchAnswersCommand([fakeShortId]);
131
+ expect(process.exitCode).toBe(1);
132
+ });
133
+ });
134
+ // ── --json flag ─────────────────────────────────────────────────────────
135
+ describe("--json flag", () => {
136
+ it("should output valid JSON for completed session", async () => {
137
+ const sessionId = await createCompletedSession(sessionManager);
138
+ await runFetchAnswersCommand([sessionId, "--json"]);
139
+ expect(process.exitCode).toBeUndefined();
140
+ // Find valid JSON in console.log calls
141
+ const jsonCalls = consoleLogSpy.mock.calls.filter((c) => {
142
+ try {
143
+ JSON.parse(c[0]);
144
+ return true;
145
+ }
146
+ catch {
147
+ return false;
148
+ }
149
+ });
150
+ expect(jsonCalls.length).toBeGreaterThanOrEqual(1);
151
+ const parsed = JSON.parse(jsonCalls[0][0]);
152
+ expect(parsed.sessionId).toBe(sessionId);
153
+ expect(parsed.status).toBe("completed");
154
+ expect(Array.isArray(parsed.answers)).toBe(true);
155
+ expect(parsed.answers).toHaveLength(2);
156
+ expect(parsed.lastReadAt).toBeDefined();
157
+ });
158
+ it("should output valid JSON for pending session", async () => {
159
+ const sessionId = await sessionManager.createSession(sampleQuestions);
160
+ await runFetchAnswersCommand([sessionId, "--json"]);
161
+ const jsonCalls = consoleLogSpy.mock.calls.filter((c) => {
162
+ try {
163
+ JSON.parse(c[0]);
164
+ return true;
165
+ }
166
+ catch {
167
+ return false;
168
+ }
169
+ });
170
+ expect(jsonCalls.length).toBeGreaterThanOrEqual(1);
171
+ const parsed = JSON.parse(jsonCalls[0][0]);
172
+ expect(parsed.sessionId).toBe(sessionId);
173
+ expect(parsed.status).toBe("pending");
174
+ expect(parsed.answers).toBeNull();
175
+ });
176
+ it("should output valid JSON for rejected session", async () => {
177
+ const sessionId = await sessionManager.createSession(sampleQuestions);
178
+ await sessionManager.updateSessionStatus(sessionId, "rejected");
179
+ await runFetchAnswersCommand([sessionId, "--json"]);
180
+ const jsonCalls = consoleLogSpy.mock.calls.filter((c) => {
181
+ try {
182
+ JSON.parse(c[0]);
183
+ return true;
184
+ }
185
+ catch {
186
+ return false;
187
+ }
188
+ });
189
+ expect(jsonCalls.length).toBeGreaterThanOrEqual(1);
190
+ const parsed = JSON.parse(jsonCalls[0][0]);
191
+ expect(parsed.sessionId).toBe(sessionId);
192
+ expect(parsed.status).toBe("rejected");
193
+ });
194
+ });
195
+ // ── Unread / no session-id mode ─────────────────────────────────────────
196
+ describe("unread / default mode (no session-id)", () => {
197
+ it("should list unread sessions when no session-id provided", async () => {
198
+ const id1 = await createCompletedSession(sessionManager);
199
+ const id2 = await createCompletedSession(sessionManager);
200
+ await runFetchAnswersCommand([]);
201
+ expect(process.exitCode).toBeUndefined();
202
+ const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
203
+ expect(allOutput).toContain(id1.slice(0, 8));
204
+ expect(allOutput).toContain(id2.slice(0, 8));
205
+ });
206
+ it("should show message when no unread sessions", async () => {
207
+ await runFetchAnswersCommand([]);
208
+ expect(process.exitCode).toBeUndefined();
209
+ const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
210
+ expect(allOutput.toLowerCase()).toContain("no unread");
211
+ });
212
+ it("should exclude already-read sessions from default unread list", async () => {
213
+ const sessionId = await createCompletedSession(sessionManager);
214
+ // Mark as read first
215
+ await sessionManager.markSessionAsRead(sessionId);
216
+ await runFetchAnswersCommand([]);
217
+ const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
218
+ // Session should NOT appear in unread list
219
+ expect(allOutput).not.toContain(sessionId.slice(0, 8));
220
+ });
221
+ it("should list unread sessions with explicit --unread flag", async () => {
222
+ const sessionId = await createCompletedSession(sessionManager);
223
+ await runFetchAnswersCommand(["--unread"]);
224
+ expect(process.exitCode).toBeUndefined();
225
+ const allOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
226
+ expect(allOutput).toContain(sessionId.slice(0, 8));
227
+ });
228
+ it("should output valid JSON array for unread list with --json", async () => {
229
+ const id1 = await createCompletedSession(sessionManager);
230
+ const id2 = await createCompletedSession(sessionManager);
231
+ await runFetchAnswersCommand(["--json"]);
232
+ expect(process.exitCode).toBeUndefined();
233
+ const jsonCalls = consoleLogSpy.mock.calls.filter((c) => {
234
+ try {
235
+ JSON.parse(c[0]);
236
+ return true;
237
+ }
238
+ catch {
239
+ return false;
240
+ }
241
+ });
242
+ expect(jsonCalls.length).toBeGreaterThanOrEqual(1);
243
+ const parsed = JSON.parse(jsonCalls[0][0]);
244
+ expect(Array.isArray(parsed)).toBe(true);
245
+ expect(parsed.length).toBe(2);
246
+ // Both session IDs appear in result
247
+ const ids = parsed.map((e) => e.sessionId);
248
+ expect(ids).toContain(id1);
249
+ expect(ids).toContain(id2);
250
+ });
251
+ it("should output empty JSON array when no unread sessions with --json", async () => {
252
+ await runFetchAnswersCommand(["--json"]);
253
+ const jsonCalls = consoleLogSpy.mock.calls.filter((c) => {
254
+ try {
255
+ JSON.parse(c[0]);
256
+ return true;
257
+ }
258
+ catch {
259
+ return false;
260
+ }
261
+ });
262
+ expect(jsonCalls.length).toBeGreaterThanOrEqual(1);
263
+ const parsed = JSON.parse(jsonCalls[0][0]);
264
+ expect(Array.isArray(parsed)).toBe(true);
265
+ expect(parsed.length).toBe(0);
266
+ });
267
+ });
268
+ // ── --blocking with no session id ───────────────────────────────────────
269
+ describe("--blocking flag validation", () => {
270
+ it("should error when --blocking used without a session ID", async () => {
271
+ await runFetchAnswersCommand(["--blocking"]);
272
+ expect(process.exitCode).toBe(1);
273
+ // Should have some error output (either console.error or console.log JSON error)
274
+ const logOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
275
+ const errOutput = consoleErrorSpy.mock.calls.map((c) => c[0]).join("\n");
276
+ const combined = logOutput + errOutput;
277
+ expect(combined.toLowerCase()).toMatch(/blocking|session id/i);
278
+ });
279
+ });
280
+ // ── lastReadAt tracking ─────────────────────────────────────────────────
281
+ describe("read tracking", () => {
282
+ it("should set lastReadAt when fetching a completed session", async () => {
283
+ const sessionId = await createCompletedSession(sessionManager);
284
+ const before = new Date();
285
+ await runFetchAnswersCommand([sessionId]);
286
+ const after = new Date();
287
+ const answers = await sessionManager.getSessionAnswers(sessionId);
288
+ expect(answers).not.toBeNull();
289
+ expect(answers.lastReadAt).toBeDefined();
290
+ const readAt = new Date(answers.lastReadAt);
291
+ expect(readAt.getTime()).toBeGreaterThanOrEqual(before.getTime() - 1000);
292
+ expect(readAt.getTime()).toBeLessThanOrEqual(after.getTime() + 1000);
293
+ });
294
+ it("should not appear in unread list after being fetched", async () => {
295
+ const sessionId = await createCompletedSession(sessionManager);
296
+ // Before fetch — should be unread
297
+ const unreadBefore = await sessionManager.getUnreadSessions();
298
+ expect(unreadBefore).toContain(sessionId);
299
+ await runFetchAnswersCommand([sessionId]);
300
+ // After fetch — should be removed from unread list
301
+ const unreadAfter = await sessionManager.getUnreadSessions();
302
+ expect(unreadAfter).not.toContain(sessionId);
303
+ // Running --unread should also not show it
304
+ consoleLogSpy.mockClear();
305
+ await runFetchAnswersCommand(["--unread"]);
306
+ const unreadOutput = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
307
+ expect(unreadOutput).not.toContain(sessionId.slice(0, 8));
308
+ });
309
+ });
310
+ });
@@ -0,0 +1,211 @@
1
+ /**
2
+ * Integration tests for the `auq history` CLI command.
3
+ */
4
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
5
+ import { promises as fs } from "fs";
6
+ import { SessionManager } from "../../../session/SessionManager.js";
7
+ import { runHistoryCommand } from "../history.js";
8
+ // ── Test directory ─────────────────────────────────────────────────────────
9
+ const testBaseDir = `/tmp/auq-test-history-${process.pid}`;
10
+ // Stub getSessionDirectory so the history command always targets our temp dir.
11
+ vi.mock("../../../session/utils.js", async (importOriginal) => {
12
+ const actual = (await importOriginal());
13
+ return {
14
+ ...actual,
15
+ getSessionDirectory: () => testBaseDir,
16
+ };
17
+ });
18
+ // ── Test fixtures ───────────────────────────────────────────────────────────
19
+ const sampleQuestions = [
20
+ {
21
+ title: "Database",
22
+ prompt: "Which database should we use?",
23
+ options: [
24
+ { label: "Postgres", description: "Relational, reliable" },
25
+ { label: "Redis", description: "In-memory cache" },
26
+ ],
27
+ multiSelect: false,
28
+ },
29
+ {
30
+ title: "Auth",
31
+ prompt: "Which auth method?",
32
+ options: [
33
+ { label: "JWT", description: "Stateless tokens" },
34
+ { label: "Cookies", description: "Server-side sessions" },
35
+ ],
36
+ multiSelect: false,
37
+ },
38
+ ];
39
+ async function createCompletedSession(mgr, opts) {
40
+ const id = await mgr.createSession(sampleQuestions);
41
+ await mgr.saveSessionAnswers(id, {
42
+ sessionId: id,
43
+ timestamp: new Date().toISOString(),
44
+ answers: sampleQuestions.map((q, i) => ({
45
+ questionIndex: i,
46
+ timestamp: new Date().toISOString(),
47
+ selectedOption: q.options[0].label,
48
+ })),
49
+ });
50
+ await mgr.updateSessionStatus(id, "completed");
51
+ if (opts?.read) {
52
+ await mgr.markSessionAsRead(id);
53
+ }
54
+ return id;
55
+ }
56
+ async function createRejectedSession(mgr) {
57
+ const id = await mgr.createSession(sampleQuestions);
58
+ await mgr.rejectSession(id, "not relevant");
59
+ return id;
60
+ }
61
+ async function createAbandonedSession(mgr) {
62
+ const id = await mgr.createSession(sampleQuestions);
63
+ await mgr.updateSessionStatus(id, "abandoned");
64
+ return id;
65
+ }
66
+ // ── Test Suite ──────────────────────────────────────────────────────────────
67
+ describe("history command", () => {
68
+ let sessionManager;
69
+ let consoleLogSpy;
70
+ let consoleErrorSpy;
71
+ beforeEach(async () => {
72
+ await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
73
+ sessionManager = new SessionManager({ baseDir: testBaseDir });
74
+ await sessionManager.initialize();
75
+ consoleLogSpy = vi.spyOn(console, "log").mockImplementation(() => { });
76
+ consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => { });
77
+ process.exitCode = undefined;
78
+ });
79
+ afterEach(async () => {
80
+ consoleLogSpy.mockRestore();
81
+ consoleErrorSpy.mockRestore();
82
+ process.exitCode = undefined;
83
+ await fs.rm(testBaseDir, { force: true, recursive: true }).catch(() => { });
84
+ });
85
+ // ── List (default) ──────────────────────────────────────────────────────
86
+ describe("list (default)", () => {
87
+ it("should list non-abandoned sessions by default", async () => {
88
+ await createCompletedSession(sessionManager);
89
+ const abandonedId = await createAbandonedSession(sessionManager);
90
+ await runHistoryCommand([]);
91
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
92
+ expect(output).toContain("completed");
93
+ // Abandoned session's short ID should not appear as a table row
94
+ expect(output).not.toContain(abandonedId.slice(0, 8));
95
+ });
96
+ it("should show abandoned hint when hidden", async () => {
97
+ await createCompletedSession(sessionManager);
98
+ await createAbandonedSession(sessionManager);
99
+ await runHistoryCommand([]);
100
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
101
+ expect(output).toContain("abandoned hidden");
102
+ expect(output).toContain("--all");
103
+ });
104
+ it("should show all with --all flag", async () => {
105
+ await createCompletedSession(sessionManager);
106
+ const abandonedId = await createAbandonedSession(sessionManager);
107
+ await runHistoryCommand(["--all"]);
108
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
109
+ expect(output).toContain(abandonedId.slice(0, 8));
110
+ });
111
+ it("should show empty state message", async () => {
112
+ await runHistoryCommand([]);
113
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
114
+ expect(output).toContain("No sessions found");
115
+ });
116
+ it("should limit output with --limit", async () => {
117
+ await createCompletedSession(sessionManager);
118
+ await createCompletedSession(sessionManager);
119
+ await createCompletedSession(sessionManager);
120
+ await runHistoryCommand(["--limit", "2"]);
121
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
122
+ // Match 8-character lowercase hex strings (UUID short IDs)
123
+ const matches = output.match(/[a-f0-9]{8}/g) || [];
124
+ const uniqueIds = [...new Set(matches)];
125
+ expect(uniqueIds.length).toBeLessThanOrEqual(2);
126
+ });
127
+ it("should filter unread with --unread", async () => {
128
+ const unreadId = await createCompletedSession(sessionManager);
129
+ await createCompletedSession(sessionManager, { read: true });
130
+ await runHistoryCommand(["--unread"]);
131
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
132
+ expect(output).toContain(unreadId.slice(0, 8));
133
+ });
134
+ it("should search with --search", async () => {
135
+ await createCompletedSession(sessionManager);
136
+ await runHistoryCommand(["--search", "database"]);
137
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
138
+ expect(output).toContain("completed");
139
+ });
140
+ it("should output JSON with --json", async () => {
141
+ await createCompletedSession(sessionManager);
142
+ await runHistoryCommand(["--json"]);
143
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
144
+ const parsed = JSON.parse(output);
145
+ expect(Array.isArray(parsed)).toBe(true);
146
+ expect(parsed[0]).toHaveProperty("sessionId");
147
+ expect(parsed[0]).toHaveProperty("status");
148
+ expect(parsed[0]).toHaveProperty("questionCount");
149
+ });
150
+ it("should find session with --session", async () => {
151
+ const id = await createCompletedSession(sessionManager);
152
+ await runHistoryCommand(["--session", id.slice(0, 8)]);
153
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
154
+ expect(output).toContain(id.slice(0, 8));
155
+ });
156
+ it("should error for --session with no match", async () => {
157
+ await runHistoryCommand(["--session", "00000000"]);
158
+ expect(process.exitCode).toBe(1);
159
+ });
160
+ });
161
+ // ── Show ────────────────────────────────────────────────────────────────
162
+ describe("show", () => {
163
+ it("should show session details", async () => {
164
+ const id = await createCompletedSession(sessionManager);
165
+ await runHistoryCommand(["show", id]);
166
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
167
+ expect(output).toContain(id);
168
+ expect(output).toContain("completed");
169
+ expect(output).toContain("Which database should we use?");
170
+ });
171
+ it("should show all options with (selected) prefix for answered options", async () => {
172
+ const id = await createCompletedSession(sessionManager);
173
+ await runHistoryCommand(["show", id]);
174
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
175
+ expect(output).toContain("(selected)");
176
+ expect(output).toContain("Postgres");
177
+ expect(output).toContain("Redis");
178
+ });
179
+ it("should resolve short session ID", async () => {
180
+ const id = await createCompletedSession(sessionManager);
181
+ await runHistoryCommand(["show", id.slice(0, 8)]);
182
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
183
+ expect(output).toContain(id);
184
+ });
185
+ it("should error for non-existent session", async () => {
186
+ await runHistoryCommand(["show", "nonexistent-id"]);
187
+ expect(process.exitCode).toBe(1);
188
+ });
189
+ it("should show rejected session", async () => {
190
+ const id = await createRejectedSession(sessionManager);
191
+ await runHistoryCommand(["show", id]);
192
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
193
+ expect(output).toContain("rejected");
194
+ });
195
+ it("should show abandoned session", async () => {
196
+ const id = await createAbandonedSession(sessionManager);
197
+ await runHistoryCommand(["show", id]);
198
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
199
+ expect(output).toContain("abandoned");
200
+ });
201
+ it("should output JSON with --json", async () => {
202
+ const id = await createCompletedSession(sessionManager);
203
+ await runHistoryCommand(["show", id, "--json"]);
204
+ const output = consoleLogSpy.mock.calls.map((c) => c[0]).join("\n");
205
+ const parsed = JSON.parse(output);
206
+ expect(parsed.sessionId).toBe(id);
207
+ expect(parsed.questions).toBeInstanceOf(Array);
208
+ expect(parsed.questions[0].options).toBeInstanceOf(Array);
209
+ });
210
+ });
211
+ });
@@ -84,6 +84,17 @@ export async function runAnswerCommand(args) {
84
84
  }
85
85
  }
86
86
  catch (err) {
87
+ if (!jsonMode) {
88
+ process.stderr.write(`Error: Invalid --answers JSON format.\n\n` +
89
+ `Expected format:\n` +
90
+ ` auq answer <sessionId> --answers '{"0": {"selectedOption": "Label Name"}}'\n\n` +
91
+ `Answer object keys are question indices (0, 1, 2, ...). Each value can have:\n` +
92
+ ` \u2022 selectedOption: "Label" \u2014 for single-select questions\n` +
93
+ ` \u2022 selectedOptions: ["A", "B"] \u2014 for multi-select questions\n` +
94
+ ` \u2022 customText: "free text" \u2014 for custom/other input\n\n` +
95
+ `Example (multi-question):\n` +
96
+ ` auq answer abc123 --answers '{"0": {"selectedOption": "Yes"}, "1": {"customText": "my input"}}'\n\n`);
97
+ }
87
98
  outputResult({
88
99
  success: false,
89
100
  error: `Invalid answers JSON: ${err instanceof Error ? err.message : String(err)}`,
@@ -155,6 +155,36 @@ async function configGet(args) {
155
155
  return;
156
156
  }
157
157
  const value = getNestedValue(config, key);
158
+ // Special handling for renderer key: show env var override and source
159
+ if (key === "renderer") {
160
+ const envRenderer = process.env.AUQ_RENDERER;
161
+ let displayValue;
162
+ let source;
163
+ if (envRenderer) {
164
+ displayValue = envRenderer;
165
+ source = "AUQ_RENDERER";
166
+ }
167
+ else {
168
+ const paths = getConfigPaths();
169
+ const localConfig = readConfigFileForWrite(paths.local);
170
+ const globalConfig = readConfigFileForWrite(paths.global);
171
+ const explicitlySet = localConfig.renderer !== undefined || globalConfig.renderer !== undefined;
172
+ displayValue = String(value);
173
+ source = explicitlySet ? "config" : "default";
174
+ }
175
+ if (jsonMode) {
176
+ console.log(JSON.stringify({ success: true, key, value: displayValue, source }, null, 2));
177
+ }
178
+ else {
179
+ const suffix = source === "AUQ_RENDERER"
180
+ ? " (from AUQ_RENDERER)"
181
+ : source === "default"
182
+ ? " (default)"
183
+ : "";
184
+ console.log(`${key} = ${displayValue}${suffix}`);
185
+ }
186
+ return;
187
+ }
158
188
  if (jsonMode) {
159
189
  console.log(JSON.stringify({ success: true, key, value }, null, 2));
160
190
  }
@@ -210,6 +240,22 @@ async function configSet(args) {
210
240
  const issues = validation.error.issues
211
241
  .map((i) => `${i.path.join(".")}: ${i.message}`)
212
242
  .join("; ");
243
+ // Build key-specific hint for human-readable mode
244
+ if (!jsonMode) {
245
+ const hints = {
246
+ maxOptions: "Expected: number between 2 and 10",
247
+ maxQuestions: "Expected: number between 1 and 10",
248
+ sessionTimeout: "Expected: number (milliseconds, e.g. 300000 for 5 minutes)",
249
+ renderer: 'Expected: \"ink\" or \"opentui\"',
250
+ staleAction: 'Expected: \"warn\", \"remove\", or \"archive\"',
251
+ theme: "Expected: a valid theme name (see auq config get theme)",
252
+ language: "Expected: a language code (e.g. \"en\", \"ko\")",
253
+ };
254
+ const hint = hints[key];
255
+ process.stderr.write(`Error: Invalid value "${rawValue}" for key "${key}".\n\n` +
256
+ (hint ? `${hint}\n\n` : "") +
257
+ `Usage: auq config set ${key} <value> [--global]\n\n`);
258
+ }
213
259
  outputResult({
214
260
  success: false,
215
261
  error: `Invalid value for "${key}": ${issues}`,
@@ -255,6 +301,8 @@ export async function runConfigCommand(args) {
255
301
  console.log(" auq config get staleThreshold --json");
256
302
  console.log(" auq config set staleThreshold 3600000");
257
303
  console.log(" auq config set notifyOnStale false --global");
304
+ console.log(" auq config get renderer");
305
+ console.log(" auq config set renderer opentui");
258
306
  if (subcommand !== undefined) {
259
307
  process.exitCode = 1;
260
308
  }