autotel-cloudflare 2.19.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +41 -0
- package/dist/agents.d.ts +626 -123
- package/dist/agents.d.ts.map +1 -1
- package/dist/agents.js +253 -171
- package/dist/agents.js.map +1 -1
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +40 -2
- package/dist/index.js.map +1 -1
- package/dist/native-tracing-D0EyAtsF.js +33 -0
- package/dist/native-tracing-D0EyAtsF.js.map +1 -0
- package/dist/native-tracing-DSo5mody.d.ts +21 -0
- package/dist/native-tracing-DSo5mody.d.ts.map +1 -0
- package/dist/native.d.ts +2 -0
- package/dist/native.js +3 -0
- package/package.json +7 -3
- package/src/agents/agent.ts +327 -0
- package/src/agents/base.ts +26 -0
- package/src/agents/index.ts +5 -1
- package/src/agents/mcp.ts +38 -0
- package/src/agents/observability.ts +145 -0
- package/src/agents/otel-observability.test.ts +165 -232
- package/src/agents/otel-observability.ts +350 -267
- package/src/agents/types.ts +26 -132
- package/src/agents.ts +17 -6
- package/src/index.ts +8 -0
- package/src/native/native-tracing.test.ts +54 -0
- package/src/native/native-tracing.ts +83 -0
- package/src/native.ts +12 -0
- package/src/wrappers/instrument.ts +83 -5
- package/src/wrappers/native-instrument.test.ts +153 -0
|
@@ -1,336 +1,269 @@
|
|
|
1
1
|
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import type { ReadableSpan, SpanProcessor } from '@opentelemetry/sdk-trace-base';
|
|
2
3
|
import {
|
|
3
4
|
createOtelObservability,
|
|
4
5
|
createOtelObservabilityFromEnv,
|
|
5
6
|
OtelObservability,
|
|
6
7
|
} from './otel-observability';
|
|
7
|
-
import
|
|
8
|
+
import { channels, genericObservability, subscribe } from './observability';
|
|
9
|
+
import type { ObservabilityEvent } from './types';
|
|
8
10
|
|
|
9
|
-
|
|
10
|
-
function createRpcEvent(method: string, streaming = false): AgentObservabilityEvent {
|
|
11
|
-
return {
|
|
12
|
-
type: 'rpc',
|
|
13
|
-
id: `rpc-${Date.now()}`,
|
|
14
|
-
displayMessage: `RPC call: ${method}`,
|
|
15
|
-
payload: { method, streaming },
|
|
16
|
-
timestamp: Date.now(),
|
|
17
|
-
};
|
|
18
|
-
}
|
|
11
|
+
type CapturingProcessor = SpanProcessor & { spans: ReadableSpan[] };
|
|
19
12
|
|
|
20
|
-
function
|
|
13
|
+
function createCapturingProcessor(): CapturingProcessor {
|
|
21
14
|
return {
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
15
|
+
spans: [],
|
|
16
|
+
forceFlush: vi.fn(async () => undefined),
|
|
17
|
+
onEnd(span) {
|
|
18
|
+
this.spans.push(span);
|
|
19
|
+
},
|
|
20
|
+
onStart: vi.fn(),
|
|
21
|
+
shutdown: vi.fn(async () => undefined),
|
|
27
22
|
};
|
|
28
23
|
}
|
|
29
24
|
|
|
30
|
-
function
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
):
|
|
25
|
+
function createObservabilityEvent<T extends ObservabilityEvent['type']>(
|
|
26
|
+
type: T,
|
|
27
|
+
payload: Extract<ObservabilityEvent, { type: T }>['payload'],
|
|
28
|
+
extra: Partial<Extract<ObservabilityEvent, { type: T }>> = {},
|
|
29
|
+
): Extract<ObservabilityEvent, { type: T }> {
|
|
35
30
|
return {
|
|
36
|
-
type
|
|
37
|
-
|
|
38
|
-
displayMessage: `Schedule ${eventType}: ${callback}`,
|
|
39
|
-
payload: { callback, id },
|
|
31
|
+
type,
|
|
32
|
+
payload,
|
|
40
33
|
timestamp: Date.now(),
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function createMcpConnectEvent(
|
|
45
|
-
url: string,
|
|
46
|
-
transport: string,
|
|
47
|
-
state: string,
|
|
48
|
-
): MCPObservabilityEvent {
|
|
49
|
-
return {
|
|
50
|
-
type: 'mcp:client:connect',
|
|
51
|
-
id: `mcp-${Date.now()}`,
|
|
52
|
-
displayMessage: `MCP connect: ${url}`,
|
|
53
|
-
payload: { url, transport, state },
|
|
54
|
-
timestamp: Date.now(),
|
|
55
|
-
};
|
|
34
|
+
...extra,
|
|
35
|
+
} as Extract<ObservabilityEvent, { type: T }>;
|
|
56
36
|
}
|
|
57
37
|
|
|
58
38
|
describe('OtelObservability', () => {
|
|
39
|
+
const processor = createCapturingProcessor();
|
|
40
|
+
|
|
59
41
|
beforeEach(() => {
|
|
60
42
|
vi.clearAllMocks();
|
|
61
|
-
|
|
62
|
-
// that cause EnvironmentTeardownError when vitest tears down the worker environment.
|
|
63
|
-
vi.spyOn(globalThis, 'fetch').mockResolvedValue(new Response(null, { status: 200 }));
|
|
43
|
+
processor.spans.length = 0;
|
|
64
44
|
});
|
|
65
45
|
|
|
66
46
|
afterEach(() => {
|
|
67
47
|
vi.restoreAllMocks();
|
|
68
48
|
});
|
|
69
49
|
|
|
70
|
-
describe('
|
|
71
|
-
it('
|
|
72
|
-
const obs = createOtelObservability({
|
|
73
|
-
service: { name: 'test-agent' },
|
|
74
|
-
exporter: { url: 'http://localhost:4318/v1/traces' },
|
|
75
|
-
});
|
|
76
|
-
|
|
77
|
-
expect(obs).toBeInstanceOf(OtelObservability);
|
|
78
|
-
});
|
|
79
|
-
|
|
80
|
-
it('should accept custom agent options', () => {
|
|
50
|
+
describe('constructors', () => {
|
|
51
|
+
it('creates an OtelObservability instance', () => {
|
|
81
52
|
const obs = createOtelObservability({
|
|
82
53
|
service: { name: 'test-agent' },
|
|
83
|
-
|
|
84
|
-
agents: {
|
|
85
|
-
traceRpc: true,
|
|
86
|
-
traceSchedule: false,
|
|
87
|
-
traceMcp: true,
|
|
88
|
-
},
|
|
54
|
+
spanProcessors: [processor],
|
|
89
55
|
});
|
|
90
56
|
|
|
91
57
|
expect(obs).toBeInstanceOf(OtelObservability);
|
|
92
58
|
});
|
|
93
|
-
});
|
|
94
59
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
const env = {
|
|
60
|
+
it('creates an instance from OTEL_* env vars', () => {
|
|
61
|
+
const obs = createOtelObservabilityFromEnv({
|
|
98
62
|
OTEL_SERVICE_NAME: 'my-agent',
|
|
99
|
-
OTEL_EXPORTER_OTLP_ENDPOINT: 'https://
|
|
100
|
-
OTEL_EXPORTER_OTLP_HEADERS: 'x-
|
|
101
|
-
};
|
|
102
|
-
|
|
103
|
-
const obs = createOtelObservabilityFromEnv(env);
|
|
104
|
-
|
|
105
|
-
expect(obs).toBeInstanceOf(OtelObservability);
|
|
106
|
-
});
|
|
107
|
-
|
|
108
|
-
it('should use defaults when env vars not set', () => {
|
|
109
|
-
const obs = createOtelObservabilityFromEnv({});
|
|
110
|
-
|
|
111
|
-
expect(obs).toBeInstanceOf(OtelObservability);
|
|
112
|
-
});
|
|
113
|
-
|
|
114
|
-
it('should parse multiple headers', () => {
|
|
115
|
-
const env = {
|
|
116
|
-
OTEL_EXPORTER_OTLP_HEADERS: 'key1=value1,key2=value2,key3=value3',
|
|
117
|
-
};
|
|
118
|
-
|
|
119
|
-
const obs = createOtelObservabilityFromEnv(env);
|
|
63
|
+
OTEL_EXPORTER_OTLP_ENDPOINT: 'https://collector.example/v1/traces',
|
|
64
|
+
OTEL_EXPORTER_OTLP_HEADERS: 'x-api-key=test,key2=value2',
|
|
65
|
+
});
|
|
120
66
|
|
|
121
67
|
expect(obs).toBeInstanceOf(OtelObservability);
|
|
122
68
|
});
|
|
123
69
|
});
|
|
124
70
|
|
|
125
71
|
describe('emit', () => {
|
|
126
|
-
it('
|
|
72
|
+
it('records rpc spans with agent metadata', () => {
|
|
127
73
|
const obs = createOtelObservability({
|
|
128
74
|
service: { name: 'test-agent' },
|
|
129
|
-
|
|
75
|
+
spanProcessors: [processor],
|
|
130
76
|
});
|
|
131
77
|
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
78
|
+
obs.emit(
|
|
79
|
+
createObservabilityEvent(
|
|
80
|
+
'rpc',
|
|
81
|
+
{ method: 'doSomething', streaming: true },
|
|
82
|
+
{
|
|
83
|
+
id: 'rpc-1',
|
|
84
|
+
agent: 'TaskAgent',
|
|
85
|
+
name: 'agent-instance-1',
|
|
86
|
+
displayMessage: 'RPC doSomething',
|
|
87
|
+
},
|
|
88
|
+
),
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
expect(processor.spans).toHaveLength(1);
|
|
92
|
+
expect(processor.spans[0]?.name).toBe('agent.rpc doSomething');
|
|
93
|
+
expect(processor.spans[0]?.attributes).toMatchObject({
|
|
94
|
+
'agent.event.type': 'rpc',
|
|
95
|
+
'agent.event.id': 'rpc-1',
|
|
96
|
+
'agent.class': 'TaskAgent',
|
|
97
|
+
'agent.instance.name': 'agent-instance-1',
|
|
98
|
+
'gen_ai.agent.name': 'TaskAgent',
|
|
99
|
+
'agent.rpc.method': 'doSomething',
|
|
100
|
+
'agent.rpc.streaming': true,
|
|
101
|
+
});
|
|
136
102
|
});
|
|
137
103
|
|
|
138
|
-
it('
|
|
104
|
+
it('records chat recovery events from the full agent event surface', () => {
|
|
139
105
|
const obs = createOtelObservability({
|
|
140
106
|
service: { name: 'test-agent' },
|
|
141
|
-
|
|
107
|
+
spanProcessors: [processor],
|
|
142
108
|
});
|
|
143
109
|
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
110
|
+
obs.emit(
|
|
111
|
+
createObservabilityEvent('chat:recovery:detected', {
|
|
112
|
+
incidentId: 'inc-1',
|
|
113
|
+
requestId: 'req-1',
|
|
114
|
+
attempt: 2,
|
|
115
|
+
maxAttempts: 5,
|
|
116
|
+
recoveryKind: 'retry',
|
|
117
|
+
}),
|
|
118
|
+
);
|
|
119
|
+
|
|
120
|
+
expect(processor.spans).toHaveLength(1);
|
|
121
|
+
expect(processor.spans[0]?.name).toBe('agent.chat.recovery.detected');
|
|
122
|
+
expect(processor.spans[0]?.attributes).toMatchObject({
|
|
123
|
+
'agent.event.type': 'chat:recovery:detected',
|
|
124
|
+
'agent.payload.incidentId': 'inc-1',
|
|
125
|
+
'agent.payload.requestId': 'req-1',
|
|
126
|
+
'agent.payload.attempt': 2,
|
|
127
|
+
'agent.payload.maxAttempts': 5,
|
|
128
|
+
'agent.payload.recoveryKind': 'retry',
|
|
129
|
+
});
|
|
147
130
|
});
|
|
148
131
|
|
|
149
|
-
it('
|
|
132
|
+
it('marks error spans when the event signals failure', () => {
|
|
150
133
|
const obs = createOtelObservability({
|
|
151
134
|
service: { name: 'test-agent' },
|
|
152
|
-
|
|
135
|
+
spanProcessors: [processor],
|
|
153
136
|
});
|
|
154
137
|
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
138
|
+
obs.emit(
|
|
139
|
+
createObservabilityEvent('rpc:error', {
|
|
140
|
+
method: 'doSomething',
|
|
141
|
+
error: 'boom',
|
|
142
|
+
}),
|
|
143
|
+
);
|
|
144
|
+
|
|
145
|
+
expect(processor.spans).toHaveLength(1);
|
|
146
|
+
expect(processor.spans[0]?.status.code).toBe(2);
|
|
147
|
+
expect(processor.spans[0]?.status.message).toBe('boom');
|
|
148
|
+
expect(processor.spans[0]?.attributes).toMatchObject({
|
|
149
|
+
error: true,
|
|
150
|
+
'exception.message': 'boom',
|
|
151
|
+
});
|
|
158
152
|
});
|
|
159
153
|
|
|
160
|
-
it('
|
|
154
|
+
it('records MCP close events from the reference event model', () => {
|
|
161
155
|
const obs = createOtelObservability({
|
|
162
156
|
service: { name: 'test-agent' },
|
|
163
|
-
|
|
157
|
+
spanProcessors: [processor],
|
|
164
158
|
});
|
|
165
159
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
160
|
+
obs.emit(
|
|
161
|
+
createObservabilityEvent('mcp:client:close', {
|
|
162
|
+
url: 'https://mcp.example',
|
|
163
|
+
transport: 'http',
|
|
164
|
+
state: 'closed',
|
|
165
|
+
phase: 'client-close',
|
|
166
|
+
}),
|
|
167
|
+
);
|
|
168
|
+
|
|
169
|
+
expect(processor.spans).toHaveLength(1);
|
|
170
|
+
expect(processor.spans[0]?.name).toBe('mcp.client.close https://mcp.example');
|
|
171
|
+
expect(processor.spans[0]?.attributes).toMatchObject({
|
|
172
|
+
'agent.mcp.url': 'https://mcp.example',
|
|
173
|
+
'agent.mcp.transport': 'http',
|
|
174
|
+
'agent.mcp.state': 'closed',
|
|
175
|
+
'agent.payload.phase': 'client-close',
|
|
176
|
+
});
|
|
169
177
|
});
|
|
170
178
|
|
|
171
|
-
it('
|
|
179
|
+
it('allows noisy categories to be disabled', () => {
|
|
172
180
|
const obs = createOtelObservability({
|
|
173
181
|
service: { name: 'test-agent' },
|
|
174
|
-
|
|
182
|
+
spanProcessors: [processor],
|
|
175
183
|
agents: {
|
|
176
184
|
traceStateUpdates: false,
|
|
177
185
|
},
|
|
178
186
|
});
|
|
179
187
|
|
|
180
|
-
|
|
181
|
-
type: 'state:update',
|
|
182
|
-
id: 'state-123',
|
|
183
|
-
displayMessage: 'State updated',
|
|
184
|
-
payload: {},
|
|
185
|
-
timestamp: Date.now(),
|
|
186
|
-
};
|
|
188
|
+
obs.emit(createObservabilityEvent('state:update', {}));
|
|
187
189
|
|
|
188
|
-
|
|
189
|
-
expect(() => obs.emit(event)).not.toThrow();
|
|
190
|
+
expect(processor.spans).toHaveLength(0);
|
|
190
191
|
});
|
|
191
192
|
|
|
192
|
-
it('
|
|
193
|
-
const
|
|
193
|
+
it('uses custom formatters and attribute extractors', () => {
|
|
194
|
+
const spanNameFormatter = vi.fn(() => 'custom.span');
|
|
195
|
+
const attributeExtractor = vi.fn(() => ({
|
|
196
|
+
'custom.attr': 'value',
|
|
197
|
+
}));
|
|
194
198
|
|
|
195
199
|
const obs = createOtelObservability({
|
|
196
200
|
service: { name: 'test-agent' },
|
|
197
|
-
|
|
201
|
+
spanProcessors: [processor],
|
|
198
202
|
agents: {
|
|
199
|
-
spanNameFormatter
|
|
203
|
+
spanNameFormatter,
|
|
204
|
+
attributeExtractor,
|
|
200
205
|
},
|
|
201
206
|
});
|
|
202
207
|
|
|
203
|
-
const event =
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
expect(customFormatter).toHaveBeenCalledWith(event);
|
|
207
|
-
});
|
|
208
|
-
|
|
209
|
-
it('should use custom attributeExtractor', () => {
|
|
210
|
-
const customExtractor = vi.fn(() => ({ 'custom.attr': 'value' }));
|
|
211
|
-
|
|
212
|
-
const obs = createOtelObservability({
|
|
213
|
-
service: { name: 'test-agent' },
|
|
214
|
-
exporter: { url: 'http://localhost:4318/v1/traces' },
|
|
215
|
-
agents: {
|
|
216
|
-
attributeExtractor: customExtractor,
|
|
217
|
-
},
|
|
208
|
+
const event = createObservabilityEvent('workflow:approved', {
|
|
209
|
+
workflowId: 'wf-1',
|
|
210
|
+
reason: 'approved',
|
|
218
211
|
});
|
|
219
212
|
|
|
220
|
-
const event = createRpcEvent('testMethod');
|
|
221
213
|
obs.emit(event);
|
|
222
214
|
|
|
223
|
-
expect(
|
|
215
|
+
expect(spanNameFormatter).toHaveBeenCalledWith(event);
|
|
216
|
+
expect(attributeExtractor).toHaveBeenCalledWith(event);
|
|
217
|
+
expect(processor.spans[0]?.name).toBe('custom.span');
|
|
218
|
+
expect(processor.spans[0]?.attributes['custom.attr']).toBe('value');
|
|
224
219
|
});
|
|
225
220
|
|
|
226
|
-
it('
|
|
221
|
+
it('uses waitUntil when an execution context is provided', () => {
|
|
227
222
|
const obs = createOtelObservability({
|
|
228
223
|
service: { name: 'test-agent' },
|
|
229
|
-
|
|
224
|
+
spanProcessors: [processor],
|
|
230
225
|
});
|
|
226
|
+
const waitUntil = vi.fn();
|
|
231
227
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
228
|
+
obs.emit(
|
|
229
|
+
createObservabilityEvent('queue:create', {
|
|
230
|
+
callback: 'process',
|
|
231
|
+
id: 'queue-1',
|
|
232
|
+
}),
|
|
233
|
+
{ waitUntil } as unknown as ExecutionContext,
|
|
234
|
+
);
|
|
237
235
|
|
|
238
|
-
expect(
|
|
236
|
+
expect(waitUntil).toHaveBeenCalledTimes(1);
|
|
239
237
|
});
|
|
240
238
|
});
|
|
241
239
|
|
|
242
|
-
describe('
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
it('should handle message:request events', () => {
|
|
249
|
-
const event: AgentObservabilityEvent = {
|
|
250
|
-
type: 'message:request',
|
|
251
|
-
id: 'msg-req-123',
|
|
252
|
-
displayMessage: 'Message request',
|
|
253
|
-
payload: {},
|
|
254
|
-
timestamp: Date.now(),
|
|
255
|
-
};
|
|
256
|
-
|
|
257
|
-
expect(() => obs.emit(event)).not.toThrow();
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
it('should handle message:response events', () => {
|
|
261
|
-
const event: AgentObservabilityEvent = {
|
|
262
|
-
type: 'message:response',
|
|
263
|
-
id: 'msg-res-123',
|
|
264
|
-
displayMessage: 'Message response',
|
|
265
|
-
payload: {},
|
|
266
|
-
timestamp: Date.now(),
|
|
267
|
-
};
|
|
268
|
-
|
|
269
|
-
expect(() => obs.emit(event)).not.toThrow();
|
|
270
|
-
});
|
|
271
|
-
|
|
272
|
-
it('should handle message:clear events', () => {
|
|
273
|
-
const event: AgentObservabilityEvent = {
|
|
274
|
-
type: 'message:clear',
|
|
275
|
-
id: 'msg-clr-123',
|
|
276
|
-
displayMessage: 'Messages cleared',
|
|
277
|
-
payload: {},
|
|
278
|
-
timestamp: Date.now(),
|
|
279
|
-
};
|
|
240
|
+
describe('generic observability channels', () => {
|
|
241
|
+
it('routes events through typed subscriptions', () => {
|
|
242
|
+
const callback = vi.fn();
|
|
243
|
+
const unsubscribe = subscribe('rpc', callback);
|
|
244
|
+
const event = createObservabilityEvent('rpc', { method: 'lookup' });
|
|
280
245
|
|
|
281
|
-
|
|
282
|
-
});
|
|
283
|
-
|
|
284
|
-
it('should handle destroy events', () => {
|
|
285
|
-
const event: AgentObservabilityEvent = {
|
|
286
|
-
type: 'destroy',
|
|
287
|
-
id: 'destroy-123',
|
|
288
|
-
displayMessage: 'Agent destroyed',
|
|
289
|
-
payload: {},
|
|
290
|
-
timestamp: Date.now(),
|
|
291
|
-
};
|
|
292
|
-
|
|
293
|
-
expect(() => obs.emit(event)).not.toThrow();
|
|
294
|
-
});
|
|
246
|
+
genericObservability.emit(event);
|
|
295
247
|
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
type: 'mcp:client:preconnect',
|
|
299
|
-
id: 'mcp-pre-123',
|
|
300
|
-
displayMessage: 'MCP preconnect',
|
|
301
|
-
payload: { serverId: 'server-1' },
|
|
302
|
-
timestamp: Date.now(),
|
|
303
|
-
};
|
|
248
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
249
|
+
expect(callback).toHaveBeenCalledWith(event);
|
|
304
250
|
|
|
305
|
-
|
|
251
|
+
unsubscribe();
|
|
252
|
+
genericObservability.emit(event);
|
|
253
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
306
254
|
});
|
|
307
255
|
|
|
308
|
-
it('
|
|
309
|
-
const
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
serverId: 'server-1',
|
|
315
|
-
authUrl: 'https://auth.example.com',
|
|
316
|
-
clientId: 'client-123',
|
|
317
|
-
},
|
|
318
|
-
timestamp: Date.now(),
|
|
319
|
-
};
|
|
320
|
-
|
|
321
|
-
expect(() => obs.emit(event)).not.toThrow();
|
|
322
|
-
});
|
|
256
|
+
it('publishes directly on exported channels', () => {
|
|
257
|
+
const callback = vi.fn();
|
|
258
|
+
const unsubscribe = subscribe('mcp', callback);
|
|
259
|
+
const event = createObservabilityEvent('mcp:client:preconnect', {
|
|
260
|
+
serverId: 'server-1',
|
|
261
|
+
});
|
|
323
262
|
|
|
324
|
-
|
|
325
|
-
const event: MCPObservabilityEvent = {
|
|
326
|
-
type: 'mcp:client:discover',
|
|
327
|
-
id: 'mcp-disc-123',
|
|
328
|
-
displayMessage: 'MCP discover',
|
|
329
|
-
payload: {},
|
|
330
|
-
timestamp: Date.now(),
|
|
331
|
-
};
|
|
263
|
+
channels.mcp.publish(event);
|
|
332
264
|
|
|
333
|
-
expect(
|
|
265
|
+
expect(callback).toHaveBeenCalledWith(event);
|
|
266
|
+
unsubscribe();
|
|
334
267
|
});
|
|
335
268
|
});
|
|
336
269
|
});
|