@memberjunction/server 6.2.0-edge.1 → 6.2.0-edge.2

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.
Files changed (93) hide show
  1. package/dist/.tsbuildinfo +1 -1
  2. package/dist/generated/graphql-schemas/__mj.d.ts +1010 -0
  3. package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
  4. package/dist/generated/graphql-schemas/__mj.js +5178 -3
  5. package/dist/generated/graphql-schemas/__mj.js.map +1 -1
  6. package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
  7. package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
  8. package/dist/generic/FireAndForgetHeartbeat.js +16 -2
  9. package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
  10. package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
  11. package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
  12. package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
  13. package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
  14. package/dist/generic/PushStatusResolver.d.ts +38 -0
  15. package/dist/generic/PushStatusResolver.d.ts.map +1 -1
  16. package/dist/generic/PushStatusResolver.js +84 -1
  17. package/dist/generic/PushStatusResolver.js.map +1 -1
  18. package/dist/generic/ResolverBase.d.ts +13 -1
  19. package/dist/generic/ResolverBase.d.ts.map +1 -1
  20. package/dist/generic/ResolverBase.js +49 -47
  21. package/dist/generic/ResolverBase.js.map +1 -1
  22. package/dist/index.d.ts +3 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +90 -2
  25. package/dist/index.js.map +1 -1
  26. package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
  27. package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
  28. package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
  29. package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
  30. package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
  31. package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
  32. package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
  33. package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
  34. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
  35. package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
  36. package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
  37. package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
  38. package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
  39. package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
  40. package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
  41. package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
  42. package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
  43. package/dist/resolvers/MergeRecordsResolver.js +16 -2
  44. package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
  45. package/dist/resolvers/RealtimeClientSessionResolver.d.ts +44 -6
  46. package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
  47. package/dist/resolvers/RealtimeClientSessionResolver.js +90 -34
  48. package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
  49. package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
  50. package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
  51. package/dist/resolvers/RunAIAgentResolver.js +23 -153
  52. package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
  53. package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
  54. package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
  55. package/dist/resolvers/RunDecisionResolver.js +396 -0
  56. package/dist/resolvers/RunDecisionResolver.js.map +1 -0
  57. package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
  58. package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
  59. package/dist/resolvers/VectorizeEntityResolver.js +14 -10
  60. package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
  61. package/package.json +94 -92
  62. package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
  63. package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
  64. package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
  65. package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
  66. package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
  67. package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
  68. package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
  69. package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
  70. package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
  71. package/src/__tests__/RunDecisionResolver.test.ts +699 -0
  72. package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +2 -0
  73. package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
  74. package/src/__tests__/conversationRunEvents.test.ts +399 -0
  75. package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
  76. package/src/__tests__/pushStatusFanOut.test.ts +206 -0
  77. package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
  78. package/src/generated/graphql-schemas/__mj.ts +3637 -4
  79. package/src/generic/FireAndForgetHeartbeat.ts +16 -2
  80. package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
  81. package/src/generic/PushStatusResolver.ts +97 -1
  82. package/src/generic/ResolverBase.ts +49 -49
  83. package/src/index.ts +102 -2
  84. package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
  85. package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
  86. package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
  87. package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
  88. package/src/resolvers/MergeRecordsResolver.ts +14 -2
  89. package/src/resolvers/RealtimeClientSessionResolver.ts +93 -35
  90. package/src/resolvers/RunAIAgentResolver.ts +28 -174
  91. package/src/resolvers/RunDecisionResolver.ts +435 -0
  92. package/src/resolvers/VectorizeEntityResolver.ts +16 -11
  93. package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
