Skip to content

Commit f3e7762

Browse files
nicohrubecclaude
andcommitted
test(node): Add failing integration tests for Mistral AI SDK
Add span-streaming (`traceLifecycle: 'stream'`) node integration tests for a planned `@mistralai/mistralai` gen_ai integration, mirroring the OpenAI suite. Covers chat, embeddings, agents (invoke_agent) and fim (text_completion), across PII-off, PII-on and explicit-integration-option variants. These tests are expected to fail until the `mistralAIIntegration` / `instrumentMistralClient` instrumentation is implemented (TDD step 1). Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 18437c8 commit f3e7762

10 files changed

Lines changed: 728 additions & 3 deletions

File tree

dev-packages/node-integration-tests/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@
4444
"@langchain/core": "^0.3.80",
4545
"@langchain/langgraph": "^0.2.32",
4646
"@langchain/openai": "^0.5.0",
47+
"@mistralai/mistralai": "2.6.4",
4748
"@modelcontextprotocol/client": "^2.0.0",
4849
"@modelcontextprotocol/server": "^2.0.0",
4950
"@nestjs/common": "^11",
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
transport: loggingTransport,
9+
traceLifecycle: 'stream',
10+
integrations: [
11+
Sentry.mistralAIIntegration({
12+
recordInputs: true,
13+
recordOutputs: true,
14+
}),
15+
],
16+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
dataCollection: { genAI: { inputs: true, outputs: true } },
9+
transport: loggingTransport,
10+
traceLifecycle: 'stream',
11+
});
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import * as Sentry from '@sentry/node';
2+
import { loggingTransport } from '@sentry-internal/node-integration-tests';
3+
4+
Sentry.init({
5+
dsn: 'https://public@dsn.ingest.sentry.io/1337',
6+
release: '1.0',
7+
tracesSampleRate: 1.0,
8+
dataCollection: { genAI: { inputs: false, outputs: false } },
9+
transport: loggingTransport,
10+
traceLifecycle: 'stream',
11+
});
Lines changed: 124 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,124 @@
1+
import { Mistral } from '@mistralai/mistralai';
2+
import * as Sentry from '@sentry/node';
3+
import express from 'express';
4+
5+
function startMockServer() {
6+
const app = express();
7+
app.use(express.json());
8+
9+
app.post('/v1/agents/completions', (req, res) => {
10+
const { agent_id: agentId, stream } = req.body;
11+
12+
if (agentId === 'error-agent') {
13+
res.status(404).set('x-request-id', 'mock-request-123').end('Agent not found');
14+
return;
15+
}
16+
17+
if (stream) {
18+
res.setHeader('Content-Type', 'text/event-stream');
19+
res.setHeader('Cache-Control', 'no-cache');
20+
res.setHeader('Connection', 'keep-alive');
21+
22+
const chunks = [
23+
{
24+
id: 'agentcmpl-stream-123',
25+
object: 'chat.completion.chunk',
26+
created: 1677652300,
27+
model: 'mistral-large-latest',
28+
choices: [
29+
{
30+
index: 0,
31+
delta: { role: 'assistant', content: '' },
32+
finish_reason: null,
33+
},
34+
],
35+
},
36+
{
37+
id: 'agentcmpl-stream-123',
38+
object: 'chat.completion.chunk',
39+
created: 1677652300,
40+
model: 'mistral-large-latest',
41+
choices: [
42+
{
43+
index: 0,
44+
delta: { content: 'Hello from Mistral agent streaming!' },
45+
finish_reason: null,
46+
},
47+
],
48+
},
49+
{
50+
id: 'agentcmpl-stream-123',
51+
object: 'chat.completion.chunk',
52+
created: 1677652300,
53+
model: 'mistral-large-latest',
54+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
55+
usage: { prompt_tokens: 12, completion_tokens: 18, total_tokens: 30 },
56+
},
57+
];
58+
59+
chunks.forEach((chunk, index) => {
60+
setTimeout(() => {
61+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
62+
if (index === chunks.length - 1) {
63+
res.write('data: [DONE]\n\n');
64+
res.end();
65+
}
66+
}, index * 10);
67+
});
68+
} else {
69+
res.send({
70+
id: 'agentcmpl-mock123',
71+
object: 'chat.completion',
72+
created: 1677652288,
73+
model: 'mistral-large-latest',
74+
choices: [
75+
{
76+
index: 0,
77+
message: {
78+
role: 'assistant',
79+
content: 'Hello from Mistral agent!',
80+
},
81+
finish_reason: 'stop',
82+
},
83+
],
84+
usage: { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25 },
85+
});
86+
}
87+
});
88+
89+
return new Promise(resolve => {
90+
const server = app.listen(0, () => {
91+
resolve(server);
92+
});
93+
});
94+
}
95+
96+
async function run() {
97+
const server = await startMockServer();
98+
99+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
100+
const client = new Mistral({
101+
apiKey: 'mock-api-key',
102+
serverURL: `http://localhost:${server.address().port}`,
103+
});
104+
105+
await client.agents.complete({
106+
agentId: 'ag-mock-123',
107+
messages: [{ role: 'user', content: 'Who is the best French painter?' }],
108+
});
109+
110+
const stream = await client.agents.stream({
111+
agentId: 'ag-mock-123',
112+
messages: [{ role: 'user', content: 'Tell me about streaming' }],
113+
});
114+
115+
for await (const event of stream) {
116+
void event;
117+
}
118+
});
119+
120+
await Sentry.flush(2000);
121+
server.close();
122+
}
123+
124+
run();
Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import { Mistral } from '@mistralai/mistralai';
2+
import * as Sentry from '@sentry/node';
3+
import express from 'express';
4+
5+
function startMockServer() {
6+
const app = express();
7+
app.use(express.json());
8+
9+
app.post('/v1/chat/completions', (req, res) => {
10+
const { model, stream } = req.body;
11+
12+
// error-model returns 404 (not retried by the SDK) so the span records an error
13+
if (model === 'error-model') {
14+
res.status(404).set('x-request-id', 'mock-request-123').end('Model not found');
15+
return;
16+
}
17+
18+
if (stream) {
19+
res.setHeader('Content-Type', 'text/event-stream');
20+
res.setHeader('Cache-Control', 'no-cache');
21+
res.setHeader('Connection', 'keep-alive');
22+
23+
const chunks = [
24+
{
25+
id: 'chatcmpl-stream-123',
26+
object: 'chat.completion.chunk',
27+
created: 1677652300,
28+
model,
29+
choices: [
30+
{
31+
index: 0,
32+
delta: { role: 'assistant', content: '' },
33+
finish_reason: null,
34+
},
35+
],
36+
},
37+
{
38+
id: 'chatcmpl-stream-123',
39+
object: 'chat.completion.chunk',
40+
created: 1677652300,
41+
model,
42+
choices: [
43+
{
44+
index: 0,
45+
delta: { content: 'Hello from Mistral streaming!' },
46+
finish_reason: null,
47+
},
48+
],
49+
},
50+
{
51+
id: 'chatcmpl-stream-123',
52+
object: 'chat.completion.chunk',
53+
created: 1677652300,
54+
model,
55+
choices: [{ index: 0, delta: {}, finish_reason: 'stop' }],
56+
usage: { prompt_tokens: 12, completion_tokens: 18, total_tokens: 30 },
57+
},
58+
];
59+
60+
chunks.forEach((chunk, index) => {
61+
setTimeout(() => {
62+
res.write(`data: ${JSON.stringify(chunk)}\n\n`);
63+
if (index === chunks.length - 1) {
64+
res.write('data: [DONE]\n\n');
65+
res.end();
66+
}
67+
}, index * 10);
68+
});
69+
} else {
70+
res.send({
71+
id: 'chatcmpl-mock123',
72+
object: 'chat.completion',
73+
created: 1677652288,
74+
model,
75+
choices: [
76+
{
77+
index: 0,
78+
message: { role: 'assistant', content: 'Hello from Mistral mock!' },
79+
finish_reason: 'stop',
80+
},
81+
],
82+
usage: { prompt_tokens: 10, completion_tokens: 15, total_tokens: 25 },
83+
});
84+
}
85+
});
86+
87+
return new Promise(resolve => {
88+
const server = app.listen(0, () => {
89+
resolve(server);
90+
});
91+
});
92+
}
93+
94+
async function run() {
95+
const server = await startMockServer();
96+
97+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
98+
const client = new Mistral({
99+
apiKey: 'mock-api-key',
100+
serverURL: `http://localhost:${server.address().port}`,
101+
});
102+
103+
await client.chat.complete({
104+
model: 'mistral-small-latest',
105+
messages: [
106+
{ role: 'system', content: 'You are a helpful assistant.' },
107+
{ role: 'user', content: 'What is the capital of France?' },
108+
],
109+
temperature: 0.7,
110+
maxTokens: 100,
111+
});
112+
113+
try {
114+
await client.chat.complete({
115+
model: 'error-model',
116+
messages: [{ role: 'user', content: 'This will fail' }],
117+
});
118+
} catch {
119+
// expected
120+
}
121+
122+
const stream = await client.chat.stream({
123+
model: 'mistral-large-latest',
124+
messages: [{ role: 'user', content: 'Tell me about streaming' }],
125+
temperature: 0.8,
126+
});
127+
128+
for await (const event of stream) {
129+
void event;
130+
}
131+
});
132+
133+
await Sentry.flush(2000);
134+
server.close();
135+
}
136+
137+
run();
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { Mistral } from '@mistralai/mistralai';
2+
import * as Sentry from '@sentry/node';
3+
import express from 'express';
4+
5+
function startMockServer() {
6+
const app = express();
7+
app.use(express.json());
8+
9+
app.post('/v1/embeddings', (req, res) => {
10+
const { model } = req.body;
11+
12+
if (model === 'error-model') {
13+
res.status(404).set('x-request-id', 'mock-request-123').end('Model not found');
14+
return;
15+
}
16+
17+
res.send({
18+
id: 'embd-mock123',
19+
object: 'list',
20+
model,
21+
data: [{ object: 'embedding', embedding: [0.1, 0.2, 0.3], index: 0 }],
22+
usage: { prompt_tokens: 8, total_tokens: 8 },
23+
});
24+
});
25+
26+
return new Promise(resolve => {
27+
const server = app.listen(0, () => {
28+
resolve(server);
29+
});
30+
});
31+
}
32+
33+
async function run() {
34+
const server = await startMockServer();
35+
36+
await Sentry.startSpan({ op: 'function', name: 'main' }, async () => {
37+
const client = new Mistral({
38+
apiKey: 'mock-api-key',
39+
serverURL: `http://localhost:${server.address().port}`,
40+
});
41+
42+
await client.embeddings.create({
43+
model: 'mistral-embed',
44+
inputs: 'Embedding test!',
45+
});
46+
47+
try {
48+
await client.embeddings.create({
49+
model: 'error-model',
50+
inputs: 'Error embedding test!',
51+
});
52+
} catch {
53+
// expected
54+
}
55+
56+
await client.embeddings.create({
57+
model: 'mistral-embed',
58+
inputs: ['First input text', 'Second input text'],
59+
});
60+
});
61+
62+
await Sentry.flush(2000);
63+
server.close();
64+
}
65+
66+
run();

0 commit comments

Comments
 (0)