@memberjunction/server 6.1.4 → 6.1.6
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/agentSessions/SessionManager.d.ts +5 -2
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +17 -8
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/agentSessions/SessionRunIDVerifier.d.ts +63 -0
- package/dist/agentSessions/SessionRunIDVerifier.d.ts.map +1 -0
- package/dist/agentSessions/SessionRunIDVerifier.js +129 -0
- package/dist/agentSessions/SessionRunIDVerifier.js.map +1 -0
- package/dist/agentSessions/index.d.ts +1 -0
- package/dist/agentSessions/index.d.ts.map +1 -1
- package/dist/agentSessions/index.js +1 -0
- package/dist/agentSessions/index.js.map +1 -1
- package/dist/auth/actionAuthorization.d.ts +40 -0
- package/dist/auth/actionAuthorization.d.ts.map +1 -0
- package/dist/auth/actionAuthorization.js +94 -0
- package/dist/auth/actionAuthorization.js.map +1 -0
- package/dist/auth/exampleNewUserSubClass.js +2 -1
- package/dist/auth/exampleNewUserSubClass.js.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.d.ts +17 -15
- package/dist/auth/magicLink/MagicLinkService.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkService.js +55 -38
- package/dist/auth/magicLink/MagicLinkService.js.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.d.ts +14 -33
- package/dist/auth/magicLink/magicLinkCore.d.ts.map +1 -1
- package/dist/auth/magicLink/magicLinkCore.js +20 -52
- package/dist/auth/magicLink/magicLinkCore.js.map +1 -1
- package/dist/auth/newUsers.d.ts.map +1 -1
- package/dist/auth/newUsers.js +15 -11
- package/dist/auth/newUsers.js.map +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +24 -34
- package/dist/context.js.map +1 -1
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +3417 -3428
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +44 -8
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +59 -19
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +95 -11
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +186 -76
- package/dist/generic/ResolverBase.js.map +1 -1
- package/dist/generic/RunViewResolver.d.ts.map +1 -1
- package/dist/generic/RunViewResolver.js +12 -0
- package/dist/generic/RunViewResolver.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +44 -45
- package/dist/index.js.map +1 -1
- package/dist/isolatedProvider.d.ts +20 -0
- package/dist/isolatedProvider.d.ts.map +1 -0
- package/dist/isolatedProvider.js +22 -0
- package/dist/isolatedProvider.js.map +1 -0
- package/dist/multiTenancy/index.d.ts.map +1 -1
- package/dist/multiTenancy/index.js +22 -2
- package/dist/multiTenancy/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/realtimeWidget/widgetGuestElevation.d.ts +4 -2
- package/dist/realtimeWidget/widgetGuestElevation.d.ts.map +1 -1
- package/dist/realtimeWidget/widgetGuestElevation.js +4 -2
- package/dist/realtimeWidget/widgetGuestElevation.js.map +1 -1
- package/dist/resolvers/ActionResolver.d.ts +31 -5
- package/dist/resolvers/ActionResolver.d.ts.map +1 -1
- package/dist/resolvers/ActionResolver.js +75 -14
- package/dist/resolvers/ActionResolver.js.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.d.ts +35 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +92 -166
- package/dist/resolvers/AdhocQueryResolver.js.map +1 -1
- package/dist/resolvers/DatasetResolver.d.ts +24 -3
- package/dist/resolvers/DatasetResolver.d.ts.map +1 -1
- package/dist/resolvers/DatasetResolver.js +57 -8
- package/dist/resolvers/DatasetResolver.js.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts +8 -23
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +11 -48
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +2 -2
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.d.ts +33 -7
- package/dist/resolvers/GetDataContextDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataContextDataResolver.js +117 -38
- package/dist/resolvers/GetDataContextDataResolver.js.map +1 -1
- package/dist/resolvers/GetDataResolver.d.ts.map +1 -1
- package/dist/resolvers/GetDataResolver.js +13 -1
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/QuerySystemUserResolver.js +2 -1
- package/dist/resolvers/QuerySystemUserResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +83 -22
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +174 -72
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts +2 -0
- package/dist/resolvers/RemoteBrowserActionResolver.d.ts.map +1 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js +5 -1
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +2 -1
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/RunTemplateResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTemplateResolver.js +12 -11
- package/dist/resolvers/RunTemplateResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.d.ts +1 -0
- package/dist/resolvers/SearchKnowledgeResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeResolver.js +5 -0
- package/dist/resolvers/SearchKnowledgeResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js +1 -0
- package/dist/resolvers/SearchKnowledgeSystemUserResolver.js.map +1 -1
- package/dist/resolvers/SyncDataResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncDataResolver.js +6 -2
- package/dist/resolvers/SyncDataResolver.js.map +1 -1
- 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/rest/ViewOperationsHandler.d.ts +3 -1
- package/dist/rest/ViewOperationsHandler.d.ts.map +1 -1
- package/dist/rest/ViewOperationsHandler.js +29 -17
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +21 -8
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +43 -16
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/package.json +92 -92
- package/src/__tests__/ActionResolver.authorization.test.ts +353 -0
- 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__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.fieldSecurity.test.ts +27 -72
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +383 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +310 -31
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +129 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +227 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +174 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +24 -2
- package/src/__tests__/ResolverBase.isaPromotionCreate.test.ts +412 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SessionManager.test.ts +94 -17
- package/src/__tests__/SessionRunIDVerifier.test.ts +251 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +131 -0
- package/src/__tests__/TaskGraphActionRunner.storableOutputs.test.ts +76 -0
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/VectorizeEntityResolver.test.ts +106 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/isolatedProviderTransactions.test.ts +169 -0
- package/src/__tests__/magicLink.test.ts +15 -76
- package/src/__tests__/magicLinkService.consume.test.ts +230 -0
- package/src/__tests__/magicLinkService.provisionLog.test.ts +5 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +181 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUsers.test.ts +62 -5
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/agentSessions/SessionManager.ts +23 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +171 -0
- package/src/agentSessions/index.ts +1 -0
- package/src/auth/actionAuthorization.ts +119 -0
- package/src/auth/exampleNewUserSubClass.ts +2 -2
- package/src/auth/magicLink/MagicLinkService.ts +61 -39
- package/src/auth/magicLink/magicLinkCore.ts +19 -56
- package/src/auth/newUsers.ts +15 -11
- package/src/context.ts +25 -34
- package/src/generated/graphql-schemas/__mj.ts +3417 -3428
- package/src/generic/CacheInvalidationResolver.ts +74 -19
- package/src/generic/ResolverBase.ts +207 -80
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/index.ts +49 -46
- package/src/isolatedProvider.ts +22 -0
- package/src/multiTenancy/index.ts +24 -2
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +4 -2
- package/src/resolvers/ActionResolver.ts +84 -21
- package/src/resolvers/AdhocQueryResolver.ts +105 -187
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +11 -52
- package/src/resolvers/FetchEntityVectorsResolver.ts +2 -2
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +14 -1
- package/src/resolvers/QuerySystemUserResolver.ts +2 -1
- package/src/resolvers/RealtimeClientSessionResolver.ts +196 -77
- package/src/resolvers/RemoteBrowserActionResolver.ts +11 -1
- package/src/resolvers/RunAIAgentResolver.ts +4 -1
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/SearchKnowledgeResolver.ts +4 -0
- package/src/resolvers/SearchKnowledgeSystemUserResolver.ts +1 -0
- package/src/resolvers/SyncDataResolver.ts +6 -2
- package/src/resolvers/VectorizeEntityResolver.ts +16 -11
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/rest/ViewOperationsHandler.ts +37 -18
- package/src/services/TaskGraphActionRunner.ts +45 -16
- package/dist/resolvers/adhoc-query-helpers.d.ts +0 -27
- package/dist/resolvers/adhoc-query-helpers.d.ts.map +0 -1
- package/dist/resolvers/adhoc-query-helpers.js +0 -42
- 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 -50
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
// bring it in automatically — this line MUST precede any import that pulls in the resolver file.
|
|
4
4
|
import 'reflect-metadata';
|
|
5
5
|
|
|
6
|
-
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
6
|
+
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
|
|
7
7
|
|
|
8
8
|
// --- Mock CanRun authorization (AIAgentPermissionHelper.HasPermission) plus the
|
|
9
9
|
// provider-scoped AIEngineBase whose cached metadata serves the resolver's pairing-row
|
|
@@ -210,9 +210,23 @@ interface FakeSession {
|
|
|
210
210
|
NewRecord: () => void;
|
|
211
211
|
Save: () => Promise<boolean>;
|
|
212
212
|
Load: (id: string) => Promise<boolean>;
|
|
213
|
-
LatestResult?: { CompleteMessage?: string };
|
|
213
|
+
LatestResult?: { CompleteMessage?: string; Success?: boolean };
|
|
214
|
+
/** Only set on the fakes exercising `describeSaveFailure`'s "no failure detail recorded" branches. */
|
|
215
|
+
ResultHistory?: unknown[];
|
|
214
216
|
}
|
|
215
217
|
|
|
218
|
+
/**
|
|
219
|
+
* A `console.error` spy shared by the save-failure-log tests (#4791) below. Assigned with
|
|
220
|
+
* `vi.spyOn(console, 'error').mockImplementation(() => undefined)` inside each test that needs it
|
|
221
|
+
* — restored here so no test leaks a mocked `console.error` into the ones after it.
|
|
222
|
+
*/
|
|
223
|
+
let errSpy: ReturnType<typeof vi.spyOn> | undefined;
|
|
224
|
+
|
|
225
|
+
afterEach(() => {
|
|
226
|
+
errSpy?.mockRestore();
|
|
227
|
+
errSpy = undefined;
|
|
228
|
+
});
|
|
229
|
+
|
|
216
230
|
function makeSessionEntity(overrides: Partial<FakeSession> = {}): FakeSession {
|
|
217
231
|
return {
|
|
218
232
|
ID: 'session-1',
|
|
@@ -228,13 +242,44 @@ function makeSessionEntity(overrides: Partial<FakeSession> = {}): FakeSession {
|
|
|
228
242
|
};
|
|
229
243
|
}
|
|
230
244
|
|
|
231
|
-
/**
|
|
232
|
-
|
|
233
|
-
|
|
245
|
+
/** Co-agent observability and paused-run ids, as a voice session's Config holds them. */
|
|
246
|
+
const CO_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000001';
|
|
247
|
+
const PROMPT_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000002';
|
|
248
|
+
const PAUSED_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000003';
|
|
249
|
+
|
|
250
|
+
/** `RunView`/`RunViews` stubs for a provider. */
|
|
251
|
+
interface RunLookups {
|
|
252
|
+
RunView: ReturnType<typeof vi.fn>;
|
|
253
|
+
RunViews: ReturnType<typeof vi.fn>;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Answers the session run-id lookups: each id in `runIDs` is a run of the session, and
|
|
258
|
+
* `promptRunID` (when given) is the TargetLogID of the co-agent run's Prompt step.
|
|
259
|
+
*/
|
|
260
|
+
function sessionRunLookups(runIDs: string[], promptRunID?: string): RunLookups {
|
|
261
|
+
const runView = vi.fn(async (params: { EntityName?: string }) => {
|
|
262
|
+
if (params.EntityName === 'MJ: AI Agent Runs') {
|
|
263
|
+
return { Success: true, Results: runIDs.map((ID) => ({ ID })) };
|
|
264
|
+
}
|
|
265
|
+
if (params.EntityName === 'MJ: AI Agent Run Steps' && promptRunID) {
|
|
266
|
+
return { Success: true, Results: [{ ID: 'c0c0c0c0-0000-4000-8000-0000000000a1', StepType: 'Prompt', TargetLogID: promptRunID }] };
|
|
267
|
+
}
|
|
268
|
+
return { Success: true, Results: [] };
|
|
269
|
+
});
|
|
270
|
+
const runViews = vi.fn(async (paramsList: Array<{ EntityName?: string }>) => Promise.all(paramsList.map((params) => runView(params))));
|
|
271
|
+
return { RunView: runView, RunViews: runViews };
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/** Provider whose GetEntityObject returns a fixed entity per entity name, plus optional run-id lookups. */
|
|
275
|
+
function makeProvider(factory: (entityName: string) => FakeSession, lookups?: RunLookups): unknown {
|
|
276
|
+
return { GetEntityObject: vi.fn(async (name: string) => factory(name)), ...lookups };
|
|
234
277
|
}
|
|
235
278
|
|
|
236
279
|
beforeEach(() => {
|
|
237
280
|
hasPermissionMock.mockReset();
|
|
281
|
+
// Callers may run the agents in play unless a test says otherwise; relays re-check CanRun on the target.
|
|
282
|
+
hasPermissionMock.mockResolvedValue(true);
|
|
238
283
|
engineCoAgentsMock.mockReset();
|
|
239
284
|
engineCoAgentsMock.mockReturnValue([]);
|
|
240
285
|
engineChannelsMock.mockReset();
|
|
@@ -671,10 +716,12 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
671
716
|
});
|
|
672
717
|
|
|
673
718
|
it('reads the target + co-agent run id from the session config and returns ResultJson', async () => {
|
|
674
|
-
currentProvider = makeProvider(
|
|
675
|
-
|
|
676
|
-
|
|
677
|
-
|
|
719
|
+
currentProvider = makeProvider(
|
|
720
|
+
() =>
|
|
721
|
+
makeSessionEntity({
|
|
722
|
+
Config_: JSON.stringify({ targetAgentID: 'target-from-session', coAgentRunID: CO_RUN_ID }),
|
|
723
|
+
}),
|
|
724
|
+
sessionRunLookups([CO_RUN_ID]),
|
|
678
725
|
);
|
|
679
726
|
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
|
|
680
727
|
const resolver = makeResolver();
|
|
@@ -697,7 +744,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
697
744
|
// Target comes from the session, NOT the client.
|
|
698
745
|
expect(relayArg.TargetAgentID).toBe('target-from-session');
|
|
699
746
|
// Delegated run nests under the co-agent observability run from the session config.
|
|
700
|
-
expect(relayArg.ParentRunID).toBe(
|
|
747
|
+
expect(relayArg.ParentRunID).toBe(CO_RUN_ID);
|
|
701
748
|
expect(relayArg.Call).toEqual({ CallID: 'call-1', ToolName: 'invoke-target-agent', Arguments: '{"request":"do it"}' });
|
|
702
749
|
expect(heartbeatMock).toHaveBeenCalledWith('session-1', USER, currentProvider);
|
|
703
750
|
});
|
|
@@ -761,9 +808,9 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
761
808
|
|
|
762
809
|
it('consumes pendingFeedbackRunID as ResumeRunID and clears it on the next call', async () => {
|
|
763
810
|
const session = makeSessionEntity({
|
|
764
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID:
|
|
811
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
|
|
765
812
|
});
|
|
766
|
-
currentProvider = makeProvider(() => session);
|
|
813
|
+
currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
|
|
767
814
|
// Resumed run completes (no new pause).
|
|
768
815
|
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"done":1}', Success: true });
|
|
769
816
|
const resolver = makeResolver();
|
|
@@ -772,7 +819,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
772
819
|
|
|
773
820
|
// The paused id was passed in to resume the run...
|
|
774
821
|
const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
|
|
775
|
-
expect(relayArg.ResumeRunID).toBe(
|
|
822
|
+
expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
|
|
776
823
|
// ...and cleared from the session config afterward.
|
|
777
824
|
expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBeUndefined();
|
|
778
825
|
expect(session.Save).toHaveBeenCalled();
|
|
@@ -780,16 +827,16 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
780
827
|
|
|
781
828
|
it('re-stores a new pendingFeedbackRunID when a resumed run pauses again', async () => {
|
|
782
829
|
const session = makeSessionEntity({
|
|
783
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID:
|
|
830
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
|
|
784
831
|
});
|
|
785
|
-
currentProvider = makeProvider(() => session);
|
|
832
|
+
currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
|
|
786
833
|
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"q":2}', Success: true, PausedRunID: 'paused-2' });
|
|
787
834
|
const resolver = makeResolver();
|
|
788
835
|
|
|
789
836
|
await resolver.ExecuteRealtimeSessionTool('session-1', 'call-3', 'invoke-target-agent', '{"request":"yes"}', makeCtx(), makePubSub());
|
|
790
837
|
|
|
791
838
|
const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
|
|
792
|
-
expect(relayArg.ResumeRunID).toBe(
|
|
839
|
+
expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
|
|
793
840
|
expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBe('paused-2');
|
|
794
841
|
});
|
|
795
842
|
|
|
@@ -848,6 +895,28 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
848
895
|
resolver.ExecuteRealtimeSessionTool('session-1', 'call-1', 'x', '{}', makeCtx(), makePubSub()),
|
|
849
896
|
).rejects.toThrow(/no target agent configured/i);
|
|
850
897
|
});
|
|
898
|
+
|
|
899
|
+
it('logs the session id and "no failure detail recorded" when updatePendingFeedbackRunID\'s save is refused silently (#4791)', async () => {
|
|
900
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
901
|
+
const session = makeSessionEntity({
|
|
902
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1' }),
|
|
903
|
+
Save: vi.fn(async () => false),
|
|
904
|
+
LatestResult: undefined,
|
|
905
|
+
ResultHistory: [],
|
|
906
|
+
});
|
|
907
|
+
currentProvider = makeProvider(() => session);
|
|
908
|
+
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"q":1}', Success: true, PausedRunID: 'paused-1' });
|
|
909
|
+
const resolver = makeResolver();
|
|
910
|
+
|
|
911
|
+
await resolver.ExecuteRealtimeSessionTool('session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub());
|
|
912
|
+
|
|
913
|
+
const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
|
|
914
|
+
const failureLine = loggedLines.find((line) => line.includes('updatePendingFeedbackRunID'));
|
|
915
|
+
expect(failureLine).toBeDefined();
|
|
916
|
+
expect(failureLine).toContain('session-1');
|
|
917
|
+
expect(failureLine).toContain('no failure detail recorded');
|
|
918
|
+
expect(failureLine).not.toContain('unknown error');
|
|
919
|
+
});
|
|
851
920
|
});
|
|
852
921
|
|
|
853
922
|
describe('RealtimeClientSessionResolver.RelayRealtimeTranscript', () => {
|
|
@@ -1690,6 +1759,7 @@ describe('RealtimeClientSessionResolver.SaveSessionChannelArtifact', () => {
|
|
|
1690
1759
|
});
|
|
1691
1760
|
|
|
1692
1761
|
it('returns a structured failure when the artifact header save fails (no version attempted)', async () => {
|
|
1762
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
1693
1763
|
const failingArtifact = makeSessionEntity({
|
|
1694
1764
|
ID: 'artifact-fail',
|
|
1695
1765
|
Save: vi.fn(async () => false),
|
|
@@ -1706,10 +1776,14 @@ describe('RealtimeClientSessionResolver.SaveSessionChannelArtifact', () => {
|
|
|
1706
1776
|
|
|
1707
1777
|
expect(result.Success).toBe(false);
|
|
1708
1778
|
expect(result.ErrorMessage).toMatch(/artifact save failed/i);
|
|
1779
|
+
expect(result.ErrorMessage).toContain('session session-1');
|
|
1780
|
+
expect(result.ErrorMessage).toContain('db down');
|
|
1781
|
+
expect(errSpy.mock.calls.map((call) => String(call[0]))).toContain(result.ErrorMessage);
|
|
1709
1782
|
expect(version.Save).not.toHaveBeenCalled();
|
|
1710
1783
|
});
|
|
1711
1784
|
|
|
1712
1785
|
it('returns a structured failure (carrying the orphaned ArtifactID) when the version save fails', async () => {
|
|
1786
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
1713
1787
|
const failingVersion = makeSessionEntity({
|
|
1714
1788
|
ID: 'version-fail',
|
|
1715
1789
|
Save: vi.fn(async () => false),
|
|
@@ -1728,6 +1802,10 @@ describe('RealtimeClientSessionResolver.SaveSessionChannelArtifact', () => {
|
|
|
1728
1802
|
expect(result.Success).toBe(false);
|
|
1729
1803
|
expect(result.ArtifactID).toBe('artifact-1');
|
|
1730
1804
|
expect(result.ArtifactVersionID).toBeUndefined();
|
|
1805
|
+
expect(result.ErrorMessage).toMatch(/artifact version save failed/i);
|
|
1806
|
+
expect(result.ErrorMessage).toContain('session session-1');
|
|
1807
|
+
expect(result.ErrorMessage).toContain('too big');
|
|
1808
|
+
expect(errSpy.mock.calls.map((call) => String(call[0]))).toContain(result.ErrorMessage);
|
|
1731
1809
|
expect(junction.Save).not.toHaveBeenCalled();
|
|
1732
1810
|
});
|
|
1733
1811
|
|
|
@@ -2247,10 +2325,11 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
|
|
|
2247
2325
|
}): { provider: unknown; promptRun: FakeSession } {
|
|
2248
2326
|
const session =
|
|
2249
2327
|
opts.session ??
|
|
2250
|
-
makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
2251
|
-
const promptRun = opts.promptRun ?? makeSessionEntity({ ID:
|
|
2328
|
+
makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }) });
|
|
2329
|
+
const promptRun = opts.promptRun ?? makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
|
|
2252
2330
|
const provider = {
|
|
2253
2331
|
GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
|
|
2332
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
2254
2333
|
};
|
|
2255
2334
|
return { provider, promptRun };
|
|
2256
2335
|
}
|
|
@@ -2276,7 +2355,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
|
|
|
2276
2355
|
const ok = await resolver.RelayRealtimeUsage('session-1', 30, 5, makeCtx());
|
|
2277
2356
|
|
|
2278
2357
|
expect(ok).toBe(true);
|
|
2279
|
-
expect(promptRun.Load).toHaveBeenCalledWith(
|
|
2358
|
+
expect(promptRun.Load).toHaveBeenCalledWith(PROMPT_RUN_ID);
|
|
2280
2359
|
expect(promptRun.TokensPrompt).toBe(130);
|
|
2281
2360
|
expect(promptRun.TokensCompletion).toBe(45);
|
|
2282
2361
|
expect(promptRun.TokensUsed).toBe(175);
|
|
@@ -2373,7 +2452,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
|
|
|
2373
2452
|
const { provider, promptRun } = makeUsageProvider({
|
|
2374
2453
|
session: makeSessionEntity({
|
|
2375
2454
|
Status: 'Closed',
|
|
2376
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
2455
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
2377
2456
|
}),
|
|
2378
2457
|
});
|
|
2379
2458
|
currentProvider = provider;
|
|
@@ -2770,7 +2849,7 @@ describe('RealtimeClientSessionResolver — app awareness (applicationId / appCo
|
|
|
2770
2849
|
expect(relayArg.AllowedAgents?.map(a => a.agentId)).toEqual(['skip-1']);
|
|
2771
2850
|
});
|
|
2772
2851
|
|
|
2773
|
-
it('
|
|
2852
|
+
it('does not pass the directActions stored in the session config into ExecuteRelayedTool', async () => {
|
|
2774
2853
|
currentProvider = makeProvider(() =>
|
|
2775
2854
|
makeSessionEntity({
|
|
2776
2855
|
Config_: JSON.stringify({
|
|
@@ -2787,7 +2866,8 @@ describe('RealtimeClientSessionResolver — app awareness (applicationId / appCo
|
|
|
2787
2866
|
);
|
|
2788
2867
|
|
|
2789
2868
|
const relayArg = executeRelayedToolMock.mock.calls[0][0] as { DirectActions?: { enabled: boolean; actionNames: string[] } };
|
|
2790
|
-
|
|
2869
|
+
// The owner can edit Config, so the service takes direct actions from server-side state instead.
|
|
2870
|
+
expect(relayArg.DirectActions).toBeUndefined();
|
|
2791
2871
|
});
|
|
2792
2872
|
});
|
|
2793
2873
|
|
|
@@ -2880,14 +2960,52 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2880
2960
|
expect(executeRelayedToolMock.mock.calls[0][1]).toBe(ANON_USER);
|
|
2881
2961
|
});
|
|
2882
2962
|
|
|
2963
|
+
it('refuses a direct action from a scoped anonymous caller without dispatching it or writing a turn as the system user (#4791)', async () => {
|
|
2964
|
+
const detail = makeSessionEntity({ ID: 'detail-1' });
|
|
2965
|
+
currentProvider = {
|
|
2966
|
+
GetEntityObject: vi.fn(async (name: string) =>
|
|
2967
|
+
name === 'MJ: Conversation Details' ? detail : makeSessionEntity({ UserID: 'anon-1' }),
|
|
2968
|
+
),
|
|
2969
|
+
};
|
|
2970
|
+
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"success":true}', Success: true });
|
|
2971
|
+
const resolver = makeAnonResolver();
|
|
2972
|
+
|
|
2973
|
+
const out = await resolver.ExecuteRealtimeSessionTool(
|
|
2974
|
+
'session-1', 'call-1', 'Some Direct Action', '{}', makeCtx(), makePubSub(),
|
|
2975
|
+
);
|
|
2976
|
+
|
|
2977
|
+
// Scope-limited callers get no direct actions, so nothing is dispatched, elevated or not…
|
|
2978
|
+
expect(JSON.parse(out)).toMatchObject({ success: false });
|
|
2979
|
+
expect(executeRelayedToolMock).not.toHaveBeenCalled();
|
|
2980
|
+
// …and no hidden Conversation Detail turn is written, least of all as the system user.
|
|
2981
|
+
expect(getEntityObjectSpy()).not.toHaveBeenCalledWith('MJ: Conversation Details', SYSTEM_USER);
|
|
2982
|
+
expect(detail.Save).not.toHaveBeenCalled();
|
|
2983
|
+
});
|
|
2984
|
+
|
|
2985
|
+
it('writes the direct-action tool turn as the caller unchanged for a normal authenticated user', async () => {
|
|
2986
|
+
const detail = makeSessionEntity({ ID: 'detail-2' });
|
|
2987
|
+
currentProvider = {
|
|
2988
|
+
GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: Conversation Details' ? detail : makeSessionEntity())),
|
|
2989
|
+
};
|
|
2990
|
+
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"success":true}', Success: true });
|
|
2991
|
+
const resolver = makeResolver();
|
|
2992
|
+
|
|
2993
|
+
await resolver.ExecuteRealtimeSessionTool(
|
|
2994
|
+
'session-1', 'call-1', 'Some Direct Action', '{}', makeCtx(), makePubSub(),
|
|
2995
|
+
);
|
|
2996
|
+
|
|
2997
|
+
expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Details', USER);
|
|
2998
|
+
});
|
|
2999
|
+
|
|
2883
3000
|
it('accumulates relayed usage onto the prompt run under the SYSTEM user', async () => {
|
|
2884
3001
|
const session = makeSessionEntity({
|
|
2885
3002
|
UserID: 'anon-1',
|
|
2886
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
3003
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
2887
3004
|
});
|
|
2888
|
-
const promptRun = makeSessionEntity({ ID:
|
|
3005
|
+
const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
|
|
2889
3006
|
currentProvider = {
|
|
2890
3007
|
GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
|
|
3008
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
2891
3009
|
};
|
|
2892
3010
|
const resolver = makeAnonResolver();
|
|
2893
3011
|
|
|
@@ -2902,9 +3020,9 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2902
3020
|
it('mirrors the transcript turn onto the prompt run as SYSTEM while the Conversation Detail stays the caller', async () => {
|
|
2903
3021
|
const session = makeSessionEntity({
|
|
2904
3022
|
UserID: 'anon-1',
|
|
2905
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
3023
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
2906
3024
|
});
|
|
2907
|
-
const promptRun = makeSessionEntity({ ID:
|
|
3025
|
+
const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
|
|
2908
3026
|
const detail = makeSessionEntity({ ID: 'detail-1' });
|
|
2909
3027
|
currentProvider = {
|
|
2910
3028
|
GetEntityObject: vi.fn(async (name: string) => {
|
|
@@ -2912,6 +3030,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2912
3030
|
if (name === 'MJ: Conversation Details') return detail;
|
|
2913
3031
|
return session;
|
|
2914
3032
|
}),
|
|
3033
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
2915
3034
|
};
|
|
2916
3035
|
const resolver = makeAnonResolver();
|
|
2917
3036
|
|
|
@@ -2928,18 +3047,46 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2928
3047
|
it('records the co-agent tool turn (RelayRealtimeToolTurn) under the SYSTEM user', async () => {
|
|
2929
3048
|
const session = makeSessionEntity({
|
|
2930
3049
|
UserID: 'anon-1',
|
|
2931
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
3050
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
2932
3051
|
});
|
|
2933
|
-
const promptRun = makeSessionEntity({ ID:
|
|
3052
|
+
const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
|
|
2934
3053
|
currentProvider = {
|
|
2935
3054
|
GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
|
|
3055
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
3056
|
+
};
|
|
3057
|
+
const resolver = makeAnonResolver();
|
|
3058
|
+
|
|
3059
|
+
const ok = await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{"url":"x"}');
|
|
3060
|
+
|
|
3061
|
+
expect(ok).toBe(true);
|
|
3062
|
+
expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: AI Prompt Runs', SYSTEM_USER);
|
|
3063
|
+
});
|
|
3064
|
+
|
|
3065
|
+
it('writes the hidden co-agent tool turn (RelayRealtimeToolTurn) as the CALLER (#4791)', async () => {
|
|
3066
|
+
const session = makeSessionEntity({
|
|
3067
|
+
UserID: 'anon-1',
|
|
3068
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
3069
|
+
});
|
|
3070
|
+
const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
|
|
3071
|
+
const detail = makeSessionEntity({ ID: 'detail-3' });
|
|
3072
|
+
currentProvider = {
|
|
3073
|
+
GetEntityObject: vi.fn(async (name: string) => {
|
|
3074
|
+
if (name === 'MJ: AI Prompt Runs') return promptRun;
|
|
3075
|
+
if (name === 'MJ: Conversation Details') return detail;
|
|
3076
|
+
return session;
|
|
3077
|
+
}),
|
|
3078
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
2936
3079
|
};
|
|
2937
3080
|
const resolver = makeAnonResolver();
|
|
2938
3081
|
|
|
2939
3082
|
const ok = await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{"url":"x"}');
|
|
2940
3083
|
|
|
2941
3084
|
expect(ok).toBe(true);
|
|
3085
|
+
// The co-agent run mirror stays SYSTEM…
|
|
2942
3086
|
expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: AI Prompt Runs', SYSTEM_USER);
|
|
3087
|
+
// …but the hidden Conversation Detail tool-turn is written as the CALLER, never SYSTEM.
|
|
3088
|
+
expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Details', ANON_USER);
|
|
3089
|
+
expect(getEntityObjectSpy()).not.toHaveBeenCalledWith('MJ: Conversation Details', SYSTEM_USER);
|
|
2943
3090
|
});
|
|
2944
3091
|
|
|
2945
3092
|
it('prepares the session with observability under the SYSTEM user while UserID stays the visitor', async () => {
|
|
@@ -2971,6 +3118,30 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2971
3118
|
expect(prepInput.UserID).toBe('anon-1');
|
|
2972
3119
|
});
|
|
2973
3120
|
|
|
3121
|
+
/** Starts a session as `resolver`'s caller and returns the input the prepare service received. */
|
|
3122
|
+
async function startAndCapturePrepInput(resolver: RealtimeClientSessionResolver, userID: string): Promise<{ ExcludeDirectActions?: boolean }> {
|
|
3123
|
+
currentProvider = makeProvider(() => makeSessionEntity({ UserID: userID }));
|
|
3124
|
+
createSessionMock.mockResolvedValue(makeSessionEntity({ ID: 'session-start', UserID: userID }));
|
|
3125
|
+
prepareClientSessionMock.mockResolvedValue({
|
|
3126
|
+
Success: true,
|
|
3127
|
+
ClientConfig: { Provider: 'openai', Model: 'gpt-realtime', EphemeralToken: 'ek_abc', ExpiresAt: '2026-01-01T00:00:00Z', SessionConfig: {} },
|
|
3128
|
+
});
|
|
3129
|
+
await resolver.StartRealtimeClientSession('target-1', makeCtx());
|
|
3130
|
+
return prepareClientSessionMock.mock.calls[0][0] as { ExcludeDirectActions?: boolean };
|
|
3131
|
+
}
|
|
3132
|
+
|
|
3133
|
+
it('prepares a scope-limited caller\'s session with no direct actions', async () => {
|
|
3134
|
+
const prepInput = await startAndCapturePrepInput(makeAnonResolver(), 'anon-1');
|
|
3135
|
+
|
|
3136
|
+
expect(prepInput.ExcludeDirectActions).toBe(true);
|
|
3137
|
+
});
|
|
3138
|
+
|
|
3139
|
+
it('prepares a caller that is not scope-limited with direct actions', async () => {
|
|
3140
|
+
const prepInput = await startAndCapturePrepInput(makeResolver(), 'user-1');
|
|
3141
|
+
|
|
3142
|
+
expect(prepInput.ExcludeDirectActions).toBe(false);
|
|
3143
|
+
});
|
|
3144
|
+
|
|
2974
3145
|
it('junction-links delegated artifacts under the SYSTEM user and stamps the hidden anchor with the SESSION owner', async () => {
|
|
2975
3146
|
const session = makeSessionEntity({ UserID: 'anon-1' });
|
|
2976
3147
|
const junctions: FakeSession[] = [];
|
|
@@ -3003,6 +3174,10 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
3003
3174
|
// The junction write (an entity the anon role does NOT hold) runs as the SYSTEM user.
|
|
3004
3175
|
expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Detail Artifacts', SYSTEM_USER);
|
|
3005
3176
|
expect(junctions).toHaveLength(1);
|
|
3177
|
+
// The hidden anchor `Conversation Detail` is written as the CALLER (#4791) — like every
|
|
3178
|
+
// other `MJ: Conversation Details` write, the System user is refused as not the owner.
|
|
3179
|
+
expect(getEntityObjectSpy()).toHaveBeenCalledWith('MJ: Conversation Details', ANON_USER);
|
|
3180
|
+
expect(getEntityObjectSpy()).not.toHaveBeenCalledWith('MJ: Conversation Details', SYSTEM_USER);
|
|
3006
3181
|
// The hidden anchor is attributed to the SESSION owner, not the elevated principal.
|
|
3007
3182
|
expect(anchors).toHaveLength(1);
|
|
3008
3183
|
expect(anchors[0].UserID).toBe('anon-1');
|
|
@@ -3102,6 +3277,108 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
3102
3277
|
expect((storeRecordingMock.mock.calls[0][0] as { ContextUser: unknown }).ContextUser).toBe(USER);
|
|
3103
3278
|
expect(getSystemUserMock).not.toHaveBeenCalled();
|
|
3104
3279
|
});
|
|
3280
|
+
|
|
3281
|
+
/** Provider routing a hidden-tool-turn RelayRealtimeToolTurn call: session, prompt run, detail. */
|
|
3282
|
+
function makeToolTurnProvider(detail: FakeSession): unknown {
|
|
3283
|
+
const session = makeSessionEntity({
|
|
3284
|
+
UserID: 'anon-1',
|
|
3285
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
3286
|
+
});
|
|
3287
|
+
const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
|
|
3288
|
+
return {
|
|
3289
|
+
GetEntityObject: vi.fn(async (name: string) => {
|
|
3290
|
+
if (name === 'MJ: AI Prompt Runs') return promptRun;
|
|
3291
|
+
if (name === 'MJ: Conversation Details') return detail;
|
|
3292
|
+
return session;
|
|
3293
|
+
}),
|
|
3294
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
3295
|
+
};
|
|
3296
|
+
}
|
|
3297
|
+
|
|
3298
|
+
it('logs the session id, write user id and "no failure detail recorded" when a hidden tool turn save is refused without a LatestResult (#4791)', async () => {
|
|
3299
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
3300
|
+
const detail = makeSessionEntity({
|
|
3301
|
+
ID: 'detail-1',
|
|
3302
|
+
Save: vi.fn(async () => false),
|
|
3303
|
+
LatestResult: undefined,
|
|
3304
|
+
ResultHistory: [],
|
|
3305
|
+
});
|
|
3306
|
+
currentProvider = makeToolTurnProvider(detail);
|
|
3307
|
+
const resolver = makeAnonResolver();
|
|
3308
|
+
|
|
3309
|
+
await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{}');
|
|
3310
|
+
|
|
3311
|
+
const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
|
|
3312
|
+
const failureLine = loggedLines.find((line) => line.includes('persistDirectActionTurn'));
|
|
3313
|
+
expect(failureLine).toBeDefined();
|
|
3314
|
+
// Exact substrings: 'anon-1' alone is the caller, session.UserID AND detail.UserID at once,
|
|
3315
|
+
// so only the labelled form proves the WRITE user is what got logged.
|
|
3316
|
+
expect(failureLine).toContain('session session-1');
|
|
3317
|
+
expect(failureLine).toContain('write user anon-1');
|
|
3318
|
+
expect(failureLine).toContain('no failure detail recorded (LatestResult null; ResultHistory length 0)');
|
|
3319
|
+
expect(failureLine).toContain('the write was refused before reaching the provider');
|
|
3320
|
+
expect(failureLine).not.toContain('unknown error');
|
|
3321
|
+
});
|
|
3322
|
+
|
|
3323
|
+
it('does not claim WHERE the write was refused when a result was registered with an empty message (#4791)', async () => {
|
|
3324
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
3325
|
+
const detail = makeSessionEntity({
|
|
3326
|
+
ID: 'detail-1',
|
|
3327
|
+
Save: vi.fn(async () => false),
|
|
3328
|
+
LatestResult: { Success: false, CompleteMessage: ' ' },
|
|
3329
|
+
ResultHistory: [{}],
|
|
3330
|
+
});
|
|
3331
|
+
currentProvider = makeToolTurnProvider(detail);
|
|
3332
|
+
const resolver = makeAnonResolver();
|
|
3333
|
+
|
|
3334
|
+
await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{}');
|
|
3335
|
+
|
|
3336
|
+
const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
|
|
3337
|
+
const failureLine = loggedLines.find((line) => line.includes('persistDirectActionTurn'));
|
|
3338
|
+
expect(failureLine).toBeDefined();
|
|
3339
|
+
expect(failureLine).toContain('no failure detail recorded (LatestResult empty; ResultHistory length 1)');
|
|
3340
|
+
expect(failureLine).not.toContain('refused before reaching the provider');
|
|
3341
|
+
});
|
|
3342
|
+
|
|
3343
|
+
it('never logs a PRIOR SUCCESS entry\'s text as the failure reason when the refusal registered nothing (#4791)', async () => {
|
|
3344
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
3345
|
+
const detail = makeSessionEntity({
|
|
3346
|
+
ID: 'detail-1',
|
|
3347
|
+
Save: vi.fn(async () => false),
|
|
3348
|
+
LatestResult: { Success: true, CompleteMessage: 'previous save ok' },
|
|
3349
|
+
ResultHistory: [{}, {}],
|
|
3350
|
+
});
|
|
3351
|
+
currentProvider = makeToolTurnProvider(detail);
|
|
3352
|
+
const resolver = makeAnonResolver();
|
|
3353
|
+
|
|
3354
|
+
await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{}');
|
|
3355
|
+
|
|
3356
|
+
const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
|
|
3357
|
+
const failureLine = loggedLines.find((line) => line.includes('persistDirectActionTurn'));
|
|
3358
|
+
expect(failureLine).toBeDefined();
|
|
3359
|
+
expect(failureLine).toContain(
|
|
3360
|
+
'no failure detail recorded (latest result entry is a prior success; ResultHistory length 2)',
|
|
3361
|
+
);
|
|
3362
|
+
expect(failureLine).not.toContain('previous save ok');
|
|
3363
|
+
});
|
|
3364
|
+
|
|
3365
|
+
it('logs the real CompleteMessage when present (#4791)', async () => {
|
|
3366
|
+
errSpy = vi.spyOn(console, 'error').mockImplementation(() => undefined);
|
|
3367
|
+
const detail = makeSessionEntity({
|
|
3368
|
+
ID: 'detail-1',
|
|
3369
|
+
Save: vi.fn(async () => false),
|
|
3370
|
+
LatestResult: { CompleteMessage: 'You do not have access to this conversation.' },
|
|
3371
|
+
});
|
|
3372
|
+
currentProvider = makeToolTurnProvider(detail);
|
|
3373
|
+
const resolver = makeAnonResolver();
|
|
3374
|
+
|
|
3375
|
+
await resolver.RelayRealtimeToolTurn('session-1', 'browser_navigate', makeCtx(), '{}');
|
|
3376
|
+
|
|
3377
|
+
const loggedLines = errSpy.mock.calls.map((call) => String(call[0]));
|
|
3378
|
+
const failureLine = loggedLines.find((line) => line.includes('persistDirectActionTurn'));
|
|
3379
|
+
expect(failureLine).toBeDefined();
|
|
3380
|
+
expect(failureLine).toContain('You do not have access to this conversation.');
|
|
3381
|
+
});
|
|
3105
3382
|
});
|
|
3106
3383
|
|
|
3107
3384
|
describe('RealtimeClientSessionResolver — colleague authorization (allowedAgents)', () => {
|
|
@@ -3174,7 +3451,8 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
|
|
|
3174
3451
|
|
|
3175
3452
|
it('excludes every colleague when the caller can run none of them', async () => {
|
|
3176
3453
|
currentProvider = makeProvider(() => makeMultiTargetSession('user-1'));
|
|
3177
|
-
|
|
3454
|
+
// The caller can run the lead target, and none of the colleagues.
|
|
3455
|
+
hasPermissionMock.mockImplementation((...args: unknown[]) => Promise.resolve(args[0] === 'target-1'));
|
|
3178
3456
|
|
|
3179
3457
|
await makeResolver().ExecuteRealtimeSessionTool(
|
|
3180
3458
|
'session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub(),
|
|
@@ -3183,7 +3461,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
|
|
|
3183
3461
|
expect(dispatchedAgentIDs()).toEqual([]);
|
|
3184
3462
|
});
|
|
3185
3463
|
|
|
3186
|
-
it('passes a single-target session through with
|
|
3464
|
+
it('passes a single-target session through with only the target permission check', async () => {
|
|
3187
3465
|
currentProvider = makeProvider(() => makeSessionEntity());
|
|
3188
3466
|
hasPermissionMock.mockResolvedValue(true);
|
|
3189
3467
|
|
|
@@ -3193,6 +3471,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
|
|
|
3193
3471
|
|
|
3194
3472
|
const input = executeRelayedToolMock.mock.calls[0][0] as { AllowedAgents?: unknown };
|
|
3195
3473
|
expect(input.AllowedAgents).toBeUndefined();
|
|
3196
|
-
expect(hasPermissionMock).
|
|
3474
|
+
expect(hasPermissionMock).toHaveBeenCalledTimes(1);
|
|
3475
|
+
expect((hasPermissionMock.mock.calls[0] as unknown[])[0]).toBe('target-1');
|
|
3197
3476
|
});
|
|
3198
3477
|
});
|