@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.
- package/dist/src/docs/python.mdx +6 -0
- package/dist/src/docs/typescript.mdx +125 -28
- package/dist/src/lib/__tests__/sdk-messages.test.d.ts +4 -0
- package/dist/src/lib/__tests__/sdk-messages.test.js +296 -0
- package/dist/src/lib/__tests__/sdk-messages.test.js.map +1 -0
- package/dist/src/lib/agent-interface.js +40 -14
- package/dist/src/lib/agent-interface.js.map +1 -1
- package/dist/src/lib/agent-prompts.js +4 -1
- package/dist/src/lib/agent-prompts.js.map +1 -1
- package/dist/src/lib/constants.d.ts +7 -0
- package/dist/src/lib/constants.js +15 -0
- package/dist/src/lib/constants.js.map +1 -1
- package/dist/src/lib/handlers.js +21 -18
- package/dist/src/lib/handlers.js.map +1 -1
- package/dist/src/lib/sdk-messages.d.ts +41 -2
- package/dist/src/lib/sdk-messages.js +89 -41
- package/dist/src/lib/sdk-messages.js.map +1 -1
- package/dist/src/lib/wizard.js +1 -1
- package/dist/src/lib/wizard.js.map +1 -1
- package/dist/src/ui/components/ToolCallDisplay.js +30 -0
- package/dist/src/ui/components/ToolCallDisplay.js.map +1 -1
- package/dist/src/ui/contexts/WizardContext.d.ts +0 -11
- package/dist/src/ui/contexts/WizardContext.js +2 -19
- package/dist/src/ui/contexts/WizardContext.js.map +1 -1
- package/dist/src/utils/__mocks__/ui.d.ts +10 -0
- package/dist/src/utils/__mocks__/ui.js +7 -0
- package/dist/src/utils/__mocks__/ui.js.map +1 -0
- package/dist/src/utils/session.d.ts +22 -0
- package/dist/src/utils/session.js +67 -1
- package/dist/src/utils/session.js.map +1 -1
- package/package.json +16 -1
package/dist/src/docs/python.mdx
CHANGED
|
@@ -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()`** –
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
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, //
|
|
70
|
-
userId: userId, //
|
|
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
|
-
|
|
95
|
-
`
|
|
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,
|
|
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
|
|
132
|
-
|
|
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, //
|
|
137
|
-
userId: userId, //
|
|
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, //
|
|
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, //
|
|
382
|
-
userId: userId, //
|
|
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, //
|
|
424
|
-
userId: userId, //
|
|
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, //
|
|
563
|
-
userId: userId, //
|
|
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,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"]}
|