@memberjunction/server 6.2.0-edge.1 → 6.2.0-edge.3
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 +30 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/config.d.ts +2189 -71
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +123 -3
- package/dist/config.js.map +1 -1
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +11 -23
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts +2143 -205
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +24441 -14433
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +15 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +16 -2
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts +31 -0
- package/dist/generic/OrphanedConversationDetailReconciler.d.ts.map +1 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js +215 -0
- package/dist/generic/OrphanedConversationDetailReconciler.js.map +1 -0
- package/dist/generic/PushStatusResolver.d.ts +38 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +84 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +56 -4
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +191 -74
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts +4 -0
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +33 -1
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +204 -50
- package/dist/index.js.map +1 -1
- package/dist/postgresqlCompat.d.ts +8 -0
- package/dist/postgresqlCompat.d.ts.map +1 -0
- package/dist/postgresqlCompat.js +19 -0
- package/dist/postgresqlCompat.js.map +1 -0
- package/dist/postgresqlPoolSettings.d.ts +108 -0
- package/dist/postgresqlPoolSettings.d.ts.map +1 -0
- package/dist/postgresqlPoolSettings.js +134 -0
- package/dist/postgresqlPoolSettings.js.map +1 -0
- package/dist/resolvers/AdhocQueryResolver.d.ts +23 -18
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +77 -167
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/AgentRunStatusPublisher.d.ts +55 -0
- package/dist/resolvers/AgentRunStatusPublisher.d.ts.map +1 -0
- package/dist/resolvers/AgentRunStatusPublisher.js +141 -0
- package/dist/resolvers/AgentRunStatusPublisher.js.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +90 -0
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -0
- package/dist/resolvers/ConversationRunEventsResolver.js +283 -0
- package/dist/resolvers/ConversationRunEventsResolver.js.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts +98 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.d.ts.map +1 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js +227 -0
- package/dist/resolvers/DuplicateEntryCheckResolver.js.map +1 -0
- package/dist/resolvers/EntityRecordNameResolver.d.ts +7 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +9 -47
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +9 -0
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/HumanHandoffResolver.d.ts +107 -0
- package/dist/resolvers/HumanHandoffResolver.d.ts.map +1 -0
- package/dist/resolvers/HumanHandoffResolver.js +324 -0
- package/dist/resolvers/HumanHandoffResolver.js.map +1 -0
- package/dist/resolvers/MeetingResolver.d.ts +69 -0
- package/dist/resolvers/MeetingResolver.d.ts.map +1 -0
- package/dist/resolvers/MeetingResolver.js +381 -0
- package/dist/resolvers/MeetingResolver.js.map +1 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts +7 -0
- package/dist/resolvers/MergeRecordsResolver.d.ts.map +1 -1
- package/dist/resolvers/MergeRecordsResolver.js +16 -2
- package/dist/resolvers/MergeRecordsResolver.js.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts +34 -1
- package/dist/resolvers/RealtimeBridgeResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeBridgeResolver.js +202 -7
- package/dist/resolvers/RealtimeBridgeResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +45 -89
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +94 -240
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts +0 -16
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +23 -153
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunDecisionResolver.d.ts +149 -0
- package/dist/resolvers/RunDecisionResolver.d.ts.map +1 -0
- package/dist/resolvers/RunDecisionResolver.js +396 -0
- package/dist/resolvers/RunDecisionResolver.js.map +1 -0
- package/dist/resolvers/VectorizeEntityResolver.d.ts +1 -1
- package/dist/resolvers/VectorizeEntityResolver.d.ts.map +1 -1
- package/dist/resolvers/VectorizeEntityResolver.js +14 -10
- package/dist/resolvers/VectorizeEntityResolver.js.map +1 -1
- package/dist/resolvers/adhocTimeout.d.ts +7 -0
- package/dist/resolvers/adhocTimeout.d.ts.map +1 -0
- package/dist/resolvers/adhocTimeout.js +12 -0
- package/dist/resolvers/adhocTimeout.js.map +1 -0
- package/dist/resolvers/meetings/index.d.ts +5 -0
- package/dist/resolvers/meetings/index.d.ts.map +1 -0
- package/dist/resolvers/meetings/index.js +5 -0
- package/dist/resolvers/meetings/index.js.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts +17 -0
- package/dist/resolvers/meetings/meetingHelpers.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingHelpers.js +212 -0
- package/dist/resolvers/meetings/meetingHelpers.js.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts +12 -0
- package/dist/resolvers/meetings/meetingMutations.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingMutations.js +418 -0
- package/dist/resolvers/meetings/meetingMutations.js.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts +7 -0
- package/dist/resolvers/meetings/meetingQueries.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingQueries.js +113 -0
- package/dist/resolvers/meetings/meetingQueries.js.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts +89 -0
- package/dist/resolvers/meetings/meetingTypes.d.ts.map +1 -0
- package/dist/resolvers/meetings/meetingTypes.js +345 -0
- package/dist/resolvers/meetings/meetingTypes.js.map +1 -0
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +8 -5
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +8 -0
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -1
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/WorkQueueHostService.d.ts +36 -0
- package/dist/services/WorkQueueHostService.d.ts.map +1 -0
- package/dist/services/WorkQueueHostService.js +97 -0
- package/dist/services/WorkQueueHostService.js.map +1 -0
- package/dist/services/workQueueConfig.d.ts +103 -0
- package/dist/services/workQueueConfig.d.ts.map +1 -0
- package/dist/services/workQueueConfig.js +48 -0
- package/dist/services/workQueueConfig.js.map +1 -0
- package/dist/types.d.ts +5 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js.map +1 -1
- package/package.json +95 -92
- package/src/__tests__/AdhocQueryResolver.bugs.test.ts +14 -35
- package/src/__tests__/AdhocQueryResolver.pagination.test.ts +6 -2
- package/src/__tests__/AdhocQueryResolver.scopeGuard.test.ts +3 -3
- package/src/__tests__/AgentRunStatusPublisher.test.ts +175 -0
- package/src/__tests__/DuplicateEntryCheckResolver.test.ts +300 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/HumanHandoffResolver.test.ts +233 -0
- package/src/__tests__/MeetingResolver.test.ts +374 -0
- package/src/__tests__/RealtimeBridgeResolver.test.ts +240 -2
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +216 -2
- package/src/__tests__/RemoteBrowserActionRecovery.test.ts +7 -1
- package/src/__tests__/RemoteBrowserAudioStream.test.ts +7 -1
- package/src/__tests__/RemoteBrowserSnapshot.test.ts +7 -1
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +96 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +52 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +21 -2
- package/src/__tests__/ResolverBase.includeBinaryFields.test.ts +291 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +182 -12
- package/src/__tests__/RunAIAgentResolver.streaming.test.ts +18 -27
- package/src/__tests__/RunDecisionResolver.test.ts +699 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +43 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/WorkQueueHostService.test.ts +133 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/conversationRunEvents.test.ts +399 -0
- package/src/__tests__/orphanedConversationDetailReconciler.test.ts +283 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusFanOut.test.ts +206 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/auth/__tests__/backward-compatibility.test.ts +1 -0
- package/src/config.ts +130 -3
- package/src/context.ts +11 -24
- package/src/generated/graphql-schemas/__mj.ts +24183 -17182
- package/src/generic/FireAndForgetHeartbeat.ts +16 -2
- package/src/generic/OrphanedConversationDetailReconciler.ts +267 -0
- package/src/generic/PushStatusResolver.ts +97 -1
- package/src/generic/ResolverBase.ts +220 -76
- package/src/generic/RunViewResolver.ts +33 -2
- package/src/index.ts +228 -51
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/resolvers/AdhocQueryResolver.ts +88 -188
- package/src/resolvers/AgentRunStatusPublisher.ts +175 -0
- package/src/resolvers/ConversationRunEventsResolver.ts +308 -0
- package/src/resolvers/DuplicateEntryCheckResolver.ts +232 -0
- package/src/resolvers/EntityRecordNameResolver.ts +9 -51
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -1
- package/src/resolvers/GetDataResolver.ts +10 -0
- package/src/resolvers/HumanHandoffResolver.ts +304 -0
- package/src/resolvers/MeetingResolver.ts +438 -0
- package/src/resolvers/MergeRecordsResolver.ts +14 -2
- package/src/resolvers/RealtimeBridgeResolver.ts +227 -13
- package/src/resolvers/RealtimeClientSessionResolver.ts +99 -287
- package/src/resolvers/RunAIAgentResolver.ts +28 -174
- package/src/resolvers/RunDecisionResolver.ts +435 -0
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/MergeRecordsResolver.test.ts +93 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/resolvers/meetings/index.ts +4 -0
- package/src/resolvers/meetings/meetingHelpers.ts +307 -0
- package/src/resolvers/meetings/meetingMutations.ts +535 -0
- package/src/resolvers/meetings/meetingQueries.ts +172 -0
- package/src/resolvers/meetings/meetingTypes.ts +238 -0
- package/src/rest/ViewOperationsHandler.ts +8 -5
- package/src/services/TaskGraphActionRunner.ts +29 -1
- package/src/services/WorkQueueHostService.ts +129 -0
- package/src/services/workQueueConfig.ts +52 -0
- package/src/types.ts +5 -0
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -33
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -50
- package/dist/resolvers/adhoc-query-helpers.js.map +0 -1
- package/src/__tests__/adhoc-query-helpers.test.ts +0 -50
- package/src/resolvers/adhoc-query-helpers.ts +0 -67
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
// ResolverBase / RunViewResolver transitively pull in type-graphql decorators, which need the
|
|
2
|
+
// Reflect.metadata polyfill at import time.
|
|
3
|
+
import 'reflect-metadata';
|
|
4
|
+
import { describe, it, expect } from 'vitest';
|
|
5
|
+
import type {
|
|
6
|
+
DatabaseProviderBase,
|
|
7
|
+
EntityInfo,
|
|
8
|
+
IMetadataProvider,
|
|
9
|
+
RunViewParams,
|
|
10
|
+
RunViewResult,
|
|
11
|
+
RunViewWithCacheCheckParams,
|
|
12
|
+
RunViewsWithCacheCheckResponse,
|
|
13
|
+
UserInfo,
|
|
14
|
+
} from '@memberjunction/core';
|
|
15
|
+
import type { MJUserViewEntityExtended } from '@memberjunction/core-entities';
|
|
16
|
+
import { ResolverBase } from '../generic/ResolverBase.js';
|
|
17
|
+
import {
|
|
18
|
+
RunViewResolver,
|
|
19
|
+
type RunDynamicViewInput,
|
|
20
|
+
type RunViewByIDInput,
|
|
21
|
+
type RunViewByNameInput,
|
|
22
|
+
type RunViewResultRow,
|
|
23
|
+
type RunViewWithCacheCheckInput,
|
|
24
|
+
} from '../generic/RunViewResolver.js';
|
|
25
|
+
import type { AppContext, UserPayload } from '../types.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Binary DB fields (varbinary / bytea) are base64 strings in JS and on the wire, and RunView
|
|
29
|
+
* omits them unless `IncludeBinaryFields: true`. These tests pin the server half:
|
|
30
|
+
*
|
|
31
|
+
* 1. the GraphQL flag reaches the provider's RunView / RunViews / RunViewsWithCacheCheck params
|
|
32
|
+
* on every resolver path (single by-ID / by-name / dynamic, batch, and the cache-check batch);
|
|
33
|
+
* 2. any byte array that still reaches a resolver is serialized as base64 — never as the
|
|
34
|
+
* `{"type":"Buffer","data":[...]}` that `JSON.stringify` produces for a Node Buffer.
|
|
35
|
+
*/
|
|
36
|
+
|
|
37
|
+
const ENTITY_NAME = 'MJ: Vector Things';
|
|
38
|
+
const ENTITY_ID = 'ent-1';
|
|
39
|
+
|
|
40
|
+
/** Bytes whose base64 form is easy to recognise: [1,2,3,4] → "AQIDBA==". */
|
|
41
|
+
const BYTES = [1, 2, 3, 4];
|
|
42
|
+
const BYTES_BASE64 = 'AQIDBA==';
|
|
43
|
+
|
|
44
|
+
const fakePayload = () =>
|
|
45
|
+
({ email: 'tester@example.com', userRecord: { Email: 'tester@example.com' } as UserInfo } as UserPayload);
|
|
46
|
+
|
|
47
|
+
function entityStub() {
|
|
48
|
+
return {
|
|
49
|
+
ID: ENTITY_ID,
|
|
50
|
+
Name: ENTITY_NAME,
|
|
51
|
+
BaseView: 'vwVectorThings',
|
|
52
|
+
PrimaryKeys: [{ Name: 'ID' }],
|
|
53
|
+
Fields: [
|
|
54
|
+
{ Name: 'ID', CodeName: 'ID' },
|
|
55
|
+
{ Name: 'Embedding', CodeName: 'Embedding' },
|
|
56
|
+
],
|
|
57
|
+
EncryptedFields: [],
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Provider that records every RunView / RunViews call and returns the given rows. */
|
|
62
|
+
function recordingProvider(rows: Record<string, unknown>[] = []) {
|
|
63
|
+
const runViewCalls: RunViewParams[] = [];
|
|
64
|
+
const runViewsCalls: RunViewParams[][] = [];
|
|
65
|
+
const viewInfo = { ID: 'view-1', Name: 'Saved View', Entity: ENTITY_NAME, EntityID: ENTITY_ID };
|
|
66
|
+
const provider = {
|
|
67
|
+
Entities: [entityStub()],
|
|
68
|
+
EntityByName: (name: string) => (name === ENTITY_NAME ? entityStub() : undefined),
|
|
69
|
+
RunView: async (params: RunViewParams): Promise<RunViewResult> => {
|
|
70
|
+
// RunViewByNameGeneric looks the saved view up first — answer that lookup with the view row.
|
|
71
|
+
if (params.EntityName === 'MJ: User Views') {
|
|
72
|
+
return { Success: true, Results: [viewInfo], RowCount: 1, TotalRowCount: 1, ErrorMessage: '' } as RunViewResult;
|
|
73
|
+
}
|
|
74
|
+
runViewCalls.push(params);
|
|
75
|
+
return { Success: true, Results: rows, RowCount: rows.length, TotalRowCount: rows.length, ErrorMessage: '' } as RunViewResult;
|
|
76
|
+
},
|
|
77
|
+
RunViews: async (params: RunViewParams[]): Promise<RunViewResult[]> => {
|
|
78
|
+
runViewsCalls.push(params);
|
|
79
|
+
return params.map(() => ({ Success: true, Results: rows, RowCount: rows.length, TotalRowCount: rows.length, ErrorMessage: '' } as RunViewResult));
|
|
80
|
+
},
|
|
81
|
+
GetEntityObject: async () => ({ ...viewInfo, Load: async () => true }),
|
|
82
|
+
} as unknown as DatabaseProviderBase;
|
|
83
|
+
return { provider, runViewCalls, runViewsCalls };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Skips the encrypted-field filter (it needs live Metadata) — irrelevant to these assertions. */
|
|
87
|
+
class Probe extends ResolverBase {
|
|
88
|
+
protected override async ArrayFilterEncryptedFieldsForAPI(
|
|
89
|
+
_entityName: string,
|
|
90
|
+
dataObjectArray: Record<string, unknown>[]
|
|
91
|
+
): Promise<Record<string, unknown>[]> {
|
|
92
|
+
return dataObjectArray;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public MapOne(entityName: string, row: unknown, provider: IMetadataProvider) {
|
|
96
|
+
return this.MapFieldNamesToCodeNames(entityName, row, undefined, provider);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
describe('IncludeBinaryFields forwarding — single-view resolver paths', () => {
|
|
101
|
+
it('RunDynamicViewGeneric forwards IncludeBinaryFields: true to provider.RunView', async () => {
|
|
102
|
+
const { provider, runViewCalls } = recordingProvider();
|
|
103
|
+
await new Probe().RunDynamicViewGeneric(
|
|
104
|
+
{ EntityName: ENTITY_NAME, ExtraFilter: '', OrderBy: '', IncludeBinaryFields: true } as RunDynamicViewInput,
|
|
105
|
+
provider, fakePayload(), undefined as never,
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
expect(runViewCalls).toHaveLength(1);
|
|
109
|
+
expect(runViewCalls[0].IncludeBinaryFields).toBe(true);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('RunDynamicViewGeneric leaves IncludeBinaryFields undefined when the caller did not set it', async () => {
|
|
113
|
+
const { provider, runViewCalls } = recordingProvider();
|
|
114
|
+
await new Probe().RunDynamicViewGeneric(
|
|
115
|
+
{ EntityName: ENTITY_NAME, ExtraFilter: '', OrderBy: '' } as RunDynamicViewInput,
|
|
116
|
+
provider, fakePayload(), undefined as never,
|
|
117
|
+
);
|
|
118
|
+
|
|
119
|
+
expect(runViewCalls).toHaveLength(1);
|
|
120
|
+
expect(runViewCalls[0].IncludeBinaryFields).toBeUndefined();
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
it('RunViewByNameGeneric forwards IncludeBinaryFields to provider.RunView', async () => {
|
|
124
|
+
const { provider, runViewCalls } = recordingProvider();
|
|
125
|
+
await new Probe().RunViewByNameGeneric(
|
|
126
|
+
{ ViewName: 'Saved View', ExtraFilter: '', OrderBy: '', IncludeBinaryFields: true } as RunViewByNameInput,
|
|
127
|
+
provider, fakePayload(), undefined as never,
|
|
128
|
+
);
|
|
129
|
+
|
|
130
|
+
expect(runViewCalls).toHaveLength(1);
|
|
131
|
+
expect(runViewCalls[0].EntityName).toBe(ENTITY_NAME);
|
|
132
|
+
expect(runViewCalls[0].IncludeBinaryFields).toBe(true);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
it('RunViewByIDGeneric forwards IncludeBinaryFields to provider.RunView', async () => {
|
|
136
|
+
const { provider, runViewCalls } = recordingProvider();
|
|
137
|
+
await new Probe().RunViewByIDGeneric(
|
|
138
|
+
{ ViewID: 'view-1', ExtraFilter: '', OrderBy: '', IncludeBinaryFields: false } as RunViewByIDInput,
|
|
139
|
+
provider, fakePayload(), undefined as never,
|
|
140
|
+
);
|
|
141
|
+
|
|
142
|
+
expect(runViewCalls).toHaveLength(1);
|
|
143
|
+
expect(runViewCalls[0].IncludeBinaryFields).toBe(false);
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
it('RunViewGenericInternal passes binary values through as the provider returned them (base64 strings)', async () => {
|
|
147
|
+
const { provider } = recordingProvider([{ ID: 'r1', Embedding: BYTES_BASE64 }]);
|
|
148
|
+
const result = await new Probe().RunDynamicViewGeneric(
|
|
149
|
+
{ EntityName: ENTITY_NAME, ExtraFilter: '', OrderBy: '', IncludeBinaryFields: true } as RunDynamicViewInput,
|
|
150
|
+
provider, fakePayload(), undefined as never,
|
|
151
|
+
);
|
|
152
|
+
|
|
153
|
+
expect((result?.Results as Record<string, unknown>[])[0].Embedding).toBe(BYTES_BASE64);
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
describe('IncludeBinaryFields forwarding — batch resolver path', () => {
|
|
158
|
+
it('RunViewsGeneric → RunViewsGenericInternal forwards the per-view flag to provider.RunViews', async () => {
|
|
159
|
+
const { provider, runViewsCalls } = recordingProvider();
|
|
160
|
+
const inputs = [
|
|
161
|
+
{ EntityName: ENTITY_NAME, ExtraFilter: '', OrderBy: '', IncludeBinaryFields: true },
|
|
162
|
+
{ EntityName: ENTITY_NAME, ExtraFilter: '', OrderBy: '' },
|
|
163
|
+
] as (RunViewByNameInput & RunViewByIDInput & RunDynamicViewInput)[];
|
|
164
|
+
|
|
165
|
+
await new Probe().RunViewsGeneric(inputs, provider, fakePayload());
|
|
166
|
+
|
|
167
|
+
expect(runViewsCalls).toHaveLength(1);
|
|
168
|
+
expect(runViewsCalls[0]).toHaveLength(2);
|
|
169
|
+
expect(runViewsCalls[0][0].IncludeBinaryFields).toBe(true);
|
|
170
|
+
expect(runViewsCalls[0][1].IncludeBinaryFields).toBeUndefined();
|
|
171
|
+
});
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
describe('MapFieldNamesToCodeNames — byte arrays become base64', () => {
|
|
175
|
+
const provider = {
|
|
176
|
+
EntityByName: (name: string) => (name === ENTITY_NAME ? (entityStub() as unknown as EntityInfo) : undefined),
|
|
177
|
+
} as unknown as IMetadataProvider;
|
|
178
|
+
|
|
179
|
+
it('converts a Buffer value to its base64 string', async () => {
|
|
180
|
+
const mapped = (await new Probe().MapOne(ENTITY_NAME, { ID: 'r1', Embedding: Buffer.from(BYTES) }, provider)) as Record<string, unknown>;
|
|
181
|
+
|
|
182
|
+
expect(mapped.Embedding).toBe(BYTES_BASE64);
|
|
183
|
+
expect(mapped.ID).toBe('r1');
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('converts a plain Uint8Array value too', async () => {
|
|
187
|
+
const mapped = (await new Probe().MapOne(ENTITY_NAME, { ID: 'r1', Embedding: new Uint8Array(BYTES) }, provider)) as Record<string, unknown>;
|
|
188
|
+
|
|
189
|
+
expect(mapped.Embedding).toBe(BYTES_BASE64);
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
it('leaves an already-base64 string untouched', async () => {
|
|
193
|
+
const mapped = (await new Probe().MapOne(ENTITY_NAME, { ID: 'r1', Embedding: BYTES_BASE64 }, provider)) as Record<string, unknown>;
|
|
194
|
+
|
|
195
|
+
expect(mapped.Embedding).toBe(BYTES_BASE64);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it('does not mutate the caller\'s row (it may be a frozen cache entry)', async () => {
|
|
199
|
+
const buffer = Buffer.from(BYTES);
|
|
200
|
+
const source = Object.freeze({ ID: 'r1', Embedding: buffer }) as Record<string, unknown>;
|
|
201
|
+
|
|
202
|
+
const mapped = (await new Probe().MapOne(ENTITY_NAME, source, provider)) as Record<string, unknown>;
|
|
203
|
+
|
|
204
|
+
expect(mapped.Embedding).toBe(BYTES_BASE64);
|
|
205
|
+
expect(source.Embedding).toBe(buffer);
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
|
|
209
|
+
/** Exposes the protected serializer. */
|
|
210
|
+
class ResolverProbe extends RunViewResolver {
|
|
211
|
+
public Process(rows: Record<string, unknown>[]): RunViewResultRow[] {
|
|
212
|
+
return this.processRawData(rows, ENTITY_ID, entityStub() as unknown as EntityInfo);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
describe('RunViewResolver.processRawData — Buffer → base64 in the Data JSON', () => {
|
|
217
|
+
it('serializes a Buffer value as base64, not {type:"Buffer",data:[...]}', () => {
|
|
218
|
+
const [row] = new ResolverProbe().Process([{ ID: 'r1', Embedding: Buffer.from(BYTES) }]);
|
|
219
|
+
const data = JSON.parse(row.Data) as Record<string, unknown>;
|
|
220
|
+
|
|
221
|
+
expect(data.Embedding).toBe(BYTES_BASE64);
|
|
222
|
+
expect(row.Data).not.toContain('"type":"Buffer"');
|
|
223
|
+
expect(row.PrimaryKey).toEqual([{ FieldName: 'ID', Value: 'r1' }]);
|
|
224
|
+
expect(row.EntityID).toBe(ENTITY_ID);
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
it('serializes a Uint8Array value as base64 (not an index-keyed object)', () => {
|
|
228
|
+
const [row] = new ResolverProbe().Process([{ ID: 'r1', Embedding: new Uint8Array(BYTES) }]);
|
|
229
|
+
|
|
230
|
+
expect((JSON.parse(row.Data) as Record<string, unknown>).Embedding).toBe(BYTES_BASE64);
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('leaves rows without byte arrays serialized exactly as before', () => {
|
|
234
|
+
const source = { ID: 'r1', Name: 'Plain', Embedding: BYTES_BASE64, Count: 3 };
|
|
235
|
+
const [row] = new ResolverProbe().Process([source]);
|
|
236
|
+
|
|
237
|
+
expect(row.Data).toBe(JSON.stringify(source));
|
|
238
|
+
});
|
|
239
|
+
|
|
240
|
+
it('does not mutate the source row', () => {
|
|
241
|
+
const buffer = Buffer.from(BYTES);
|
|
242
|
+
const source: Record<string, unknown> = { ID: 'r1', Embedding: buffer };
|
|
243
|
+
|
|
244
|
+
new ResolverProbe().Process([source]);
|
|
245
|
+
|
|
246
|
+
expect(source.Embedding).toBe(buffer);
|
|
247
|
+
});
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
describe('RunViewResolver.RunViewsWithCacheCheck — IncludeBinaryFields + Buffer serialization', () => {
|
|
251
|
+
function cacheCheckContext(rows: Record<string, unknown>[]) {
|
|
252
|
+
const received: RunViewWithCacheCheckParams[][] = [];
|
|
253
|
+
const provider = {
|
|
254
|
+
EntityByName: (name: string) => (name === ENTITY_NAME ? entityStub() : undefined),
|
|
255
|
+
RunViewsWithCacheCheck: async (params: RunViewWithCacheCheckParams[]): Promise<RunViewsWithCacheCheckResponse> => {
|
|
256
|
+
received.push(params);
|
|
257
|
+
return {
|
|
258
|
+
success: true,
|
|
259
|
+
results: params.map((_p, i) => ({ viewIndex: i, status: 'stale', results: rows, maxUpdatedAt: 'T', rowCount: rows.length })),
|
|
260
|
+
} as RunViewsWithCacheCheckResponse;
|
|
261
|
+
},
|
|
262
|
+
};
|
|
263
|
+
const ctx = { providers: [{ type: 'Read-Only', provider }], userPayload: fakePayload() } as unknown as AppContext;
|
|
264
|
+
return { ctx, received };
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
it('forwards IncludeBinaryFields into the core params (true, false and unset)', async () => {
|
|
268
|
+
const { ctx, received } = cacheCheckContext([]);
|
|
269
|
+
const input = [
|
|
270
|
+
{ params: { EntityName: ENTITY_NAME, IncludeBinaryFields: true } },
|
|
271
|
+
{ params: { EntityName: ENTITY_NAME, IncludeBinaryFields: false } },
|
|
272
|
+
{ params: { EntityName: ENTITY_NAME } },
|
|
273
|
+
] as RunViewWithCacheCheckInput[];
|
|
274
|
+
|
|
275
|
+
const out = await new RunViewResolver().RunViewsWithCacheCheck(input, ctx);
|
|
276
|
+
|
|
277
|
+
expect(out.success).toBe(true);
|
|
278
|
+
expect(received).toHaveLength(1);
|
|
279
|
+
expect(received[0].map(p => p.params.IncludeBinaryFields)).toEqual([true, false, undefined]);
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it('serializes Buffers in stale results as base64', async () => {
|
|
283
|
+
const { ctx } = cacheCheckContext([{ ID: 'r1', Embedding: Buffer.from(BYTES) }]);
|
|
284
|
+
const input = [{ params: { EntityName: ENTITY_NAME, IncludeBinaryFields: true } }] as RunViewWithCacheCheckInput[];
|
|
285
|
+
|
|
286
|
+
const out = await new RunViewResolver().RunViewsWithCacheCheck(input, ctx);
|
|
287
|
+
const rows = out.results[0].Results as RunViewResultRow[];
|
|
288
|
+
|
|
289
|
+
expect((JSON.parse(rows[0].Data) as Record<string, unknown>).Embedding).toBe(BYTES_BASE64);
|
|
290
|
+
});
|
|
291
|
+
});
|
|
@@ -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(...
|
|
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(...
|
|
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 {
|
|
32
|
+
import { AgentRunStatusPublisher } from '../resolvers/AgentRunStatusPublisher.js';
|
|
33
33
|
import type { UserPayload } from '../types.js';
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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('
|
|
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
|
|
114
|
-
const callback =
|
|
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 });
|