auq-mcp-server 2.6.4 → 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.
- package/README.md +56 -2
- package/dist/bin/auq.js +36 -3
- package/dist/bin/tui-app.js +27 -6
- package/dist/package.json +7 -2
- package/dist/src/__tests__/schema-validation.test.js +61 -1
- package/dist/src/cli/commands/__tests__/fetch-answers.test.js +310 -0
- package/dist/src/cli/commands/__tests__/history.test.js +211 -0
- package/dist/src/cli/commands/answer.js +11 -0
- package/dist/src/cli/commands/config.js +48 -0
- package/dist/src/cli/commands/fetch-answers.js +205 -0
- package/dist/src/cli/commands/history.js +375 -0
- package/dist/src/config/__tests__/ConfigLoader.test.js +38 -0
- package/dist/src/config/defaults.js +1 -0
- package/dist/src/config/types.js +1 -0
- package/dist/src/core/ask-user-questions.js +63 -0
- package/dist/src/i18n/locales/en.js +2 -2
- package/dist/src/server.js +59 -2
- package/dist/src/session/ResponseFormatter.js +79 -2
- package/dist/src/session/SessionManager.js +36 -0
- package/dist/src/session/__tests__/ResponseFormatter.test.js +86 -0
- package/dist/src/session/__tests__/SessionManager.test.js +129 -0
- package/dist/src/shared/schemas.js +8 -0
- package/dist/src/tui/ThemeProvider.js +3 -3
- package/dist/src/tui/components/Header.js +2 -1
- package/dist/src/tui/components/OptionsList.js +1 -1
- package/dist/src/tui/components/SessionPicker.js +1 -1
- package/dist/src/tui/components/StepperView.js +1 -1
- package/dist/src/tui/components/__tests__/ConfirmationDialog.test.js +1 -1
- package/dist/src/tui/components/__tests__/Footer.test.js +1 -1
- package/dist/src/tui/components/__tests__/MarkdownPrompt.test.js +1 -1
- package/dist/src/tui/components/__tests__/ReviewScreen.test.js +1 -1
- package/dist/src/tui/components/__tests__/SessionDots.test.js +1 -1
- package/dist/src/tui/components/__tests__/SessionPicker.test.js +1 -1
- package/dist/src/tui/components/__tests__/StepperView.abandoned.test.js +1 -1
- package/dist/src/tui/components/__tests__/StepperView.keyboard.test.js +1 -1
- package/dist/src/tui/components/__tests__/StepperView.state.test.js +1 -1
- package/dist/src/tui/components/__tests__/WaitingScreen.test.js +1 -1
- package/dist/src/tui/shared/session-events.js +4 -0
- package/dist/src/tui/shared/themes/catppuccin-latte.js +130 -0
- package/dist/src/tui/shared/themes/catppuccin-mocha.js +131 -0
- package/dist/src/tui/shared/themes/dark.js +131 -0
- package/dist/src/tui/shared/themes/dracula.js +131 -0
- package/dist/src/tui/shared/themes/github-dark.js +129 -0
- package/dist/src/tui/shared/themes/github-light.js +129 -0
- package/dist/src/tui/shared/themes/gruvbox-dark.js +130 -0
- package/dist/src/tui/shared/themes/gruvbox-light.js +130 -0
- package/dist/src/tui/shared/themes/index.js +70 -0
- package/dist/src/tui/shared/themes/light.js +130 -0
- package/dist/src/tui/shared/themes/loader.js +111 -0
- package/dist/src/tui/shared/themes/monokai.js +132 -0
- package/dist/src/tui/shared/themes/nord.js +130 -0
- package/dist/src/tui/shared/themes/one-dark.js +131 -0
- package/dist/src/tui/shared/themes/rose-pine.js +131 -0
- package/dist/src/tui/shared/themes/solarized-dark.js +130 -0
- package/dist/src/tui/shared/themes/solarized-light.js +130 -0
- package/dist/src/tui/shared/themes/tokyo-night.js +131 -0
- package/dist/src/tui/shared/themes/types.js +1 -0
- package/dist/src/tui/shared/types.js +1 -0
- package/dist/src/tui/shared/utils/config.js +80 -0
- package/dist/src/tui/shared/utils/detectTheme.js +33 -0
- package/dist/src/tui/shared/utils/index.js +6 -0
- package/dist/src/tui/shared/utils/recommended.js +52 -0
- package/dist/src/tui/shared/utils/relativeTime.js +24 -0
- package/dist/src/tui/shared/utils/sessionSwitching.js +56 -0
- package/dist/src/tui/shared/utils/staleDetection.js +51 -0
- package/dist/src/tui/themes/catppuccin-latte.js +2 -127
- package/dist/src/tui/themes/catppuccin-mocha.js +2 -127
- package/dist/src/tui/themes/dark.js +2 -128
- package/dist/src/tui/themes/dracula.js +2 -127
- package/dist/src/tui/themes/github-dark.js +2 -126
- package/dist/src/tui/themes/github-light.js +2 -126
- package/dist/src/tui/themes/gruvbox-dark.js +2 -127
- package/dist/src/tui/themes/gruvbox-light.js +2 -127
- package/dist/src/tui/themes/index.js +2 -70
- package/dist/src/tui/themes/light.js +2 -127
- package/dist/src/tui/themes/loader.js +2 -111
- package/dist/src/tui/themes/monokai.js +2 -128
- package/dist/src/tui/themes/nord.js +2 -127
- package/dist/src/tui/themes/one-dark.js +2 -127
- package/dist/src/tui/themes/rose-pine.js +2 -128
- package/dist/src/tui/themes/solarized-dark.js +2 -127
- package/dist/src/tui/themes/solarized-light.js +2 -127
- package/dist/src/tui/themes/tokyo-night.js +2 -127
- package/dist/src/tui/themes/types.js +2 -1
- package/dist/src/tui/types.js +1 -1
- package/dist/src/tui/utils/__tests__/recommended.test.js +1 -1
- package/dist/src/tui/utils/__tests__/relativeTime.test.js +1 -1
- package/dist/src/tui/utils/__tests__/sessionSwitching.test.js +1 -1
- package/dist/src/tui/utils/__tests__/staleDetection.test.js +1 -1
- package/dist/src/tui/utils/config.js +1 -80
- package/dist/src/tui/utils/detectTheme.js +1 -22
- package/dist/src/tui/utils/recommended.js +1 -52
- package/dist/src/tui/utils/relativeTime.js +1 -24
- package/dist/src/tui/utils/sessionSwitching.js +1 -56
- package/dist/src/tui/utils/staleDetection.js +1 -51
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -212,6 +212,38 @@ Whenever you need clarification on what you are working on, never guess, and cal
|
|
|
212
212
|
|
|
213
213
|
When the AI asks questions, you'll see them appear in the AUQ TUI. Answer them **at your convenience**.
|
|
214
214
|
|
|
215
|
+
### Renderer Selection
|
|
216
|
+
|
|
217
|
+
AUQ supports two terminal rendering engines:
|
|
218
|
+
|
|
219
|
+
| Renderer | Description | Status |
|
|
220
|
+
| ----------------- | --------------------------------------------------- | ------------------- |
|
|
221
|
+
| **ink** (default) | React-based terminal renderer | Stable |
|
|
222
|
+
| **OpenTUI** | Native Zig-based renderer with improved performance | Opt-in/Experimental |
|
|
223
|
+
|
|
224
|
+
**To use OpenTUI**, set one of the following (in priority order):
|
|
225
|
+
|
|
226
|
+
1. **Environment variable** (highest priority):
|
|
227
|
+
|
|
228
|
+
```bash
|
|
229
|
+
AUQ_RENDERER=opentui auq
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
2. **Config file** (`.auqrc.json`):
|
|
233
|
+
|
|
234
|
+
```json
|
|
235
|
+
{
|
|
236
|
+
"renderer": "opentui"
|
|
237
|
+
}
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
3. **CLI command**:
|
|
241
|
+
```bash
|
|
242
|
+
auq config set renderer opentui
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
> **Note:** OpenTUI is currently opt-in and experimental. It provides native CJK character support, built-in markdown rendering with syntax highlighting, and mouse support. The environment variable overrides any config setting.
|
|
246
|
+
|
|
215
247
|
### Markdown rendering in question prompts
|
|
216
248
|
|
|
217
249
|
Question prompts now support **Markdown formatting** in the `prompt` text.
|
|
@@ -240,6 +272,15 @@ It is recommended to **disable** the built-in questioning tool in your harness (
|
|
|
240
272
|
| `Ctrl+R` | Quick Submit | Auto-select recommended options for all questions and go to review |
|
|
241
273
|
| `Esc` | Reject | Reject the whole question set and optionally explain why to the AI |
|
|
242
274
|
| `Ctrl+T` | Theme | Cycle through available color themes |
|
|
275
|
+
| `[`/`]` | Sessions | Switch to previous/next session (OpenTUI: also click session dots) |
|
|
276
|
+
|
|
277
|
+
**Mouse Support (OpenTUI renderer only):**
|
|
278
|
+
|
|
279
|
+
| Action | Description |
|
|
280
|
+
| ----------------- | ----------------------------------------------- |
|
|
281
|
+
| Click option | Select/toggle option |
|
|
282
|
+
| Scroll | Scroll through session picker or update overlay |
|
|
283
|
+
| Click session dot | Switch to that session |
|
|
243
284
|
|
|
244
285
|
<details>
|
|
245
286
|
<summary><strong>More Commands (advanced)</strong></summary>
|
|
@@ -372,10 +413,21 @@ Update checks are automatically disabled in CI environments (`CI=true`).
|
|
|
372
413
|
|
|
373
414
|
The `auq update` command always works regardless of these settings.
|
|
374
415
|
|
|
375
|
-
###
|
|
416
|
+
### Theme System
|
|
376
417
|
|
|
377
418
|
AUQ supports **16 built-in color themes** with automatic persistence. Press `Ctrl+T` to cycle through themes.
|
|
378
419
|
|
|
420
|
+
#### Theme Differences by Renderer
|
|
421
|
+
|
|
422
|
+
| Feature | ink | OpenTUI |
|
|
423
|
+
| ---------------- | ------------------ | ----------------------------------------- |
|
|
424
|
+
| Header text | Gradient animation | Solid accent color |
|
|
425
|
+
| Toast animations | `setTimeout` based | `useTimeline` based |
|
|
426
|
+
| Markdown syntax | Basic highlighting | Tree-sitter powered |
|
|
427
|
+
| Mouse support | No | Yes (click options, scroll, session dots) |
|
|
428
|
+
|
|
429
|
+
> Both renderers support all 16 built-in themes and custom themes. Colors are consistent; only implementation details differ.
|
|
430
|
+
|
|
379
431
|
<details>
|
|
380
432
|
<summary><strong>Built-in Themes</strong></summary>
|
|
381
433
|
|
|
@@ -547,12 +599,13 @@ _Settings from local config override global config, which overrides defaults._
|
|
|
547
599
|
|
|
548
600
|
```json
|
|
549
601
|
{
|
|
602
|
+
"renderer": "ink",
|
|
550
603
|
"maxOptions": 5,
|
|
551
604
|
"maxQuestions": 5,
|
|
552
605
|
"recommendedOptions": 4,
|
|
553
606
|
"recommendedQuestions": 4,
|
|
554
607
|
"sessionTimeout": 0,
|
|
555
|
-
"retentionPeriod": 604800000,
|
|
608
|
+
"retentionPeriod": 604800000,
|
|
556
609
|
"language": "auto",
|
|
557
610
|
"theme": "system",
|
|
558
611
|
"autoSelectRecommended": true,
|
|
@@ -569,6 +622,7 @@ _Settings from local config override global config, which overrides defaults._
|
|
|
569
622
|
|
|
570
623
|
| Setting | Type | Default | Range/Values | Description |
|
|
571
624
|
| ----------------------- | ------- | --------- | ------------------------------- | ----------------------------------------------------- |
|
|
625
|
+
| `renderer` | string | "ink" | "ink", "opentui" | Terminal rendering engine (ink or OpenTUI) |
|
|
572
626
|
| `maxOptions` | number | 5 | 2-10 | Maximum options per question |
|
|
573
627
|
| `maxQuestions` | number | 5 | 1-10 | Maximum questions per session |
|
|
574
628
|
| `recommendedOptions` | number | 4 | 1-10 | Suggested number of options (for AI guidance) |
|
package/dist/bin/auq.js
CHANGED
|
@@ -21,10 +21,19 @@ Commands:
|
|
|
21
21
|
sessions <sub> [flags] List/dismiss sessions
|
|
22
22
|
config <sub> [flags] Get/set configuration
|
|
23
23
|
update Check for and install updates
|
|
24
|
+
fetch-answers [flags] Fetch answers for non-blocking sessions
|
|
25
|
+
history [sub] [flags] Browse session history
|
|
26
|
+
|
|
27
|
+
Ask:
|
|
28
|
+
auq ask '<json>'
|
|
29
|
+
auq ask - (read JSON from stdin)
|
|
30
|
+
Format: {"questions":[{"prompt":"...","title":"...","options":[{"label":"..."}]}]}
|
|
24
31
|
|
|
25
32
|
Answer:
|
|
26
|
-
auq answer <id> --answers '<json>'
|
|
27
|
-
auq answer <id> --reject [--reason]
|
|
33
|
+
auq answer <id> --answers '<json>' Submit answers
|
|
34
|
+
auq answer <id> --reject [--reason "text"] Reject session
|
|
35
|
+
Format: --answers '{"0":{"selectedOption":"Label"}}'
|
|
36
|
+
Keys = question indices (0,1,2…); values: selectedOption, selectedOptions[], customText
|
|
28
37
|
Flags: --force --json
|
|
29
38
|
|
|
30
39
|
Sessions:
|
|
@@ -32,9 +41,19 @@ Sessions:
|
|
|
32
41
|
auq sessions show <id> [--json]
|
|
33
42
|
auq sessions dismiss <id> [--force] [--json]
|
|
34
43
|
|
|
44
|
+
Fetch Answers:
|
|
45
|
+
auq fetch-answers [session-id] [--blocking] [--json]
|
|
46
|
+
auq fetch-answers --unread [--json]
|
|
47
|
+
|
|
48
|
+
History:
|
|
49
|
+
auq history [--all] [--json] [--limit N] [--unread] [--search TEXT]
|
|
50
|
+
auq history show <id> [--json]
|
|
51
|
+
|
|
35
52
|
Config:
|
|
36
53
|
auq config get [key] [--json]
|
|
37
54
|
auq config set <key> <value> [--global] [--json]
|
|
55
|
+
Keys: maxOptions (2-10) maxQuestions (1-10) sessionTimeout (ms)
|
|
56
|
+
theme language renderer (ink|opentui) staleAction (warn|remove|archive)
|
|
38
57
|
|
|
39
58
|
Options:
|
|
40
59
|
-h, --help Show this help
|
|
@@ -46,7 +65,7 @@ Keys (TUI):
|
|
|
46
65
|
[/] sessions 1-9 jump Ctrl+S picker Ctrl+T theme
|
|
47
66
|
|
|
48
67
|
Config: ./.auqrc.json (local) > ~/.config/auq/.auqrc.json (global)
|
|
49
|
-
Env: AUQ_SESSION_DIR XDG_CONFIG_HOME`);
|
|
68
|
+
Env: AUQ_SESSION_DIR XDG_CONFIG_HOME AUQ_RENDERER`);
|
|
50
69
|
process.exit(0);
|
|
51
70
|
}
|
|
52
71
|
// Display version
|
|
@@ -214,6 +233,20 @@ if (command === "config") {
|
|
|
214
233
|
await updateNotification;
|
|
215
234
|
process.exit(0);
|
|
216
235
|
}
|
|
236
|
+
// Handle 'fetch-answers' command
|
|
237
|
+
if (command === "fetch-answers") {
|
|
238
|
+
const { runFetchAnswersCommand } = await import("../src/cli/commands/fetch-answers.js");
|
|
239
|
+
await runFetchAnswersCommand(args.slice(1));
|
|
240
|
+
await updateNotification;
|
|
241
|
+
process.exit(0);
|
|
242
|
+
}
|
|
243
|
+
// Handle 'history' command
|
|
244
|
+
if (command === "history") {
|
|
245
|
+
const { runHistoryCommand } = await import("../src/cli/commands/history.js");
|
|
246
|
+
await runHistoryCommand(args.slice(1));
|
|
247
|
+
await updateNotification;
|
|
248
|
+
process.exit(0);
|
|
249
|
+
}
|
|
217
250
|
// Default: Start TUI
|
|
218
251
|
// Important: Lazy-load Ink/React so non-interactive commands (ask/server) don't pull them in.
|
|
219
252
|
// Also force production mode before importing React/Ink to avoid perf_hooks measure accumulation warnings.
|
package/dist/bin/tui-app.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Box, render, Text, useInput } from "ink";
|
|
2
2
|
import { promises as fs } from "fs";
|
|
3
3
|
import React, { useCallback, useEffect, useMemo, useRef, useState, } from "react";
|
|
4
|
+
import { DEFAULT_CONFIG } from "../src/config/defaults.js";
|
|
4
5
|
import { ensureDirectoryExists, getSessionDirectory, } from "../src/session/utils.js";
|
|
5
6
|
import { Header } from "../src/tui/components/Header.js";
|
|
6
7
|
import { SessionDots } from "../src/tui/components/SessionDots.js";
|
|
@@ -11,10 +12,10 @@ import { Toast } from "../src/tui/components/Toast.js";
|
|
|
11
12
|
import { WaitingScreen } from "../src/tui/components/WaitingScreen.js";
|
|
12
13
|
import { createNotificationBatcher, showProgress, clearProgress, calculateProgress, checkLinuxDependencies, } from "../src/tui/notifications/index.js";
|
|
13
14
|
import { createTUIWatcher } from "../src/tui/session-watcher.js";
|
|
14
|
-
import { isSessionStale, isSessionAbandoned, formatStaleToastMessage, } from "../src/tui/utils/staleDetection.js";
|
|
15
|
+
import { isSessionStale, isSessionAbandoned, formatStaleToastMessage, } from "../src/tui/shared/utils/staleDetection.js";
|
|
15
16
|
import { ThemeProvider } from "../src/tui/ThemeProvider.js";
|
|
16
17
|
import { ConfigProvider } from "../src/tui/ConfigContext.js";
|
|
17
|
-
import { getAdjustedIndexAfterRemoval, getDirectJumpIndex, getNextSessionIndex, getPrevSessionIndex, } from "../src/tui/utils/sessionSwitching.js";
|
|
18
|
+
import { getAdjustedIndexAfterRemoval, getDirectJumpIndex, getNextSessionIndex, getPrevSessionIndex, } from "../src/tui/shared/utils/sessionSwitching.js";
|
|
18
19
|
import { UpdateChecker, fetchChangelog, installUpdate, detectPackageManager, readCache, writeCache, } from "../src/update/index.js";
|
|
19
20
|
import { UpdateOverlay } from "../src/tui/components/UpdateOverlay.js";
|
|
20
21
|
import { KEYS } from "../src/tui/constants/keybindings.js";
|
|
@@ -37,6 +38,7 @@ const App = ({ config }) => {
|
|
|
37
38
|
const [installError, setInstallError] = useState(null);
|
|
38
39
|
const [changelogContent, setChangelogContent] = useState(null);
|
|
39
40
|
const [updateDismissed, setUpdateDismissed] = useState(false);
|
|
41
|
+
const [isCheckingUpdate, setIsCheckingUpdate] = useState(false);
|
|
40
42
|
// Get session directory for logging
|
|
41
43
|
const sessionDir = getSessionDirectory();
|
|
42
44
|
// Notification configuration from config
|
|
@@ -182,7 +184,8 @@ const App = ({ config }) => {
|
|
|
182
184
|
// Silently fail — update checks should never break the TUI
|
|
183
185
|
}
|
|
184
186
|
};
|
|
185
|
-
|
|
187
|
+
setIsCheckingUpdate(true);
|
|
188
|
+
void runCheck().finally(() => { setIsCheckingUpdate(false); });
|
|
186
189
|
intervalId = setInterval(() => {
|
|
187
190
|
checker.clearCache();
|
|
188
191
|
runCheck();
|
|
@@ -540,7 +543,7 @@ const App = ({ config }) => {
|
|
|
540
543
|
updateType: updateInfo.updateType,
|
|
541
544
|
latestVersion: updateInfo.latestVersion,
|
|
542
545
|
}
|
|
543
|
-
: null, onUpdateBadgeActivate: () => setShowUpdateOverlay(true) }),
|
|
546
|
+
: null, onUpdateBadgeActivate: () => setShowUpdateOverlay(true), isCheckingUpdate: isCheckingUpdate }),
|
|
544
547
|
mainContent,
|
|
545
548
|
state.mode === "PROCESSING" && sessionQueue.length >= 2 && (React.createElement(SessionDots, { sessions: sessionQueue.map((s) => ({
|
|
546
549
|
...s,
|
|
@@ -564,7 +567,7 @@ const App = ({ config }) => {
|
|
|
564
567
|
showUpdateOverlay && updateInfo && (React.createElement(UpdateOverlay, { isOpen: showUpdateOverlay, currentVersion: updateInfo.currentVersion, latestVersion: updateInfo.latestVersion, updateType: updateInfo.updateType, changelog: changelogContent, changelogUrl: updateInfo.changelogUrl, isInstalling: isInstallingUpdate, installError: installError, onInstall: handleUpdateInstall, onSkipVersion: handleSkipVersion, onRemindLater: handleRemindLater })),
|
|
565
568
|
React.createElement(ThemeIndicator, null)))));
|
|
566
569
|
};
|
|
567
|
-
|
|
570
|
+
async function runInkTui(config) {
|
|
568
571
|
// Clear terminal before showing app
|
|
569
572
|
console.clear();
|
|
570
573
|
// 1-tick AI agent hint: ANSI hidden attribute makes it invisible to humans,
|
|
@@ -576,8 +579,26 @@ export const runTui = (config) => {
|
|
|
576
579
|
process.exit(0);
|
|
577
580
|
});
|
|
578
581
|
// Show goodbye after Ink unmounts
|
|
579
|
-
waitUntilExit().then(() => {
|
|
582
|
+
await waitUntilExit().then(() => {
|
|
580
583
|
process.stdout.write("\n");
|
|
581
584
|
console.log("👋 Goodbye! See you next time.");
|
|
582
585
|
});
|
|
586
|
+
}
|
|
587
|
+
export const runTui = async (config) => {
|
|
588
|
+
const mergedConfig = { ...DEFAULT_CONFIG, ...config };
|
|
589
|
+
const rendererType = process.env.AUQ_RENDERER || mergedConfig.renderer || "opentui";
|
|
590
|
+
if (rendererType === "opentui") {
|
|
591
|
+
try {
|
|
592
|
+
const opentuiPath = "../src/tui-opentui/app.js";
|
|
593
|
+
const { runTui: runOpenTui } = (await import(opentuiPath));
|
|
594
|
+
await runOpenTui(mergedConfig);
|
|
595
|
+
}
|
|
596
|
+
catch (err) {
|
|
597
|
+
console.warn(`⚠️ OpenTUI failed to initialize: ${err instanceof Error ? err.message : String(err)}. Falling back to ink renderer.`);
|
|
598
|
+
await runInkTui(mergedConfig);
|
|
599
|
+
}
|
|
600
|
+
}
|
|
601
|
+
else {
|
|
602
|
+
await runInkTui(mergedConfig);
|
|
603
|
+
}
|
|
583
604
|
};
|
package/dist/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "auq-mcp-server",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.7.0",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"bin": {
|
|
6
6
|
"auq": "dist/bin/auq.js"
|
|
@@ -28,7 +28,10 @@
|
|
|
28
28
|
"generate:skill": "bun run scripts/generate-skill.ts",
|
|
29
29
|
"validate:skill": "bunx skills-ref validate skills/ask-user-questions",
|
|
30
30
|
"test": "vitest run",
|
|
31
|
-
"format": "prettier --write . && eslint --fix ."
|
|
31
|
+
"format": "prettier --write . && eslint --fix .",
|
|
32
|
+
"build:opentui": "tsc -p tsconfig.opentui.json --noEmit",
|
|
33
|
+
"test:opentui": "bun test src/tui-opentui",
|
|
34
|
+
"typecheck:all": "tsc --noEmit && tsc -p tsconfig.opentui.json --noEmit"
|
|
32
35
|
},
|
|
33
36
|
"keywords": [
|
|
34
37
|
"fastmcp",
|
|
@@ -50,6 +53,8 @@
|
|
|
50
53
|
"description": "An MCP server that provides a tool to ask a user questions via the terminal",
|
|
51
54
|
"dependencies": {
|
|
52
55
|
"@inkjs/ui": "^2.0.0",
|
|
56
|
+
"@opentui/core": "0.1.87",
|
|
57
|
+
"@opentui/react": "0.1.87",
|
|
53
58
|
"@modelcontextprotocol/sdk": "1.17.2",
|
|
54
59
|
"@types/uuid": "^10.0.0",
|
|
55
60
|
"chalk": "^5.6.2",
|
|
@@ -3,7 +3,8 @@
|
|
|
3
3
|
* Tests the most common edge cases to catch obvious bugs
|
|
4
4
|
*/
|
|
5
5
|
import { describe, expect, it } from "vitest";
|
|
6
|
-
import { QuestionSchema, QuestionsSchema } from "../core/ask-user-questions.js";
|
|
6
|
+
import { QuestionSchema, QuestionsSchema, AskUserQuestionsParametersSchema } from "../core/ask-user-questions.js";
|
|
7
|
+
import { GetAnsweredQuestionsArgsSchema } from "../shared/schemas.js";
|
|
7
8
|
describe("Schema Validation - Edge Cases", () => {
|
|
8
9
|
describe("Invalid Input (should reject)", () => {
|
|
9
10
|
it("should reject missing title field", () => {
|
|
@@ -176,3 +177,62 @@ describe("Schema Validation - Edge Cases", () => {
|
|
|
176
177
|
});
|
|
177
178
|
});
|
|
178
179
|
});
|
|
180
|
+
describe("nonBlocking parameter", () => {
|
|
181
|
+
const validQuestion = {
|
|
182
|
+
options: [{ label: "Option A" }, { label: "Option B" }],
|
|
183
|
+
prompt: "Which do you prefer?",
|
|
184
|
+
title: "Preference",
|
|
185
|
+
multiSelect: false,
|
|
186
|
+
};
|
|
187
|
+
it("should accept nonBlocking: true", () => {
|
|
188
|
+
const result = AskUserQuestionsParametersSchema.parse({
|
|
189
|
+
questions: [validQuestion],
|
|
190
|
+
nonBlocking: true,
|
|
191
|
+
});
|
|
192
|
+
expect(result.nonBlocking).toBe(true);
|
|
193
|
+
});
|
|
194
|
+
it("should accept nonBlocking: false", () => {
|
|
195
|
+
const result = AskUserQuestionsParametersSchema.parse({
|
|
196
|
+
questions: [validQuestion],
|
|
197
|
+
nonBlocking: false,
|
|
198
|
+
});
|
|
199
|
+
expect(result.nonBlocking).toBe(false);
|
|
200
|
+
});
|
|
201
|
+
it("should default nonBlocking to false when omitted", () => {
|
|
202
|
+
const result = AskUserQuestionsParametersSchema.parse({
|
|
203
|
+
questions: [validQuestion],
|
|
204
|
+
});
|
|
205
|
+
expect(result.nonBlocking).toBe(false);
|
|
206
|
+
});
|
|
207
|
+
it("should reject non-boolean nonBlocking", () => {
|
|
208
|
+
expect(() => AskUserQuestionsParametersSchema.parse({
|
|
209
|
+
questions: [validQuestion],
|
|
210
|
+
nonBlocking: "yes",
|
|
211
|
+
})).toThrow();
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
describe("GetAnsweredQuestionsArgsSchema", () => {
|
|
215
|
+
it("should accept valid session_id", () => {
|
|
216
|
+
const result = GetAnsweredQuestionsArgsSchema.parse({
|
|
217
|
+
session_id: "a3f2e8b1-1234-4567-89ab-cdef01234567",
|
|
218
|
+
});
|
|
219
|
+
expect(result.session_id).toBe("a3f2e8b1-1234-4567-89ab-cdef01234567");
|
|
220
|
+
expect(result.blocking).toBe(false);
|
|
221
|
+
});
|
|
222
|
+
it("should accept session_id with blocking: true", () => {
|
|
223
|
+
const result = GetAnsweredQuestionsArgsSchema.parse({
|
|
224
|
+
session_id: "a3f2e8b1",
|
|
225
|
+
blocking: true,
|
|
226
|
+
});
|
|
227
|
+
expect(result.blocking).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
it("should reject missing session_id", () => {
|
|
230
|
+
expect(() => GetAnsweredQuestionsArgsSchema.parse({})).toThrow();
|
|
231
|
+
});
|
|
232
|
+
it("should reject non-boolean blocking", () => {
|
|
233
|
+
expect(() => GetAnsweredQuestionsArgsSchema.parse({
|
|
234
|
+
session_id: "test",
|
|
235
|
+
blocking: "yes",
|
|
236
|
+
})).toThrow();
|
|
237
|
+
});
|
|
238
|
+
});
|
|
@@ -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
|
+
});
|