@@ -0,0 +1,52 @@
1
+ // Reflect.metadata polyfill at import time.
2
+ import 'reflect-metadata';
3
+ import { describe, it, expect } from 'vitest';
4
+ import { EntityFieldTSType, type EntityFieldInfo, type EntityInfo } from '@memberjunction/core';
5
+ import { ResolverBase } from '../generic/ResolverBase.js';
6
+
7
+ /**
8
+ * Client old values arrive as strings, with dates as epoch milliseconds (the GraphQL Timestamp form).
9
+ * Loading them as-is made an unchanged date field an Invalid Date, so it read as edited: a
10
+ * Comments-only update to an MJ: Record Changes row was refused for "changing" ChangedAt.
11
+ */
12
+ class Probe extends ResolverBase {
13
+ public Convert(field: Partial<EntityFieldInfo>, raw: unknown) {
14
+ return this.ClientOldValueToFieldValue(field as EntityFieldInfo, raw);
15
+ }
16
+ public MustLoad(entity: Partial<EntityInfo>, oldValues: boolean) {
17
+ return this.MustLoadTruthFromDatabase(entity as EntityInfo, { OldValues___: oldValues ? [{ Key: 'Comments', Value: null }] : undefined }, false, false);
18
+ }
19
+ }
20
+ const probe = new Probe();
21
+ const dateField = { Name: 'ChangedAt', TSType: EntityFieldTSType.Date, Type: 'datetimeoffset', AllowsNull: false, DefaultValue: 'getutcdate()' };
22
+
23
+ describe('ResolverBase.ClientOldValueToFieldValue', () => {
24
+ it('reads an epoch-milliseconds string as the same instant', () => {
25
+ const value = probe.Convert(dateField, '1790365634563') as Date;
26
+ expect(value).toBeInstanceOf(Date);
27
+ expect(value.getTime()).toBe(1790365634563);
28
+ });
29
+
30
+ it('reads an ISO date string too, and keeps null as null for a nullable date', () => {
31
+ expect((probe.Convert(dateField, '2026-09-25T19:47:14.563Z') as Date).toISOString()).toBe('2026-09-25T19:47:14.563Z');
32
+ expect(probe.Convert({ ...dateField, AllowsNull: true, DefaultValue: null }, null)).toBeNull();
33
+ });
34
+
35
+ it('types numbers and booleans like the field', () => {
36
+ expect(probe.Convert({ Name: 'N', TSType: EntityFieldTSType.Number, Type: 'int' }, '42')).toBe(42);
37
+ expect(probe.Convert({ Name: 'B', TSType: EntityFieldTSType.Boolean, Type: 'bit' }, 'false')).toBe(false);
38
+ expect(probe.Convert({ Name: 'B', TSType: EntityFieldTSType.Boolean, Type: 'bit', AllowsNull: true }, null)).toBe(false);
39
+ expect(probe.Convert({ Name: 'B', TSType: EntityFieldTSType.Boolean, Type: 'bit' }, '1')).toBe(true);
40
+ expect(probe.Convert({ Name: 'M', TSType: EntityFieldTSType.Number, Type: 'money' }, '1.5')).toBe(1.5);
41
+ });
42
+ });
43
+
44
+ describe('ResolverBase.MustLoadTruthFromDatabase', () => {
45
+ it('always loads a Record Change from the database, so client OldValues cannot pin forged audit columns', () => {
46
+ expect(probe.MustLoad({ Name: 'MJ: Record Changes', TrackRecordChanges: false }, true)).toBe(true);
47
+ });
48
+
49
+ it('still trusts OldValues on other untracked entities without field security', () => {
50
+ expect(probe.MustLoad({ Name: 'MJ: Tags', TrackRecordChanges: false, EnableFieldLevelSecurity: false }, true)).toBe(false);
51
+ });
52
+ });
@@ -183,6 +183,16 @@ describe('loadConversationHistoryWithAttachments — history floor', () => {
183
183
  });
184
184
  });
185
185
 
