@memoryrelay/plugin-memoryrelay-ai 0.12.0 → 0.12.1

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.
@@ -0,0 +1,233 @@
1
+ /**
2
+ * Daily Memory Stats for Heartbeat (Phase 1 - Issue #10)
3
+ *
4
+ * Provides morning and evening memory stat summaries for agent heartbeat checks.
5
+ * Shows growth trends, categories, and most valuable memories.
6
+ */
7
+
8
+ export interface DailyStatsConfig {
9
+ enabled: boolean;
10
+ morningTime?: string; // HH:MM format (default: "09:00")
11
+ eveningTime?: string; // HH:MM format (default: "20:00")
12
+ timezone?: string; // IANA timezone (default: system timezone)
13
+ }
14
+
15
+ export interface MemoryStats {
16
+ total: number;
17
+ today: number;
18
+ thisWeek: number;
19
+ weeklyGrowth: number; // Percentage change from last week
20
+ topCategories: Array<{ category: string; count: number }>;
21
+ mostValuable?: {
22
+ id: string;
23
+ content: string;
24
+ recallCount: number;
25
+ };
26
+ }
27
+
28
+ export interface HeartbeatResult {
29
+ shouldNotify: boolean;
30
+ message?: string;
31
+ stats?: MemoryStats;
32
+ }
33
+
34
+ /**
35
+ * Calculate memory statistics for the current period
36
+ */
37
+ export async function calculateStats(
38
+ getAllMemories: () => Promise<Array<{ id: string; content: string; metadata: Record<string, string>; created_at: number }>>,
39
+ getRecallCount: (memoryId: string) => number = () => 0
40
+ ): Promise<MemoryStats> {
41
+ const memories = await getAllMemories();
42
+ const now = Date.now();
43
+ const todayStart = new Date().setHours(0, 0, 0, 0);
44
+ const weekStart = now - 7 * 24 * 60 * 60 * 1000;
45
+ const lastWeekStart = now - 14 * 24 * 60 * 60 * 1000;
46
+
47
+ // Count memories by period
48
+ const total = memories.length;
49
+ const today = memories.filter((m) => m.created_at >= todayStart).length;
50
+ const thisWeek = memories.filter((m) => m.created_at >= weekStart).length;
51
+ const lastWeek = memories.filter(
52
+ (m) => m.created_at >= lastWeekStart && m.created_at < weekStart
53
+ ).length;
54
+
55
+ // Calculate weekly growth
56
+ const weeklyGrowth = lastWeek > 0 ? ((thisWeek - lastWeek) / lastWeek) * 100 : 0;
57
+
58
+ // Top categories
59
+ const categoryCount = new Map<string, number>();
60
+ for (const memory of memories) {
61
+ const category = memory.metadata.category || "uncategorized";
62
+ categoryCount.set(category, (categoryCount.get(category) || 0) + 1);
63
+ }
64
+
65
+ const topCategories = Array.from(categoryCount.entries())
66
+ .map(([category, count]) => ({ category, count }))
67
+ .sort((a, b) => b.count - a.count)
68
+ .slice(0, 5);
69
+
70
+ // Most valuable memory (by recall count - simulated for now)
71
+ let mostValuable: MemoryStats["mostValuable"];
72
+ if (memories.length > 0) {
73
+ const memoriesWithRecalls = memories.map((m) => ({
74
+ ...m,
75
+ recallCount: getRecallCount(m.id),
76
+ }));
77
+ memoriesWithRecalls.sort((a, b) => b.recallCount - a.recallCount);
78
+
79
+ if (memoriesWithRecalls[0].recallCount > 0) {
80
+ mostValuable = {
81
+ id: memoriesWithRecalls[0].id,
82
+ content: memoriesWithRecalls[0].content.slice(0, 100) + "...",
83
+ recallCount: memoriesWithRecalls[0].recallCount,
84
+ };
85
+ }
86
+ }
87
+
88
+ return {
89
+ total,
90
+ today,
91
+ thisWeek,
92
+ weeklyGrowth,
93
+ topCategories,
94
+ mostValuable,
95
+ };
96
+ }
97
+
98
+ /**
99
+ * Morning check: Show memory stats and growth
100
+ */
101
+ export async function morningCheck(
102
+ stats: MemoryStats
103
+ ): Promise<HeartbeatResult> {
104
+ // Don't notify if no activity or very early in adoption
105
+ if (stats.total < 5 && stats.today === 0) {
106
+ return { shouldNotify: false };
107
+ }
108
+
109
+ // Build morning message
110
+ const lines: string[] = [];
111
+ lines.push("📊 Morning Memory Check");
112
+ lines.push("");
113
+ lines.push(`📚 Total memories: ${stats.total}`);
114
+
115
+ if (stats.today > 0) {
116
+ lines.push(`✨ Added today: ${stats.today}`);
117
+ }
118
+
119
+ if (stats.thisWeek > 0) {
120
+ const growthIndicator = stats.weeklyGrowth > 0 ? "📈" : stats.weeklyGrowth < 0 ? "📉" : "➡️";
121
+ lines.push(`${growthIndicator} This week: ${stats.thisWeek} (${stats.weeklyGrowth > 0 ? '+' : ''}${stats.weeklyGrowth.toFixed(0)}%)`);
122
+ }
123
+
124
+ if (stats.topCategories.length > 0) {
125
+ lines.push("");
126
+ lines.push("🏆 Top categories:");
127
+ for (const cat of stats.topCategories.slice(0, 3)) {
128
+ lines.push(` • ${cat.category}: ${cat.count}`);
129
+ }
130
+ }
131
+
132
+ return {
133
+ shouldNotify: true,
134
+ message: lines.join("\n"),
135
+ stats,
136
+ };
137
+ }
138
+
139
+ /**
140
+ * Evening review: Show today's activity and most valuable memory
141
+ */
142
+ export async function eveningReview(
143
+ stats: MemoryStats
144
+ ): Promise<HeartbeatResult> {
145
+ // Don't notify if no activity today
146
+ if (stats.today === 0 && !stats.mostValuable) {
147
+ return { shouldNotify: false };
148
+ }
149
+
150
+ const lines: string[] = [];
151
+ lines.push("🌙 Evening Memory Review");
152
+ lines.push("");
153
+
154
+ if (stats.today > 0) {
155
+ lines.push(`✅ Stored today: ${stats.today} memories`);
156
+ } else {
157
+ lines.push("📝 No new memories today");
158
+ }
159
+
160
+ if (stats.mostValuable) {
161
+ lines.push("");
162
+ lines.push("💎 Most valuable memory:");
163
+ lines.push(` "${stats.mostValuable.content}"`);
164
+ lines.push(` Recalled ${stats.mostValuable.recallCount} times`);
165
+ }
166
+
167
+ return {
168
+ shouldNotify: stats.today > 0 || (stats.mostValuable !== undefined),
169
+ message: lines.join("\n"),
170
+ stats,
171
+ };
172
+ }
173
+
174
+ /**
175
+ * Check if it's time for a heartbeat notification
176
+ */
177
+ export function shouldRunHeartbeat(
178
+ config: DailyStatsConfig,
179
+ currentTime: Date = new Date()
180
+ ): "morning" | "evening" | null {
181
+ if (!config.enabled) return null;
182
+
183
+ const morningTime = config.morningTime || "09:00";
184
+ const eveningTime = config.eveningTime || "20:00";
185
+
186
+ const [morningHour, morningMin] = morningTime.split(":").map(Number);
187
+ const [eveningHour, eveningMin] = eveningTime.split(":").map(Number);
188
+
189
+ const currentHour = currentTime.getHours();
190
+ const currentMin = currentTime.getMinutes();
191
+
192
+ // Check if within 5-minute window of morning time
193
+ const morningStart = morningHour * 60 + morningMin;
194
+ const morningEnd = morningStart + 5;
195
+ const currentMinutes = currentHour * 60 + currentMin;
196
+
197
+ if (currentMinutes >= morningStart && currentMinutes < morningEnd) {
198
+ return "morning";
199
+ }
200
+
201
+ // Check if within 5-minute window of evening time
202
+ const eveningStart = eveningHour * 60 + eveningMin;
203
+ const eveningEnd = eveningStart + 5;
204
+
205
+ if (currentMinutes >= eveningStart && currentMinutes < eveningEnd) {
206
+ return "evening";
207
+ }
208
+
209
+ return null;
210
+ }
211
+
212
+ /**
213
+ * Format stats for console/log output
214
+ */
215
+ export function formatStatsForDisplay(stats: MemoryStats): string {
216
+ const lines: string[] = [];
217
+ lines.push(`Total: ${stats.total} | Today: ${stats.today} | Week: ${stats.thisWeek}`);
218
+
219
+ if (stats.weeklyGrowth !== 0) {
220
+ const sign = stats.weeklyGrowth > 0 ? "+" : "";
221
+ lines.push(`Growth: ${sign}${stats.weeklyGrowth.toFixed(0)}%`);
222
+ }
223
+
224
+ if (stats.topCategories.length > 0) {
225
+ const topCats = stats.topCategories
226
+ .slice(0, 3)
227
+ .map((c) => `${c.category}(${c.count})`)
228
+ .join(", ");
229
+ lines.push(`Top: ${topCats}`);
230
+ }
231
+
232
+ return lines.join(" | ");
233
+ }
@@ -0,0 +1,236 @@
1
+ /**
2
+ * First-Run Onboarding Wizard (Phase 1 - Issue #9)
3
+ *
4
+ * Detects first run and guides user through initial setup
5
+ * Stores first memory, configures auto-capture preferences
6
+ */
7
+
8
+ import * as fs from "fs/promises";
9
+ import * as path from "path";
10
+ import * as os from "os";
11
+
12
+ export interface OnboardingState {
13
+ completed: boolean;
14
+ completedAt?: number;
15
+ firstMemoryId?: string;
16
+ autoCaptureOptIn?: boolean;
17
+ }
18
+
19
+ export interface OnboardingResult {
20
+ isFirstRun: boolean;
21
+ shouldOnboard: boolean;
22
+ state?: OnboardingState;
23
+ }
24
+
25
+ /**
26
+ * Get onboarding state file path
27
+ */
28
+ function getStateFilePath(): string {
29
+ const homeDir = os.homedir();
30
+ const openclawDir = path.join(homeDir, ".openclaw");
31
+ return path.join(openclawDir, "memoryrelay-onboarding.json");
32
+ }
33
+
34
+ /**
35
+ * Check if this is the first run
36
+ */
37
+ export async function checkFirstRun(
38
+ getTotalMemories: () => Promise<number>
39
+ ): Promise<OnboardingResult> {
40
+ const stateFile = getStateFilePath();
41
+
42
+ try {
43
+ // Check if state file exists
44
+ const stateContent = await fs.readFile(stateFile, "utf-8");
45
+ const state: OnboardingState = JSON.parse(stateContent);
46
+
47
+ // Already onboarded
48
+ if (state.completed) {
49
+ return {
50
+ isFirstRun: false,
51
+ shouldOnboard: false,
52
+ state,
53
+ };
54
+ }
55
+ } catch (err) {
56
+ // State file doesn't exist - might be first run
57
+ }
58
+
59
+ // Check if there are any memories
60
+ const totalMemories = await getTotalMemories();
61
+
62
+ // First run if: no state file AND no memories
63
+ const isFirstRun = totalMemories === 0;
64
+
65
+ return {
66
+ isFirstRun,
67
+ shouldOnboard: isFirstRun,
68
+ };
69
+ }
70
+
71
+ /**
72
+ * Mark onboarding as complete
73
+ */
74
+ export async function markOnboardingComplete(
75
+ firstMemoryId: string,
76
+ autoCaptureOptIn: boolean
77
+ ): Promise<void> {
78
+ const stateFile = getStateFilePath();
79
+ const state: OnboardingState = {
80
+ completed: true,
81
+ completedAt: Date.now(),
82
+ firstMemoryId,
83
+ autoCaptureOptIn,
84
+ };
85
+
86
+ // Ensure directory exists
87
+ const dir = path.dirname(stateFile);
88
+ await fs.mkdir(dir, { recursive: true });
89
+
90
+ // Write state file
91
+ await fs.writeFile(stateFile, JSON.stringify(state, null, 2), "utf-8");
92
+ }
93
+
94
+ /**
95
+ * Generate onboarding prompt text
96
+ */
97
+ export function generateOnboardingPrompt(): string {
98
+ const lines: string[] = [];
99
+
100
+ lines.push("👋 Welcome to MemoryRelay!");
101
+ lines.push("");
102
+ lines.push("This is your first time using MemoryRelay. Let's get you set up!");
103
+ lines.push("");
104
+ lines.push("MemoryRelay helps you remember important information across conversations.");
105
+ lines.push("It uses semantic search to recall relevant context when you need it.");
106
+ lines.push("");
107
+ lines.push("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
108
+ lines.push("");
109
+ lines.push("🎯 QUICK START");
110
+ lines.push("");
111
+ lines.push("1. Store your first memory (try: 'I prefer concise responses')");
112
+ lines.push("2. Later, I'll automatically recall it when relevant");
113
+ lines.push("3. Your memories grow smarter over time");
114
+ lines.push("");
115
+ lines.push("━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━");
116
+ lines.push("");
117
+ lines.push("⚙️ SMART AUTO-CAPTURE");
118
+ lines.push("");
119
+ lines.push("MemoryRelay can automatically detect and store important information:");
120
+ lines.push("");
121
+ lines.push(" ✅ Credentials (API keys, connection strings)");
122
+ lines.push(" ✅ Preferences (communication style, tool choices)");
123
+ lines.push(" ✅ Technical facts (commands, configs, troubleshooting)");
124
+ lines.push(" ❌ Personal info (requires your confirmation first)");
125
+ lines.push("");
126
+ lines.push("Your first 5 auto-captures will ask for confirmation to ensure");
127
+ lines.push("you're comfortable with what's being stored. After that, it runs");
128
+ lines.push("silently in the background.");
129
+ lines.push("");
130
+ lines.push("Would you like to enable smart auto-capture? (Recommended)");
131
+ lines.push("");
132
+ lines.push("Type 'yes' to enable, or 'no' to manually store memories");
133
+ lines.push("");
134
+
135
+ return lines.join("\n");
136
+ }
137
+
138
+ /**
139
+ * Generate success message after onboarding
140
+ */
141
+ export function generateSuccessMessage(
142
+ firstMemoryContent: string,
143
+ autoCaptureEnabled: boolean
144
+ ): string {
145
+ const lines: string[] = [];
146
+
147
+ lines.push("✅ Onboarding Complete!");
148
+ lines.push("");
149
+ lines.push(`Your first memory has been stored:`);
150
+ lines.push(`"${firstMemoryContent}"`);
151
+ lines.push("");
152
+
153
+ if (autoCaptureEnabled) {
154
+ lines.push("🎯 Smart auto-capture is enabled");
155
+ lines.push(" I'll detect and store important information automatically");
156
+ lines.push(" (Your first 5 captures will ask for confirmation)");
157
+ } else {
158
+ lines.push("📝 Manual mode selected");
159
+ lines.push(" Use memory_store tool to save memories manually");
160
+ lines.push(" You can enable auto-capture anytime in config");
161
+ }
162
+
163
+ lines.push("");
164
+ lines.push("💡 Quick tips:");
165
+ lines.push(" • Store preferences, facts, commands, and insights");
166
+ lines.push(" • I'll automatically recall relevant memories in future conversations");
167
+ lines.push(" • Check stats anytime with: openclaw gateway-call memoryrelay.stats");
168
+ lines.push(" • Morning/evening summaries show your memory growth");
169
+ lines.push("");
170
+ lines.push("Ready to remember everything! 🚀");
171
+ lines.push("");
172
+
173
+ return lines.join("\n");
174
+ }
175
+
176
+ /**
177
+ * Interactive onboarding flow (for CLI or chat)
178
+ */
179
+ export async function runOnboardingFlow(
180
+ storeMemory: (content: string, metadata?: Record<string, string>) => Promise<{ id: string }>,
181
+ getUserResponse: (prompt: string) => Promise<string>
182
+ ): Promise<{
183
+ completed: boolean;
184
+ firstMemoryId?: string;
185
+ autoCaptureEnabled?: boolean;
186
+ }> {
187
+ // Show welcome
188
+ const welcomePrompt = generateOnboardingPrompt();
189
+ const autoCaptureResponse = await getUserResponse(welcomePrompt);
190
+
191
+ // Parse auto-capture preference
192
+ const autoCaptureEnabled = autoCaptureResponse.toLowerCase().includes("yes");
193
+
194
+ // Prompt for first memory
195
+ const firstMemoryPrompt = [
196
+ "",
197
+ "Great! Let's store your first memory.",
198
+ "",
199
+ "What would you like me to remember?",
200
+ "(Examples: 'I prefer Python', 'My project uses PostgreSQL', 'I'm building a chatbot')",
201
+ "",
202
+ ].join("\n");
203
+
204
+ const firstMemoryContent = await getUserResponse(firstMemoryPrompt);
205
+
206
+ // Store first memory
207
+ const result = await storeMemory(firstMemoryContent, {
208
+ category: "onboarding",
209
+ source: "first-run-wizard",
210
+ });
211
+
212
+ // Mark complete
213
+ await markOnboardingComplete(result.id, autoCaptureEnabled);
214
+
215
+ return {
216
+ completed: true,
217
+ firstMemoryId: result.id,
218
+ autoCaptureEnabled,
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Simple non-interactive onboarding (for automatic setup)
224
+ */
225
+ export async function runSimpleOnboarding(
226
+ storeMemory: (content: string, metadata?: Record<string, string>) => Promise<{ id: string }>,
227
+ defaultMemory: string = "MemoryRelay onboarding complete",
228
+ autoCaptureEnabled: boolean = true
229
+ ): Promise<void> {
230
+ const result = await storeMemory(defaultMemory, {
231
+ category: "system",
232
+ source: "auto-onboarding",
233
+ });
234
+
235
+ await markOnboardingComplete(result.id, autoCaptureEnabled);
236
+ }
@@ -0,0 +1,230 @@
1
+ /**
2
+ * StatusReporter Tests (Simplified)
3
+ *
4
+ * Tests matching actual implementation
5
+ */
6
+
7
+ import { describe, test, expect, beforeEach } from "vitest";
8
+ import { StatusReporter, type ConnectionStatus, type PluginConfig, type MemoryStats } from "./status-reporter";
9
+ import { DebugLogger } from "./debug-logger";
10
+
11
+ describe("StatusReporter", () => {
12
+ let reporter: StatusReporter;
13
+ let debugLogger: DebugLogger;
14
+ let mockConnection: ConnectionStatus;
15
+ let mockConfig: PluginConfig;
16
+ let mockStats: MemoryStats;
17
+
18
+ beforeEach(() => {
19
+ debugLogger = new DebugLogger({
20
+ enabled: true,
21
+ verbose: false,
22
+ maxEntries: 100,
23
+ });
24
+ reporter = new StatusReporter(debugLogger);
25
+
26
+ mockConnection = {
27
+ status: "connected",
28
+ endpoint: "https://api.memoryrelay.net",
29
+ lastCheck: new Date().toISOString(),
30
+ responseTime: 45,
31
+ };
32
+
33
+ mockConfig = {
34
+ agentId: "test-agent",
35
+ autoRecall: true,
36
+ autoCapture: false,
37
+ recallLimit: 5,
38
+ recallThreshold: 0.3,
39
+ excludeChannels: [],
40
+ };
41
+
42
+ mockStats = {
43
+ total_memories: 100,
44
+ };
45
+ });
46
+
47
+ test("records and clears tool failures", () => {
48
+ reporter.recordFailure("memory_store", "500 Error");
49
+ let issues = reporter.getIssues();
50
+ expect(issues).toHaveLength(1);
51
+ expect(issues[0].tool).toBe("memory_store");
52
+
53
+ reporter.recordSuccess("memory_store");
54
+ issues = reporter.getIssues();
55
+ expect(issues).toHaveLength(0);
56
+ });
57
+
58
+ test("buildReport creates status report", () => {
59
+ const toolGroups = {
60
+ "Core Memory": ["memory_store", "memory_recall"],
61
+ "Projects": ["project_list"],
62
+ };
63
+
64
+ const report = reporter.buildReport(
65
+ mockConnection,
66
+ mockConfig,
67
+ mockStats,
68
+ toolGroups,
69
+ );
70
+
71
+ expect(report.connection).toEqual(mockConnection);
72
+ expect(report.config).toEqual(mockConfig);
73
+ expect(report.stats).toEqual(mockStats);
74
+ expect(report.tools["Core Memory"]).toBeDefined();
75
+ expect(report.tools["Projects"]).toBeDefined();
76
+ });
77
+
78
+ test("buildReport includes tool status from debug logs", () => {
79
+ debugLogger.log({
80
+ timestamp: new Date().toISOString(),
81
+ tool: "memory_store",
82
+ method: "POST",
83
+ path: "/v1/memories",
84
+ duration: 142,
85
+ status: "success",
86
+ });
87
+
88
+ const toolGroups = {
89
+ "Core Memory": ["memory_store"],
90
+ };
91
+
92
+ const report = reporter.buildReport(
93
+ mockConnection,
94
+ mockConfig,
95
+ mockStats,
96
+ toolGroups,
97
+ );
98
+
99
+ const memoryTools = report.tools["Core Memory"];
100
+ expect(memoryTools.available).toBe(1);
101
+ expect(memoryTools.failed).toBe(0);
102
+ expect(memoryTools.tools[0].status).toBe("working");
103
+ });
104
+
105
+ test("buildReport shows failed tools", () => {
106
+ debugLogger.log({
107
+ timestamp: new Date().toISOString(),
108
+ tool: "memory_store",
109
+ method: "POST",
110
+ path: "/v1/memories",
111
+ duration: 156,
112
+ status: "error",
113
+ error: "500 Internal Server Error",
114
+ });
115
+
116
+ const toolGroups = {
117
+ "Core Memory": ["memory_store"],
118
+ };
119
+
120
+ const report = reporter.buildReport(
121
+ mockConnection,
122
+ mockConfig,
123
+ mockStats,
124
+ toolGroups,
125
+ );
126
+
127
+ const memoryTools = report.tools["Core Memory"];
128
+ expect(memoryTools.available).toBe(0);
129
+ expect(memoryTools.failed).toBe(1);
130
+ expect(memoryTools.tools[0].status).toBe("error");
131
+ expect(memoryTools.tools[0].error).toBe("500 Internal Server Error");
132
+ });
133
+
134
+ test("buildReport includes recent calls", () => {
135
+ debugLogger.log({
136
+ timestamp: new Date().toISOString(),
137
+ tool: "memory_store",
138
+ method: "POST",
139
+ path: "/v1/memories",
140
+ duration: 142,
141
+ status: "success",
142
+ });
143
+
144
+ const toolGroups = {
145
+ "Core Memory": ["memory_store"],
146
+ };
147
+
148
+ const report = reporter.buildReport(
149
+ mockConnection,
150
+ mockConfig,
151
+ mockStats,
152
+ toolGroups,
153
+ );
154
+
155
+ expect(report.recentCalls).toHaveLength(1);
156
+ expect(report.recentCalls[0].tool).toBe("memory_store");
157
+ });
158
+
159
+ test("formatReport creates human-readable output", () => {
160
+ const toolGroups = {
161
+ "Core Memory": ["memory_store", "memory_recall"],
162
+ };
163
+
164
+ const report = reporter.buildReport(
165
+ mockConnection,
166
+ mockConfig,
167
+ mockStats,
168
+ toolGroups,
169
+ );
170
+
171
+ const formatted = StatusReporter.formatReport(report);
172
+ expect(formatted).toContain("MemoryRelay Plugin Status");
173
+ expect(formatted).toContain("connected");
174
+ expect(formatted).toContain("Core Memory");
175
+ });
176
+
177
+ test("formatCompact creates brief output", () => {
178
+ const toolGroups = {
179
+ "Core Memory": ["memory_store"],
180
+ };
181
+
182
+ const report = reporter.buildReport(
183
+ mockConnection,
184
+ mockConfig,
185
+ mockStats,
186
+ toolGroups,
187
+ );
188
+
189
+ const compact = StatusReporter.formatCompact(report);
190
+ expect(compact).toContain("connected");
191
+ expect(compact.length).toBeLessThan(200); // Should be brief
192
+ });
193
+
194
+ test("handles disconnected status", () => {
195
+ mockConnection.status = "disconnected";
196
+
197
+ const toolGroups = {
198
+ "Core Memory": ["memory_store"],
199
+ };
200
+
201
+ const report = reporter.buildReport(
202
+ mockConnection,
203
+ mockConfig,
204
+ mockStats,
205
+ toolGroups,
206
+ );
207
+
208
+ expect(report.connection.status).toBe("disconnected");
209
+ const formatted = StatusReporter.formatReport(report);
210
+ expect(formatted).toContain("disconnected");
211
+ });
212
+
213
+ test("includes issues in report", () => {
214
+ reporter.recordFailure("memory_batch_store", "500 Error");
215
+
216
+ const toolGroups = {
217
+ "Core Memory": ["memory_batch_store"],
218
+ };
219
+
220
+ const report = reporter.buildReport(
221
+ mockConnection,
222
+ mockConfig,
223
+ mockStats,
224
+ toolGroups,
225
+ );
226
+
227
+ expect(report.issues).toHaveLength(1);
228
+ expect(report.issues[0].tool).toBe("memory_batch_store");
229
+ });
230
+ });