@raindrop-ai/wizard 0.0.11 → 0.0.12

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.
@@ -39,6 +39,12 @@ raindrop.init(os.getenv("RAINDROP_WRITE_KEY"),
39
39
  # raindrop.init(os.getenv("RAINDROP_WRITE_KEY"),
40
40
  # wizard_session="__WIZARD_SESSION_UUID__", # REQUIRED
41
41
  # tracing_enabled=True)
42
+
43
+ # Optional: bypass OTEL exporters for interaction.track_tool() spans only
44
+ # raindrop.init(os.getenv("RAINDROP_WRITE_KEY"),
45
+ # wizard_session="__WIZARD_SESSION_UUID__", # REQUIRED
46
+ # tracing_enabled=True,
47
+ # bypass_otel_for_tools=True)
42
48
  ```
43
49
 
44
50
  ### **Tracking AI Interactions**
@@ -28,7 +28,7 @@ import { Raindrop } from 'raindrop-ai';
28
28
 
29
29
  // RAINDROP_WRITE_KEY should have already been loaded from environment variables
30
30
  const raindrop = new Raindrop({
31
- writeKey: process.env.RAINDROP_WRITE_KEY,
31
+ writeKey: process.env.RAINDROP_WRITE_KEY!,
32
32
  wizardSession: '__WIZARD_SESSION_UUID__', // REQUIRED
33
33
  });
34
34
  ```
@@ -39,10 +39,14 @@ const raindrop = new Raindrop({
39
39
 
40
40
  The Interaction API uses a simple three-step pattern:
41
41
 
42
- 1. **`begin()`** – Create an interaction and log the initial user input
43
- 2. **Update** – Optionally call `setProperty`, `setProperties`, or
44
- `addAttachments`
45
- 3. **`finish()`** Record the AI's final output and close the interaction
42
+ 1. **`begin()`** – Start interaction; pass AI data (`model`, `input`, `convoId`)
43
+ here.
44
+ 2. **Update** – Optional: `setProperty` / `setProperties` / `addAttachments` for
45
+ **custom metadata only** (not `model`/`input`/`output`/`convoId`).
46
+ 3. **`finish()`** – End interaction; pass `output` and optionally `model`.
47
+
48
+ For event tracking calls (`begin()` and `trackAi()`), both `event` and `userId`
49
+ are **REQUIRED**.
46
50
 
47
51
  ### Example: Chat Completion
48
52
 
@@ -57,7 +61,7 @@ import { randomUUID } from 'crypto';
57
61
  import { Raindrop } from 'raindrop-ai';
58
62
 
59
63
  const raindrop = new Raindrop({
60
- writeKey: process.env.RAINDROP_WRITE_KEY,
64
+ writeKey: process.env.RAINDROP_WRITE_KEY!,
61
65
  wizardSession: '__WIZARD_SESSION_UUID__', // REQUIRED
62
66
  });
63
67
 
@@ -66,8 +70,8 @@ const eventId = randomUUID(); // Only generate if the app doesn't already have a
66
70
  // 1. Start the interaction — use actual values from your app
67
71
  const interaction = raindrop.begin({
68
72
  eventId,
69
- event: eventName, // A descriptive name for this AI action (e.g. 'chat_message', 'code_generation')
70
- userId: userId, // The authenticated user's unique identifier from your app
73
+ event: eventName, // REQUIRED: descriptive name for this AI action (e.g. 'chat_message', 'code_generation')
74
+ userId: userId, // REQUIRED: authenticated user's unique identifier from your app
71
75
  input: userMessage, // The actual user message/prompt
72
76
  model: modelName, // The model being called (e.g. 'gpt-4o')
73
77
  convoId: conversationId, // Your app's conversation/thread ID (if applicable)
@@ -91,8 +95,10 @@ interaction.finish({
91
95
 
92
96
  ### Updating an Interaction
93
97
 
94
- Update an interaction at any point using `setProperty`, `setProperties`, or
95
- `addAttachments`:
98
+ Use `setProperty`, `setProperties`, or `addAttachments` for custom metadata and
99
+ attachments. Do not use them for `model`, `input`, `output`, or `convoId` (use
100
+ `begin()` / `finish()` instead; see
101
+ [AI data vs properties](#ai-data-fields-vs-custom-properties)).
96
102
 
97
103
  ```typescript
98
104
  interaction.setProperty('stage', 'embedding');
@@ -113,6 +119,22 @@ interaction.addAttachments([
113
119
  ]);
114
120
  ```
115
121
 
122
+ ### AI data fields vs custom properties
123
+
124
+ **Rule:** `model`, `input`, `output`, `convoId` are **AI data** — set only via
125
+ `begin()`, `finish()`, or `trackAi()`. `setProperty()` / `setProperties()` only
126
+ write to **custom properties**; they do not update AI data or UI badges.
127
+
128
+ | Field | Set via | Not set via |
129
+ | ------------------------------------- | ------------------------------------------ | --------------- |
130
+ | `model`, `input`, `output`, `convoId` | `begin()`, `finish()`, `trackAi()` | `setProperty()` |
131
+ | Other metadata | `setProperty()`, `properties` in `begin()` | — |
132
+
133
+ - **Wrong:** `interaction.setProperty('model', 'gpt-4o')` → adds a property
134
+ only; Model badge stays empty.
135
+ - **Correct:** pass `model` in `begin({ ... model: 'gpt-4o' })` or
136
+ `finish({ output: '...', model: 'gpt-4o' })`.
137
+
116
138
  ### Resuming an Interaction
117
139
 
118
140
  To resume an interaction without the original object returned from `begin()`,
@@ -126,15 +148,16 @@ const interaction = raindrop.resumeInteraction(eventId);
126
148
 
127
149
  ## Single-Shot Tracking (`trackAi`)
128
150
 
129
- For simple request-response interactions, you can use `trackAi()` directly:
151
+ For simple request-response interactions, use `trackAi()`; pass all AI data
152
+ (`model`, `input`, `output`, `convoId`) in that call (not via `setProperty()`).
130
153
 
131
- Note: `input` and `output` should be the actual prompt sent to and response
132
- received from your AI model - not literal placeholder strings.
154
+ Note: `input` and `output` should be the actual prompt and response from your
155
+ model, not placeholder strings.
133
156
 
134
157
  ```typescript
135
158
  raindrop.trackAi({
136
- event: eventName, // A descriptive name for this AI action (e.g. 'chat_message', 'code_generation')
137
- userId: userId, // The authenticated user's unique identifier from your app
159
+ event: eventName, // REQUIRED: descriptive name for this AI action (e.g. 'chat_message', 'code_generation')
160
+ userId: userId, // REQUIRED: authenticated user's unique identifier from your app
138
161
  model: modelName, // The model being called (e.g. 'gpt-4o-mini')
139
162
  input: userPrompt, // The actual prompt sent to the model
140
163
  output: modelResponse, // The actual response from the model
@@ -216,7 +239,7 @@ interaction.addAttachments([
216
239
  ```typescript
217
240
  // Pass actual user data from your app's auth/session
218
241
  raindrop.setUserDetails({
219
- userId: userId, // The authenticated user's unique identifier
242
+ userId: userId, // REQUIRED: authenticated user's unique identifier
220
243
  traits: {
221
244
  // Pass any user traits available in your app — all keys are optional and freeform
222
245
  name: userName,
@@ -233,7 +256,7 @@ Enable client-side PII redaction when initializing the SDK:
233
256
 
234
257
  ```typescript
235
258
  new Raindrop({
236
- writeKey: process.env.RAINDROP_WRITE_KEY,
259
+ writeKey: process.env.RAINDROP_WRITE_KEY!,
237
260
  wizardSession: '__WIZARD_SESSION_UUID__', // REQUIRED
238
261
  redactPii: true,
239
262
  });
@@ -252,7 +275,7 @@ appropriate error handling in the application.
252
275
 
253
276
  ```typescript
254
277
  new Raindrop({
255
- writeKey: process.env.RAINDROP_WRITE_KEY,
278
+ writeKey: process.env.RAINDROP_WRITE_KEY!,
256
279
  wizardSession: '__WIZARD_SESSION_UUID__', // REQUIRED
257
280
  debugLogs: process.env.NODE_ENV !== 'production', // Print queued events
258
281
  disabled: process.env.NODE_ENV === 'test', // Disable all tracking
@@ -277,7 +300,7 @@ automatically captured:
277
300
  import { Raindrop } from "raindrop-ai";
278
301
 
279
302
  const raindrop = new Raindrop({
280
- writeKey: process.env.RAINDROP_WRITE_KEY,
303
+ writeKey: process.env.RAINDROP_WRITE_KEY!,
281
304
  wizardSession: '__WIZARD_SESSION_UUID__', // REQUIRED
282
305
  });
283
306
 
@@ -378,8 +401,8 @@ Use `trackTool` to log a tool call after it has completed:
378
401
  ```typescript
379
402
  const interaction = raindrop.begin({
380
403
  eventId: eventId,
381
- event: eventName, // A descriptive name for this AI action (e.g. 'chat_message', 'agent_run')
382
- userId: userId, // The authenticated user's unique identifier from your app
404
+ event: eventName, // REQUIRED: descriptive name for this AI action (e.g. 'chat_message', 'agent_run')
405
+ userId: userId, // REQUIRED: authenticated user's unique identifier from your app
383
406
  input: userQuery, // The actual user input
384
407
  });
385
408
 
@@ -420,8 +443,8 @@ Use `startToolSpan` to track a tool as it executes:
420
443
  ```typescript
421
444
  const interaction = raindrop.begin({
422
445
  eventId: eventId,
423
- event: eventName, // A descriptive name for this AI action (e.g. 'chat_message', 'agent_run')
424
- userId: userId, // The authenticated user's unique identifier from your app
446
+ event: eventName, // REQUIRED: descriptive name for this AI action (e.g. 'chat_message', 'agent_run')
447
+ userId: userId, // REQUIRED: authenticated user's unique identifier from your app
425
448
  input: userInput, // The actual user input
426
449
  });
427
450
 
@@ -467,7 +490,7 @@ import * as AnthropicModule from '@anthropic-ai/sdk'; // Required for instrument
467
490
  import { Raindrop } from 'raindrop-ai';
468
491
 
469
492
  const raindrop = new Raindrop({
470
- writeKey: process.env.RAINDROP_WRITE_KEY,
493
+ writeKey: process.env.RAINDROP_WRITE_KEY!,
471
494
  wizardSession: '__WIZARD_SESSION_UUID__', // REQUIRED
472
495
  instrumentModules: {
473
496
  openAI: OpenAI,
@@ -498,7 +521,7 @@ import { Raindrop } from 'raindrop-ai';
498
521
 
499
522
  // 1. Create Raindrop with useExternalOtel
500
523
  const raindrop = new Raindrop({
501
- writeKey: process.env.RAINDROP_WRITE_KEY,
524
+ writeKey: process.env.RAINDROP_WRITE_KEY!,
502
525
  wizardSession: '__WIZARD_SESSION_UUID__',
503
526
  useExternalOtel: true, // ⬅️ REQUIRED
504
527
  });
@@ -525,7 +548,7 @@ import { Raindrop } from 'raindrop-ai';
525
548
 
526
549
  // 1. Create Raindrop with useExternalOtel
527
550
  const raindrop = new Raindrop({
528
- writeKey: process.env.RAINDROP_WRITE_KEY,
551
+ writeKey: process.env.RAINDROP_WRITE_KEY!,
529
552
  wizardSession: '__WIZARD_SESSION_UUID__',
530
553
  useExternalOtel: true, // ⬅️ REQUIRED
531
554
  instrumentModules: { anthropic: AnthropicModule }, // ← Adapt to project's AI SDK
@@ -559,8 +582,8 @@ const anthropic = new Anthropic({ apiKey: '...' });
559
582
  // Note: input and output should be actual model I/O from your app, not placeholder strings
560
583
  const interaction = raindrop.begin({
561
584
  eventId: eventId,
562
- event: eventName, // A descriptive name for this AI action
563
- userId: userId, // The authenticated user's unique identifier from your app
585
+ event: eventName, // REQUIRED: descriptive name for this AI action
586
+ userId: userId, // REQUIRED: authenticated user's unique identifier from your app
564
587
  input: userMessage, // The actual user input
565
588
  });
566
589
  const response = await interaction.withSpan({ name: 'gen' }, async () => {
@@ -582,6 +605,80 @@ interaction.finish({ output: response.content[0].text }); // The actual model re
582
605
 
583
606
  ---
584
607
 
608
+ ### Framework-specific guides
609
+
610
+ #### Mastra Exporter Integration Guide
611
+
612
+ When integrating Raindrop into a Mastra project as a custom
613
+ ObservabilityExporter, the following runtime behaviors must be accounted for:
614
+
615
+ 1. AGENT_RUN spans are not root spans
616
+
617
+ Mastra's HTTP request handler creates a parent span before the agent execution
618
+ span. As a result, AGENT_RUN spans have isRootSpan: false and a non-empty
619
+ parentSpanId. Match on span type alone (span.type === SpanType.AGENT_RUN) and
620
+ deduplicate by traceId using a Map to prevent creating multiple interactions per
621
+ trace.
622
+
623
+ 2. Span IDs are not UUIDs
624
+
625
+ Mastra uses its own internal span ID format which may not conform to UUID v4.
626
+ Always generate a fresh UUID via crypto.randomUUID() for Raindrop's eventId
627
+ parameter. Do not reuse framework-internal span or trace IDs.
628
+
629
+ 3. Agent input/output is a structured messages array
630
+
631
+ **Input:** The input field on an AGENT_RUN span is not a plain string. It is an
632
+ AI SDK messages array in the format:
633
+
634
+ [{ "role": "user", "content": [{ "type": "text", "text": "actual message" }] }]
635
+
636
+ Extract the last user message's text content before passing it to Raindrop's
637
+ input field. Handle both the string shorthand (content: "text") and the
638
+ structured content blocks format (content: [{ type: "text", text: "..." }]).
639
+
640
+ **Output:** The output field on an AGENT_RUN span is not a plain text string. It
641
+ is a JSON-serialized string containing a structured object, typically:
642
+
643
+ '{"text":"actual assistant response","files":[]}'
644
+
645
+ Parse the JSON string and extract the text field before passing it to Raindrop's
646
+ output field. Handle parse failures gracefully by falling back to the raw
647
+ string.
648
+
649
+ 4. Model name is only available on MODEL_GENERATION spans
650
+
651
+ The AGENT_RUN span does not carry model information. The model name and provider
652
+ are only available in span.attributes.model and span.attributes.provider on
653
+ MODEL_GENERATION child spans. To populate Raindrop's top-level model field in
654
+ aiData, defer the begin() call:
655
+
656
+ * On AGENT_RUN SPAN_STARTED: buffer the trace info (input, convoId, eventId) in a pending map.
657
+ * On MODEL_GENERATION SPAN_STARTED: materialize the pending trace by calling begin() with the model parameter from span.attributes.model.
658
+ * On AGENT_RUN SPAN_ENDED: call finish() on the interaction. If no MODEL_GENERATION span was seen, materialize without a model as a fallback.
659
+
660
+ 5. Span event ordering
661
+
662
+ Mastra emits tracing events in this order for a typical agent chat interaction:
663
+
664
+ AGENT_RUN SPAN_STARTED
665
+ → PROCESSOR_RUN spans (started + ended)
666
+ → MODEL_GENERATION SPAN_STARTED
667
+ → MODEL_STEP SPAN_STARTED
668
+ → MODEL_CHUNK spans (streaming)
669
+ → MODEL_STEP SPAN_ENDED (contains usage/token counts)
670
+ → MODEL_GENERATION SPAN_ENDED
671
+ → TOOL_CALL SPAN_STARTED (if tools invoked)
672
+ → TOOL_CALL SPAN_ENDED
673
+ → (repeat MODEL_GENERATION + TOOL_CALL for multi-step agents)
674
+ → PROCESSOR_RUN spans (post-processing)
675
+ AGENT_RUN SPAN_ENDED
676
+
677
+ TOOL_CALL and MCP_TOOL_CALL spans should be tracked via interaction.trackTool()
678
+ on SPAN_ENDED, when input, output, and duration are all available.
679
+
680
+ ---
681
+
585
682
  ## Troubleshooting
586
683
 
587
684
  - Interactions are subject to a 1 MB event limit. Oversized payloads will be
@@ -0,0 +1,4 @@
1
+ /**
2
+ * Tests for SDK message processing utilities
3
+ */
4
+ export {};
@@ -0,0 +1,296 @@
1
+ /**
2
+ * Tests for SDK message processing utilities
3
+ */
4
+ const reportSessionErrorMock = jest.fn();
5
+ jest.mock('../../utils/debug.js', () => ({
6
+ debug: () => { },
7
+ logToFile: () => { },
8
+ }));
9
+ jest.mock('../../utils/session.js', () => ({
10
+ reportSessionError: (...args) => reportSessionErrorMock(...args),
11
+ }));
12
+ jest.mock('../constants.js', () => ({
13
+ ERROR_DISPLAY_MESSAGE: (code) => code
14
+ ? `Sorry, I encountered a ${code} error. Send another message to continue. If the issue persists, please contact support@raindrop.ai`
15
+ : `Sorry, I encountered an error. Send another message to continue. If the issue persists, please contact support@raindrop.ai`,
16
+ INTERNAL_TOOL_NAMES: new Set(),
17
+ MCP_TOOL_PREFIX: 'mcp__',
18
+ }));
19
+ jest.mock('../handlers.js', () => ({}));
20
+ import { extractResultSummary, getCannedErrorDisplayText, processSDKMessage, } from '../sdk-messages.js';
21
+ import ui from '../../utils/ui.js';
22
+ const addItemMock = ui.addItem;
23
+ const sessionErrorContext = {
24
+ wizardSessionId: 'wizard-1',
25
+ accessToken: 'token',
26
+ orgId: 'org-1',
27
+ workingDirectory: '/tmp/project',
28
+ };
29
+ const baseContext = {
30
+ updateSpinner: jest.fn(),
31
+ baseSpinnerMessage: 'Working',
32
+ onError: sessionErrorContext,
33
+ };
34
+ const baseOptions = {
35
+ debug: false,
36
+ forceInstall: false,
37
+ installDir: '/tmp',
38
+ default: false,
39
+ sessionId: 'test-session',
40
+ compiledSetup: '',
41
+ };
42
+ const pendingToolCalls = new Map();
43
+ describe('getCannedErrorDisplayText', () => {
44
+ it('extracts code from "Error: 504" and returns canned message with code', () => {
45
+ const text = getCannedErrorDisplayText('Error: 504');
46
+ expect(text).toContain('504');
47
+ expect(text).toContain('Sorry, I encountered');
48
+ expect(text).toContain('Send another message to continue');
49
+ });
50
+ it('extracts code from "API Error: 429"', () => {
51
+ const text = getCannedErrorDisplayText('API Error: 429');
52
+ expect(text).toContain('429');
53
+ expect(text).toContain('Sorry, I encountered');
54
+ });
55
+ it('returns generic message when no code match', () => {
56
+ const text = getCannedErrorDisplayText('Something went wrong');
57
+ expect(text).not.toMatch(/\d+ error/);
58
+ expect(text).toContain('Sorry, I encountered an error');
59
+ expect(text).toContain('Send another message to continue');
60
+ });
61
+ it('returns generic message for empty string', () => {
62
+ const text = getCannedErrorDisplayText('');
63
+ expect(text).toContain('Sorry, I encountered an error');
64
+ });
65
+ });
66
+ describe('extractResultSummary', () => {
67
+ it('returns undefined for empty content', () => {
68
+ expect(extractResultSummary('Glob', '')).toBeUndefined();
69
+ expect(extractResultSummary('Read', '')).toBeUndefined();
70
+ expect(extractResultSummary('Grep', undefined)).toBeUndefined();
71
+ });
72
+ it('Glob: counts non-empty lines as files', () => {
73
+ expect(extractResultSummary('Glob', '/a\n/b\n/c')).toBe('Found 3 files');
74
+ expect(extractResultSummary('Glob', ' \n/a\n \n')).toBe('Found 1 files');
75
+ expect(extractResultSummary('Glob', '\n\n')).toBe('Found 0 files');
76
+ });
77
+ it('Read: returns line count', () => {
78
+ expect(extractResultSummary('Read', 'line1')).toBe('Read 1 lines');
79
+ expect(extractResultSummary('Read', 'a\nb\nc')).toBe('Read 3 lines');
80
+ });
81
+ it('Grep: counts non-empty match lines', () => {
82
+ expect(extractResultSummary('Grep', 'match1\nmatch2')).toBe('Found 2 matches');
83
+ expect(extractResultSummary('Grep', ' \n')).toBe('Found 0 matches');
84
+ });
85
+ it('Edit: returns summary from input when input provided', () => {
86
+ expect(extractResultSummary('Edit', 'ok', {
87
+ old_string: 'a\nb',
88
+ new_string: 'a\nb\nc',
89
+ })).toBe('Added 1 line');
90
+ expect(extractResultSummary('Edit', 'ok', {
91
+ old_string: 'x',
92
+ new_string: 'y',
93
+ })).toBe('Updated 1 line');
94
+ });
95
+ it('Edit: returns undefined when input not provided', () => {
96
+ expect(extractResultSummary('Edit', 'content')).toBeUndefined();
97
+ });
98
+ it('Write: returns summary from input when input provided', () => {
99
+ expect(extractResultSummary('Write', 'ok', { path: '/foo/bar.ts', content: 'line1\nline2\nline3' })).toBe('Wrote 3 lines to bar.ts');
100
+ expect(extractResultSummary('Write', 'ok', { path: 'file', content: '' })).toBe('Wrote 0 lines to file');
101
+ });
102
+ it('Write: returns undefined when input not provided', () => {
103
+ expect(extractResultSummary('Write', 'content')).toBeUndefined();
104
+ });
105
+ it('handles array result content (e.g. multi-block)', () => {
106
+ expect(extractResultSummary('Read', [{ text: 'a' }, { text: 'b' }])).toBe('Read 2 lines');
107
+ });
108
+ it('returns undefined for unknown tool', () => {
109
+ expect(extractResultSummary('UnknownTool', 'content')).toBeUndefined();
110
+ });
111
+ });
112
+ describe('processSDKMessage', () => {
113
+ beforeEach(() => {
114
+ reportSessionErrorMock.mockClear();
115
+ addItemMock.mockClear();
116
+ });
117
+ describe('assistant return value lastAssistantHadVisibleContent', () => {
118
+ it('returns lastAssistantHadVisibleContent true when message has text block', () => {
119
+ const result = processSDKMessage({
120
+ type: 'assistant',
121
+ session_id: 'conv-1',
122
+ message: {
123
+ content: [{ type: 'text', text: 'Hello' }],
124
+ },
125
+ }, baseOptions, pendingToolCalls, false, baseContext);
126
+ expect(result).toEqual({ lastAssistantHadVisibleContent: true });
127
+ });
128
+ it('returns lastAssistantHadVisibleContent false when message has only thinking block', () => {
129
+ const result = processSDKMessage({
130
+ type: 'assistant',
131
+ session_id: 'conv-2',
132
+ message: {
133
+ content: [
134
+ {
135
+ type: 'thinking',
136
+ thinking: 'Some internal reasoning.',
137
+ },
138
+ ],
139
+ },
140
+ }, baseOptions, pendingToolCalls, false, baseContext);
141
+ expect(result).toEqual({ lastAssistantHadVisibleContent: false });
142
+ });
143
+ it('returns lastAssistantHadVisibleContent false when message has empty or no content', () => {
144
+ const result = processSDKMessage({
145
+ type: 'assistant',
146
+ session_id: 'conv-3',
147
+ message: { content: [] },
148
+ }, baseOptions, pendingToolCalls, false, baseContext);
149
+ expect(result).toEqual({ lastAssistantHadVisibleContent: false });
150
+ });
151
+ });
152
+ describe('assistant with error', () => {
153
+ it('reports to session/error and shows canned message in UI when message.error is "unknown" and error text', () => {
154
+ processSDKMessage({
155
+ type: 'assistant',
156
+ error: 'unknown',
157
+ session_id: 'conv-123',
158
+ message: {
159
+ content: [{ type: 'text', text: 'Error: 504' }],
160
+ },
161
+ }, baseOptions, pendingToolCalls, false, baseContext);
162
+ expect(reportSessionErrorMock).toHaveBeenCalledTimes(1);
163
+ expect(reportSessionErrorMock).toHaveBeenCalledWith('Error: 504', 'conv-123', sessionErrorContext);
164
+ expect(addItemMock).toHaveBeenCalledWith({
165
+ type: 'agent-message',
166
+ text: expect.stringContaining('504'),
167
+ });
168
+ expect(addItemMock.mock.calls[0][0].text).toContain('Send another message to continue');
169
+ });
170
+ it('does not call reportSessionError when context.onError or session_id missing', () => {
171
+ processSDKMessage({
172
+ type: 'assistant',
173
+ error: {},
174
+ session_id: undefined,
175
+ message: { content: [{ type: 'text', text: 'Error: 504' }] },
176
+ }, baseOptions, pendingToolCalls, false, baseContext);
177
+ expect(reportSessionErrorMock).not.toHaveBeenCalled();
178
+ expect(addItemMock).toHaveBeenCalledWith({
179
+ type: 'agent-message',
180
+ text: expect.stringContaining('504'),
181
+ });
182
+ });
183
+ });
184
+ describe('result with errors', () => {
185
+ it('reports combined non-interrupt errors to session/error and shows first canned message in UI when not interrupting', () => {
186
+ processSDKMessage({
187
+ type: 'result',
188
+ subtype: 'error',
189
+ session_id: 'conv-456',
190
+ errors: ['Error: 503', 'Error: 504'],
191
+ }, baseOptions, pendingToolCalls, false, baseContext);
192
+ expect(reportSessionErrorMock).toHaveBeenCalledTimes(1);
193
+ expect(reportSessionErrorMock).toHaveBeenCalledWith('Error: 503\n\nError: 504', 'conv-456', sessionErrorContext);
194
+ expect(addItemMock).toHaveBeenCalledTimes(1);
195
+ expect(addItemMock).toHaveBeenCalledWith({
196
+ type: 'error',
197
+ text: expect.stringContaining('503'),
198
+ });
199
+ });
200
+ it('when isInterrupting: still reports to session/error but does not add error to UI', () => {
201
+ processSDKMessage({
202
+ type: 'result',
203
+ subtype: 'error',
204
+ session_id: 'conv-789',
205
+ errors: ['Error: 500'],
206
+ }, baseOptions, pendingToolCalls, true, baseContext);
207
+ expect(reportSessionErrorMock).toHaveBeenCalledTimes(1);
208
+ expect(reportSessionErrorMock).toHaveBeenCalledWith('Error: 500', 'conv-789', sessionErrorContext);
209
+ const errorCalls = addItemMock.mock.calls.filter((c) => c[0] && c[0].type === 'error');
210
+ expect(errorCalls).toHaveLength(0);
211
+ });
212
+ it('does not report or show interrupt-like errors (aborted, interrupted, 403)', () => {
213
+ processSDKMessage({
214
+ type: 'result',
215
+ subtype: 'error',
216
+ session_id: 'conv-x',
217
+ errors: ['request aborted', 'interrupted by user', 'Error: 403'],
218
+ }, baseOptions, pendingToolCalls, false, baseContext);
219
+ expect(reportSessionErrorMock).not.toHaveBeenCalled();
220
+ const errorCalls = addItemMock.mock.calls.filter((c) => c[0] && c[0].type === 'error');
221
+ expect(errorCalls).toHaveLength(0);
222
+ });
223
+ });
224
+ describe('result success with last assistant having only thinking content', () => {
225
+ it('reports to session/error and queues follow-up prompt when lastAssistantHadVisibleContent is false', () => {
226
+ const queueFollowUpPromptMock = jest.fn();
227
+ processSDKMessage({
228
+ type: 'result',
229
+ subtype: 'success',
230
+ session_id: 'conv-no-text',
231
+ }, baseOptions, pendingToolCalls, false, {
232
+ ...baseContext,
233
+ lastAssistantHadVisibleContent: false,
234
+ queueFollowUpPrompt: queueFollowUpPromptMock,
235
+ });
236
+ expect(reportSessionErrorMock).toHaveBeenCalledTimes(1);
237
+ expect(reportSessionErrorMock).toHaveBeenCalledWith('Last assistant message did not have a text response.', 'conv-no-text', sessionErrorContext);
238
+ expect(queueFollowUpPromptMock).toHaveBeenCalledTimes(1);
239
+ expect(queueFollowUpPromptMock).toHaveBeenCalledWith('[Wizard] Continue.');
240
+ });
241
+ it('does not report or queue when lastAssistantHadVisibleContent is true', () => {
242
+ const queueFollowUpPromptMock = jest.fn();
243
+ processSDKMessage({
244
+ type: 'result',
245
+ subtype: 'success',
246
+ session_id: 'conv-ok',
247
+ }, baseOptions, pendingToolCalls, false, {
248
+ ...baseContext,
249
+ lastAssistantHadVisibleContent: true,
250
+ queueFollowUpPrompt: queueFollowUpPromptMock,
251
+ });
252
+ expect(reportSessionErrorMock).not.toHaveBeenCalled();
253
+ expect(queueFollowUpPromptMock).not.toHaveBeenCalled();
254
+ });
255
+ it('does not report or queue when lastAssistantHadVisibleContent is undefined (treated as had visible)', () => {
256
+ const queueFollowUpPromptMock = jest.fn();
257
+ processSDKMessage({
258
+ type: 'result',
259
+ subtype: 'success',
260
+ session_id: 'conv-ok',
261
+ }, baseOptions, pendingToolCalls, false, { ...baseContext, queueFollowUpPrompt: queueFollowUpPromptMock });
262
+ expect(reportSessionErrorMock).not.toHaveBeenCalled();
263
+ expect(queueFollowUpPromptMock).not.toHaveBeenCalled();
264
+ });
265
+ it('does not report or queue when isInterrupting is true', () => {
266
+ const queueFollowUpPromptMock = jest.fn();
267
+ processSDKMessage({
268
+ type: 'result',
269
+ subtype: 'success',
270
+ session_id: 'conv-no-text',
271
+ }, baseOptions, pendingToolCalls, true, {
272
+ ...baseContext,
273
+ lastAssistantHadVisibleContent: false,
274
+ queueFollowUpPrompt: queueFollowUpPromptMock,
275
+ });
276
+ expect(reportSessionErrorMock).not.toHaveBeenCalled();
277
+ expect(queueFollowUpPromptMock).not.toHaveBeenCalled();
278
+ });
279
+ it('does not report or queue when context.onError or session_id is missing', () => {
280
+ const queueFollowUpPromptMock = jest.fn();
281
+ processSDKMessage({
282
+ type: 'result',
283
+ subtype: 'success',
284
+ session_id: undefined,
285
+ }, baseOptions, pendingToolCalls, false, {
286
+ updateSpinner: jest.fn(),
287
+ baseSpinnerMessage: 'Working',
288
+ lastAssistantHadVisibleContent: false,
289
+ queueFollowUpPrompt: queueFollowUpPromptMock,
290
+ });
291
+ expect(reportSessionErrorMock).not.toHaveBeenCalled();
292
+ expect(queueFollowUpPromptMock).not.toHaveBeenCalled();
293
+ });
294
+ });
295
+ });
296
+ //# sourceMappingURL=sdk-messages.test.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"sdk-messages.test.js","sourceRoot":"","sources":["../../../../src/lib/__tests__/sdk-messages.test.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Tests for SDK message processing utilities\n */\n\nconst reportSessionErrorMock = jest.fn();\n\njest.mock('../../utils/debug.js', () => ({\n debug: () => {},\n logToFile: () => {},\n}));\njest.mock('../../utils/session.js', () => ({\n reportSessionError: (...args: unknown[]) => reportSessionErrorMock(...args),\n}));\njest.mock('../constants.js', () => ({\n ERROR_DISPLAY_MESSAGE: (code?: string) =>\n code\n ? `Sorry, I encountered a ${code} error. Send another message to continue. If the issue persists, please contact support@raindrop.ai`\n : `Sorry, I encountered an error. Send another message to continue. If the issue persists, please contact support@raindrop.ai`,\n INTERNAL_TOOL_NAMES: new Set<string>(),\n MCP_TOOL_PREFIX: 'mcp__',\n}));\njest.mock('../handlers.js', () => ({}));\n\nimport type { PendingToolCall } from '../handlers.js';\nimport {\n extractResultSummary,\n getCannedErrorDisplayText,\n processSDKMessage,\n} from '../sdk-messages.js';\nimport ui from '../../utils/ui.js';\nconst addItemMock = ui.addItem as jest.Mock;\n\nconst sessionErrorContext = {\n wizardSessionId: 'wizard-1',\n accessToken: 'token',\n orgId: 'org-1',\n workingDirectory: '/tmp/project',\n};\nconst baseContext = {\n updateSpinner: jest.fn(),\n baseSpinnerMessage: 'Working',\n onError: sessionErrorContext,\n};\nconst baseOptions = {\n debug: false,\n forceInstall: false,\n installDir: '/tmp',\n default: false,\n sessionId: 'test-session',\n compiledSetup: '',\n};\nconst pendingToolCalls = new Map<string, PendingToolCall>();\n\ndescribe('getCannedErrorDisplayText', () => {\n it('extracts code from \"Error: 504\" and returns canned message with code', () => {\n const text = getCannedErrorDisplayText('Error: 504');\n expect(text).toContain('504');\n expect(text).toContain('Sorry, I encountered');\n expect(text).toContain('Send another message to continue');\n });\n\n it('extracts code from \"API Error: 429\"', () => {\n const text = getCannedErrorDisplayText('API Error: 429');\n expect(text).toContain('429');\n expect(text).toContain('Sorry, I encountered');\n });\n\n it('returns generic message when no code match', () => {\n const text = getCannedErrorDisplayText('Something went wrong');\n expect(text).not.toMatch(/\\d+ error/);\n expect(text).toContain('Sorry, I encountered an error');\n expect(text).toContain('Send another message to continue');\n });\n\n it('returns generic message for empty string', () => {\n const text = getCannedErrorDisplayText('');\n expect(text).toContain('Sorry, I encountered an error');\n });\n});\n\ndescribe('extractResultSummary', () => {\n it('returns undefined for empty content', () => {\n expect(extractResultSummary('Glob', '')).toBeUndefined();\n expect(extractResultSummary('Read', '')).toBeUndefined();\n expect(extractResultSummary('Grep', undefined)).toBeUndefined();\n });\n\n it('Glob: counts non-empty lines as files', () => {\n expect(extractResultSummary('Glob', '/a\\n/b\\n/c')).toBe('Found 3 files');\n expect(extractResultSummary('Glob', ' \\n/a\\n \\n')).toBe('Found 1 files');\n expect(extractResultSummary('Glob', '\\n\\n')).toBe('Found 0 files');\n });\n\n it('Read: returns line count', () => {\n expect(extractResultSummary('Read', 'line1')).toBe('Read 1 lines');\n expect(extractResultSummary('Read', 'a\\nb\\nc')).toBe('Read 3 lines');\n });\n\n it('Grep: counts non-empty match lines', () => {\n expect(extractResultSummary('Grep', 'match1\\nmatch2')).toBe(\n 'Found 2 matches',\n );\n expect(extractResultSummary('Grep', ' \\n')).toBe('Found 0 matches');\n });\n\n it('Edit: returns summary from input when input provided', () => {\n expect(\n extractResultSummary(\n 'Edit',\n 'ok',\n {\n old_string: 'a\\nb',\n new_string: 'a\\nb\\nc',\n },\n ),\n ).toBe('Added 1 line');\n expect(\n extractResultSummary(\n 'Edit',\n 'ok',\n {\n old_string: 'x',\n new_string: 'y',\n },\n ),\n ).toBe('Updated 1 line');\n });\n\n it('Edit: returns undefined when input not provided', () => {\n expect(extractResultSummary('Edit', 'content')).toBeUndefined();\n });\n\n it('Write: returns summary from input when input provided', () => {\n expect(\n extractResultSummary(\n 'Write',\n 'ok',\n { path: '/foo/bar.ts', content: 'line1\\nline2\\nline3' },\n ),\n ).toBe('Wrote 3 lines to bar.ts');\n expect(\n extractResultSummary('Write', 'ok', { path: 'file', content: '' }),\n ).toBe('Wrote 0 lines to file');\n });\n\n it('Write: returns undefined when input not provided', () => {\n expect(extractResultSummary('Write', 'content')).toBeUndefined();\n });\n\n it('handles array result content (e.g. multi-block)', () => {\n expect(\n extractResultSummary('Read', [{ text: 'a' }, { text: 'b' }]),\n ).toBe('Read 2 lines');\n });\n\n it('returns undefined for unknown tool', () => {\n expect(extractResultSummary('UnknownTool', 'content')).toBeUndefined();\n });\n});\n\ndescribe('processSDKMessage', () => {\n beforeEach(() => {\n reportSessionErrorMock.mockClear();\n addItemMock.mockClear();\n });\n\n describe('assistant return value lastAssistantHadVisibleContent', () => {\n it('returns lastAssistantHadVisibleContent true when message has text block', () => {\n const result = processSDKMessage(\n {\n type: 'assistant',\n session_id: 'conv-1',\n message: {\n content: [{ type: 'text', text: 'Hello' }],\n },\n },\n baseOptions,\n pendingToolCalls,\n false,\n baseContext,\n );\n expect(result).toEqual({ lastAssistantHadVisibleContent: true });\n });\n\n it('returns lastAssistantHadVisibleContent false when message has only thinking block', () => {\n const result = processSDKMessage(\n {\n type: 'assistant',\n session_id: 'conv-2',\n message: {\n content: [\n {\n type: 'thinking',\n thinking: 'Some internal reasoning.',\n },\n ],\n },\n },\n baseOptions,\n pendingToolCalls,\n false,\n baseContext,\n );\n expect(result).toEqual({ lastAssistantHadVisibleContent: false });\n });\n\n it('returns lastAssistantHadVisibleContent false when message has empty or no content', () => {\n const result = processSDKMessage(\n {\n type: 'assistant',\n session_id: 'conv-3',\n message: { content: [] },\n },\n baseOptions,\n pendingToolCalls,\n false,\n baseContext,\n );\n expect(result).toEqual({ lastAssistantHadVisibleContent: false });\n });\n });\n\n describe('assistant with error', () => {\n it('reports to session/error and shows canned message in UI when message.error is \"unknown\" and error text', () => {\n processSDKMessage(\n {\n type: 'assistant',\n error: 'unknown',\n session_id: 'conv-123',\n message: {\n content: [{ type: 'text', text: 'Error: 504' }],\n },\n },\n baseOptions,\n pendingToolCalls,\n false,\n baseContext,\n );\n\n expect(reportSessionErrorMock).toHaveBeenCalledTimes(1);\n expect(reportSessionErrorMock).toHaveBeenCalledWith(\n 'Error: 504',\n 'conv-123',\n sessionErrorContext,\n );\n expect(addItemMock).toHaveBeenCalledWith({\n type: 'agent-message',\n text: expect.stringContaining('504'),\n });\n expect(addItemMock.mock.calls[0][0].text).toContain(\n 'Send another message to continue',\n );\n });\n\n it('does not call reportSessionError when context.onError or session_id missing', () => {\n processSDKMessage(\n {\n type: 'assistant',\n error: {},\n session_id: undefined,\n message: { content: [{ type: 'text', text: 'Error: 504' }] },\n },\n baseOptions,\n pendingToolCalls,\n false,\n baseContext,\n );\n expect(reportSessionErrorMock).not.toHaveBeenCalled();\n expect(addItemMock).toHaveBeenCalledWith({\n type: 'agent-message',\n text: expect.stringContaining('504'),\n });\n });\n });\n\n describe('result with errors', () => {\n it('reports combined non-interrupt errors to session/error and shows first canned message in UI when not interrupting', () => {\n processSDKMessage(\n {\n type: 'result',\n subtype: 'error',\n session_id: 'conv-456',\n errors: ['Error: 503', 'Error: 504'],\n },\n baseOptions,\n pendingToolCalls,\n false,\n baseContext,\n );\n\n expect(reportSessionErrorMock).toHaveBeenCalledTimes(1);\n expect(reportSessionErrorMock).toHaveBeenCalledWith(\n 'Error: 503\\n\\nError: 504',\n 'conv-456',\n sessionErrorContext,\n );\n expect(addItemMock).toHaveBeenCalledTimes(1);\n expect(addItemMock).toHaveBeenCalledWith({\n type: 'error',\n text: expect.stringContaining('503'),\n });\n });\n\n it('when isInterrupting: still reports to session/error but does not add error to UI', () => {\n processSDKMessage(\n {\n type: 'result',\n subtype: 'error',\n session_id: 'conv-789',\n errors: ['Error: 500'],\n },\n baseOptions,\n pendingToolCalls,\n true,\n baseContext,\n );\n\n expect(reportSessionErrorMock).toHaveBeenCalledTimes(1);\n expect(reportSessionErrorMock).toHaveBeenCalledWith(\n 'Error: 500',\n 'conv-789',\n sessionErrorContext,\n );\n const errorCalls = addItemMock.mock.calls.filter(\n (c: unknown[]) => c[0] && (c[0] as { type: string }).type === 'error',\n );\n expect(errorCalls).toHaveLength(0);\n });\n\n it('does not report or show interrupt-like errors (aborted, interrupted, 403)', () => {\n processSDKMessage(\n {\n type: 'result',\n subtype: 'error',\n session_id: 'conv-x',\n errors: ['request aborted', 'interrupted by user', 'Error: 403'],\n },\n baseOptions,\n pendingToolCalls,\n false,\n baseContext,\n );\n\n expect(reportSessionErrorMock).not.toHaveBeenCalled();\n const errorCalls = addItemMock.mock.calls.filter(\n (c: unknown[]) => c[0] && (c[0] as { type: string }).type === 'error',\n );\n expect(errorCalls).toHaveLength(0);\n });\n });\n\n describe('result success with last assistant having only thinking content', () => {\n it('reports to session/error and queues follow-up prompt when lastAssistantHadVisibleContent is false', () => {\n const queueFollowUpPromptMock = jest.fn();\n processSDKMessage(\n {\n type: 'result',\n subtype: 'success',\n session_id: 'conv-no-text',\n },\n baseOptions,\n pendingToolCalls,\n false,\n {\n ...baseContext,\n lastAssistantHadVisibleContent: false,\n queueFollowUpPrompt: queueFollowUpPromptMock,\n },\n );\n\n expect(reportSessionErrorMock).toHaveBeenCalledTimes(1);\n expect(reportSessionErrorMock).toHaveBeenCalledWith(\n 'Last assistant message did not have a text response.',\n 'conv-no-text',\n sessionErrorContext,\n );\n expect(queueFollowUpPromptMock).toHaveBeenCalledTimes(1);\n expect(queueFollowUpPromptMock).toHaveBeenCalledWith(\n '[Wizard] Continue.',\n );\n });\n\n it('does not report or queue when lastAssistantHadVisibleContent is true', () => {\n const queueFollowUpPromptMock = jest.fn();\n processSDKMessage(\n {\n type: 'result',\n subtype: 'success',\n session_id: 'conv-ok',\n },\n baseOptions,\n pendingToolCalls,\n false,\n {\n ...baseContext,\n lastAssistantHadVisibleContent: true,\n queueFollowUpPrompt: queueFollowUpPromptMock,\n },\n );\n\n expect(reportSessionErrorMock).not.toHaveBeenCalled();\n expect(queueFollowUpPromptMock).not.toHaveBeenCalled();\n });\n\n it('does not report or queue when lastAssistantHadVisibleContent is undefined (treated as had visible)', () => {\n const queueFollowUpPromptMock = jest.fn();\n processSDKMessage(\n {\n type: 'result',\n subtype: 'success',\n session_id: 'conv-ok',\n },\n baseOptions,\n pendingToolCalls,\n false,\n { ...baseContext, queueFollowUpPrompt: queueFollowUpPromptMock },\n );\n\n expect(reportSessionErrorMock).not.toHaveBeenCalled();\n expect(queueFollowUpPromptMock).not.toHaveBeenCalled();\n });\n\n it('does not report or queue when isInterrupting is true', () => {\n const queueFollowUpPromptMock = jest.fn();\n processSDKMessage(\n {\n type: 'result',\n subtype: 'success',\n session_id: 'conv-no-text',\n },\n baseOptions,\n pendingToolCalls,\n true,\n {\n ...baseContext,\n lastAssistantHadVisibleContent: false,\n queueFollowUpPrompt: queueFollowUpPromptMock,\n },\n );\n\n expect(reportSessionErrorMock).not.toHaveBeenCalled();\n expect(queueFollowUpPromptMock).not.toHaveBeenCalled();\n });\n\n it('does not report or queue when context.onError or session_id is missing', () => {\n const queueFollowUpPromptMock = jest.fn();\n processSDKMessage(\n {\n type: 'result',\n subtype: 'success',\n session_id: undefined,\n },\n baseOptions,\n pendingToolCalls,\n false,\n {\n updateSpinner: jest.fn(),\n baseSpinnerMessage: 'Working',\n lastAssistantHadVisibleContent: false,\n queueFollowUpPrompt: queueFollowUpPromptMock,\n },\n );\n\n expect(reportSessionErrorMock).not.toHaveBeenCalled();\n expect(queueFollowUpPromptMock).not.toHaveBeenCalled();\n });\n });\n});\n"]}