186
+ function executeArgs(conversationHistoryFrom?: Date): unknown[] {
187
+ const userPayload = { sessionId: 'session-1', userRecord: USER };
188
+ return [
189
+ {}, {}, 'agent-1', userPayload, '[{"role":"user","content":"hi"}]', 'session-1', pubSub,
190
+ undefined, undefined, undefined, undefined, undefined, undefined, 'detail-1', false, false,
191
+ undefined, undefined, 'conv-1', undefined, undefined, undefined, undefined,
192
+ conversationHistoryFrom,
193
+ ];
194
+ }
195
+
186
196
  describe('executeAIAgent — ConversationHistoryFrom', () => {
187
197
  function wire() {
188
198
  const { resolver, seams } = makeResolver();
@@ -199,20 +209,10 @@ describe('executeAIAgent — ConversationHistoryFrom', () => {
199
209
  return execute;
200
210
  }
201
211
 
202
- function positional(conversationHistoryFrom?: Date): unknown[] {
203
- const userPayload = { sessionId: 'session-1', userRecord: USER };
204
- return [
205
- {}, {}, 'agent-1', userPayload, '[{"role":"user","content":"hi"}]', 'session-1', pubSub,
206
- undefined, undefined, undefined, undefined, undefined, undefined, 'detail-1', false, false,
207
- undefined, undefined, 'conv-1', undefined, undefined, undefined, undefined,
208
- conversationHistoryFrom,
209
- ];
210
- }
211
-
212
212
  it('puts the floor on the run\'s params', async () => {
213
213
  const execute = wire();
214
214
  const floor = new Date(FLOOR_ISO);
215
- await execute(...positional(floor));
215
+ await execute(...executeArgs(floor));
216
216
 
217
217
  const params = hoisted.runAgentInConversation.mock.calls[0][0] as Record<string, unknown>;
218
218
  expect(params.ConversationHistoryFrom).toBe(floor);
@@ -220,9 +220,179 @@ describe('executeAIAgent — ConversationHistoryFrom', () => {
220
220
 
221
221
  it('leaves it unset without a floor', async () => {
222
222
  const execute = wire();
223
- await execute(...positional());
223
+ await execute(...executeArgs());
224
224
 
225
225
  const params = hoisted.runAgentInConversation.mock.calls[0][0] as Record<string, unknown>;
226
226
  expect(params.ConversationHistoryFrom).toBeUndefined();
227
227
  });
228
228
  });
229
+
230
+ describe('executeAIAgent — status publishing', () => {
231
+ const agentRun = {
232
+ ID: 'run-1',
233
+ ConversationDetailID: 'detail-1',
234
+ Steps: [{ StepName: 'Answer' }],
235
+ GetAll: () => ({ ID: 'run-1', ConversationDetailID: 'detail-1', Agent: 'Betty' }),
236
+ };
237
+
238
+ function envelopes(publish: ReturnType<typeof vi.fn>): Array<Record<string, unknown>> {
239
+ return publish.mock.calls.map((call) => JSON.parse((call[1] as { message: string }).message) as Record<string, unknown>);
240
+ }
241
+
242
+ it('publishes progress, streamed text, the partial result, and the completion from the runner callbacks', async () => {
243
+ const { resolver, seams } = makeResolver();
244
+ seams.validateAgent = vi.fn().mockResolvedValue({ ID: 'agent-1', Name: 'Test Agent' });
245
+ seams.persistInFlightAgentFailure = vi.fn().mockResolvedValue(undefined);
246
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
247
+ vi.spyOn(console, 'log').mockImplementation(() => undefined);
248
+ const publish = vi.fn().mockResolvedValue(undefined);
249
+ (pubSub as unknown as { publish: Fn }).publish = publish;
250
+ hoisted.runAgentInConversation.mockReset().mockImplementation(async (params: {
251
+ onProgress?: (progress: { step: string; message?: string; percentage?: number; metadata?: Record<string, unknown> }) => void;
252
+ onStreaming?: (chunk: { content: string; isComplete: boolean; stepType?: string; kind?: string }) => void;
253
+ }) => {
254
+ params.onProgress?.({ step: 'initialization', message: 'noise', metadata: { agentRun } });
255
+ params.onProgress?.({
256
+ step: 'prompt_execution',
257
+ message: 'Writing',
258
+ percentage: 40,
259
+ metadata: { agentRun, agentName: 'Betty' },
260
+ });
261
+ params.onStreaming?.({ content: 'Hel', isComplete: false, stepType: 'prompt', kind: 'final-response' });
262
+ return {
263
+ agentResult: { success: true, agentRun, payload: { text: 'Hello' } },
264
+ agentResponseDetailId: 'detail-1',
265
+ conversationId: 'conv-1',
266
+ };
267
+ });
268
+ const execute = (resolver as unknown as {
269
+ executeAIAgent(...args: unknown[]): Promise<{ success: boolean }>;
270
+ }).executeAIAgent.bind(resolver);
271
+
272
+ const result = await execute(...executeArgs());
273
+
274
+ expect(result.success).toBe(true);
275
+ const sent = envelopes(publish);
276
+ expect(sent.map((row) => (row.data as { type: string }).type)).toEqual([
277
+ 'progress',
278
+ 'streaming',
279
+ 'partial_result',
280
+ 'complete',
281
+ ]);
282
+ expect(sent[0].type).toBe('ExecutionProgress');
283
+ expect(sent[0].resolver).toBe('RunAIAgentResolver');
284
+ expect((sent[1].data as { streaming: { content: string } }).streaming.content).toBe('Hel');
285
+ const completion = sent[3].data as { conversationDetailId: string; agentRunId: string; success: boolean };
286
+ expect(completion.conversationDetailId).toBe('detail-1');
287
+ expect(completion.agentRunId).toBe('run-1');
288
+ expect(completion.success).toBe(true);
289
+ });
290
+
291
+ it('publishes a failure completion when a fire-and-forget run rejects', async () => {
292
+ const { resolver, seams } = makeResolver();
293
+ seams.executeAIAgent = vi.fn().mockRejectedValue(new Error('background blew up'));
294
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
295
+ vi.spyOn(console, 'log').mockImplementation(() => undefined);
296
+ const publish = vi.fn().mockResolvedValue(undefined);
297
+ const localPubSub = { publish } as unknown as PubSubEngine;
298
+ const userPayload = { sessionId: 'session-1', userRecord: USER };
299
+ const runInBackground = (resolver as unknown as {
300
+ executeAgentInBackground(...args: unknown[]): void;
301
+ }).executeAgentInBackground.bind(resolver);
302
+
303
+ runInBackground(
304
+ {}, {}, 'agent-1', userPayload, '[]', 'session-1', localPubSub,
305
+ undefined, undefined, undefined, undefined, undefined, 'detail-1',
306
+ );
307
+ await vi.waitFor(() => expect(publish).toHaveBeenCalled());
308
+
309
+ const sent = envelopes(publish);
310
+ expect(sent).toHaveLength(1);
311
+ expect(sent[0].type).toBe('StreamingContent');
312
+ expect(sent[0].resolver).toBe('RunAIAgentResolver');
313
+ const completion = sent[0].data as {
314
+ type: string;
315
+ agentRunId: string;
316
+ conversationDetailId: string;
317
+ success: boolean;
318
+ errorMessage: string;
319
+ result: string;
320
+ };
321
+ expect(completion).toMatchObject({
322
+ type: 'complete',
323
+ agentRunId: 'unknown',
324
+ conversationDetailId: 'detail-1',
325
+ success: false,
326
+ errorMessage: 'background blew up',
327
+ result: JSON.stringify({ success: false, errorMessage: 'background blew up' }),
328
+ });
329
+ });
330
+
331
+ it('saves a Running run reported by progress as Failed when the agent throws', async () => {
332
+ const { resolver, seams } = makeResolver();
333
+ seams.validateAgent = vi.fn().mockResolvedValue({ ID: 'agent-1', Name: 'Test Agent' });
334
+ vi.spyOn(console, 'error').mockImplementation(() => undefined);
335
+ vi.spyOn(console, 'log').mockImplementation(() => undefined);
336
+ const runningRun = {
337
+ ID: 'run-1',
338
+ Status: 'Running',
339
+ ErrorMessage: '',
340
+ CompletedAt: null as Date | null,
341
+ EnsureSaveComplete: vi.fn().mockResolvedValue(undefined),
342
+ Save: vi.fn().mockResolvedValue(true),
343
+ };
344
+ const detail = {
345
+ Status: 'Complete',
346
+ Load: vi.fn().mockResolvedValue(true),
347
+ Save: vi.fn().mockResolvedValue(true),
348
+ };
349
+ const provider = { GetEntityObject: vi.fn().mockResolvedValue(detail) };
350
+ hoisted.runAgentInConversation.mockReset().mockImplementation(async (params: {
351
+ onProgress?: (progress: { step: string; metadata?: Record<string, unknown> }) => void;
352
+ }) => {
353
+ params.onProgress?.({ step: 'prompt_execution', metadata: { agentRun: runningRun } });
354
+ throw new Error('agent threw mid-run');
355
+ });
356
+ const execute = (resolver as unknown as {
357
+ executeAIAgent(...args: unknown[]): Promise<{ success: boolean; errorMessage?: string }>;
358
+ }).executeAIAgent.bind(resolver);
359
+ const args = executeArgs();
360
+ args[0] = provider;
361
+
362
+ const result = await execute(...args);
363
+
364
+ expect(result.success).toBe(false);
365
+ expect(result.errorMessage).toBe('agent threw mid-run');
366
+ expect(runningRun.Status).toBe('Failed');
367
+ expect(runningRun.ErrorMessage).toBe('agent threw mid-run');
368
+ expect(runningRun.CompletedAt).toBeInstanceOf(Date);
369
+ expect(runningRun.EnsureSaveComplete).toHaveBeenCalled();
370
+ expect(runningRun.Save).toHaveBeenCalled();
371
+ expect(detail.Save).not.toHaveBeenCalled();
372
+ });
373
+ });
374
+
375
+ describe('persistInFlightAgentFailure — a refused conversation-detail save logs the reason (MJ#4791)', () => {
376
+ it('includes LatestResult.CompleteMessage when the Error-status save is refused', async () => {
377
+ const { resolver } = makeResolver();
378
+ const errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
379
+ const detail = {
380
+ Status: 'In-Progress',
381
+ LatestResult: { CompleteMessage: 'You do not have access to this conversation.' },
382
+ Load: vi.fn().mockResolvedValue(true),
383
+ EnsureSaveComplete: vi.fn().mockResolvedValue(undefined),
384
+ Save: vi.fn().mockResolvedValue(false),
385
+ };
386
+ const provider = { GetEntityObject: vi.fn().mockResolvedValue(detail) };
387
+ const persist = (resolver as unknown as {
388
+ persistInFlightAgentFailure(...args: unknown[]): Promise<void>;
389
+ }).persistInFlightAgentFailure.bind(resolver);
390
+
391
+ await persist(provider, { sessionId: 'session-1', userRecord: USER }, null, 'detail-1', 'agent threw');
392
+
393
+ const lines = errSpy.mock.calls.map((call) => String(call[0]));
394
+ const line = lines.find((l) => l.includes('Failed to persist Error on conversation detail detail-1'));
395
+ expect(line).toBeDefined();
396
+ expect(line).toContain('You do not have access to this conversation.');
397
+ });
398
+ });
@@ -29,31 +29,27 @@ vi.mock('type-graphql', () => {
29
29
  });
30
30
  import type { AgentExecutionStreamingCallback } from '@memberjunction/ai-core-plus';
31
31
 
32
- import { RunAIAgentResolver } from '../resolvers/RunAIAgentResolver.js';
32
+ import { AgentRunStatusPublisher } from '../resolvers/AgentRunStatusPublisher.js';
33
33
  import type { UserPayload } from '../types.js';
34
34
 
35
- /** Access the private factory without widening the class's public API. */
36
- interface StreamingCallbackFactory {
37
- createStreamingCallback(
38
- pubSub: PubSubEngine,
39
- sessionId: string,
40
- userPayload: UserPayload,
41
- agentRunRef: { current: unknown }
42
- ): AgentExecutionStreamingCallback;
43
- }
35
+ const agentRun = {
36
+ ID: 'run-1',
37
+ GetAll: () => ({ ID: 'run-1', ConversationDetailID: 'detail-1', Agent: 'Betty' }),
38
+ };
44
39
 
45
40
  function buildHarness() {
46
41
  const publish = vi.fn().mockResolvedValue(undefined);
47
42
  const pubSub = { publish } as unknown as PubSubEngine;
48
- const userPayload = { sessionId: 'session-1' } as UserPayload;
49
- const agentRunRef = {
50
- current: {
51
- ID: 'run-1',
52
- GetAll: () => ({ ID: 'run-1', ConversationDetailID: 'detail-1', Agent: 'Betty' }),
53
- },
54
- };
55
- const resolver = new RunAIAgentResolver() as unknown as StreamingCallbackFactory;
56
- const callback = resolver.createStreamingCallback(pubSub, 'session-1', userPayload, agentRunRef);
43
+ const userPayload = { sessionId: 'session-1', userRecord: { ID: 'user-1' } } as UserPayload;
44
+ const publisher = new AgentRunStatusPublisher(pubSub, userPayload, 'session-1');
45
+ // A progress event, even a noise step, is how the publisher learns the run.
46
+ publisher.OnProgress({
47
+ step: 'initialization',
48
+ message: 'starting',
49
+ metadata: { agentRun },
50
+ });
51
+ publish.mockClear();
52
+ const callback: AgentExecutionStreamingCallback = publisher.OnStreaming;
57
53
  return { publish, callback };
58
54
  }
59
55
 
@@ -67,7 +63,7 @@ function publishedData(publish: ReturnType<typeof vi.fn>): Record<string, unknow
67
63
  return parsed.data as Record<string, unknown>;
68
64
  }
69
65
 
70
- describe('RunAIAgentResolver.createStreamingCallback', () => {
66
+ describe('AgentRunStatusPublisher streaming wire shape', () => {
71
67
  it('publishes the streaming wire shape the conversation client parses', () => {
72
68
  const { publish, callback } = buildHarness();
73
69
 
@@ -110,13 +106,8 @@ describe('RunAIAgentResolver.createStreamingCallback', () => {
110
106
  it('does not publish when no agent run is available yet', () => {
111
107
  const publish = vi.fn();
112
108
  const pubSub = { publish } as unknown as PubSubEngine;
113
- const resolver = new RunAIAgentResolver() as unknown as StreamingCallbackFactory;
114
- const callback = resolver.createStreamingCallback(
115
- pubSub,
116
- 'session-1',
117
- { sessionId: 'session-1' } as UserPayload,
118
- { current: null }
119
- );
109
+ const publisher = new AgentRunStatusPublisher(pubSub, { sessionId: 'session-1', userRecord: { ID: 'user-1' } } as UserPayload, 'session-1');
110
+ const callback = publisher.OnStreaming;
120
111
  const consoleSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
121
112
  try {
122
113
  callback({ content: 'early', isComplete: false });