@memberjunction/server 5.51.3 → 5.51.4
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 +34 -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 +136 -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/index.d.ts +1 -0
- package/dist/auth/index.d.ts.map +1 -1
- package/dist/auth/index.js +1 -0
- package/dist/auth/index.js.map +1 -1
- package/dist/auth/scopeLimitedPrincipal.d.ts +39 -0
- package/dist/auth/scopeLimitedPrincipal.d.ts.map +1 -0
- package/dist/auth/scopeLimitedPrincipal.js +49 -0
- package/dist/auth/scopeLimitedPrincipal.js.map +1 -0
- package/dist/config.d.ts +53 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +34 -1
- package/dist/config.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/generated.d.ts.map +1 -1
- package/dist/generated/generated.js +3262 -3273
- package/dist/generated/generated.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +124 -4
- package/dist/generic/CacheInvalidationResolver.d.ts.map +1 -1
- package/dist/generic/CacheInvalidationResolver.js +170 -15
- package/dist/generic/CacheInvalidationResolver.js.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.d.ts +5 -0
- package/dist/generic/FireAndForgetHeartbeat.d.ts.map +1 -1
- package/dist/generic/FireAndForgetHeartbeat.js +5 -4
- package/dist/generic/FireAndForgetHeartbeat.js.map +1 -1
- package/dist/generic/PushStatusResolver.d.ts +58 -0
- package/dist/generic/PushStatusResolver.d.ts.map +1 -1
- package/dist/generic/PushStatusResolver.js +42 -3
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +148 -3
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +287 -30
- 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.map +1 -1
- package/dist/index.js +56 -48
- package/dist/index.js.map +1 -1
- 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 +2 -2
- package/dist/realtimeWidget/widgetGuestElevation.js +2 -2
- 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 +39 -17
- package/dist/resolvers/AdhocQueryResolver.d.ts.map +1 -1
- package/dist/resolvers/AdhocQueryResolver.js +97 -157
- 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 +14 -4
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +18 -6
- package/dist/resolvers/EntityRecordNameResolver.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/MCPResolver.d.ts.map +1 -1
- package/dist/resolvers/MCPResolver.js +13 -16
- package/dist/resolvers/MCPResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +26 -14
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +63 -46
- 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 +24 -27
- package/dist/resolvers/RemoteBrowserActionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +30 -40
- 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/RunTestResolver.d.ts.map +1 -1
- package/dist/resolvers/RunTestResolver.js +90 -112
- package/dist/resolvers/RunTestResolver.js.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.d.ts.map +1 -1
- package/dist/resolvers/SyncRolesUsersResolver.js +11 -2
- package/dist/resolvers/SyncRolesUsersResolver.js.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.d.ts.map +1 -1
- package/dist/resolvers/VersionHistoryResolver.js +8 -12
- package/dist/resolvers/VersionHistoryResolver.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/TaskOrchestrator.d.ts.map +1 -1
- package/dist/services/TaskOrchestrator.js +17 -15
- package/dist/services/TaskOrchestrator.js.map +1 -1
- package/package.json +90 -89
- 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 +84 -0
- package/src/__tests__/AdhocQueryResolver.sqlAuthorizer.test.ts +104 -0
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- 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 +136 -0
- package/src/__tests__/RealtimeClientSessionResolver.configTamper.test.ts +318 -0
- package/src/__tests__/RealtimeClientSessionResolver.test.ts +66 -27
- package/src/__tests__/RemoteBrowserActionResolver.goalRunLink.test.ts +134 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +221 -0
- package/src/__tests__/ResolverBase.aggregateScreen.test.ts +173 -0
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +116 -0
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +351 -0
- package/src/__tests__/RunTemplateResolver.test.ts +132 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +164 -0
- package/src/__tests__/SessionManager.test.ts +88 -13
- package/src/__tests__/SessionRunIDVerifier.test.ts +255 -0
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +155 -0
- package/src/__tests__/actionAuthorization.test.ts +130 -0
- package/src/__tests__/adhocTimeoutClamp.test.ts +26 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +241 -0
- package/src/__tests__/multiTenancy.postgresRewrite.test.ts +180 -0
- package/src/__tests__/multiTenancy.security.test.ts +51 -0
- package/src/__tests__/multiTenancy.test.ts +1 -1
- package/src/__tests__/newUserRolesDefault.test.ts +81 -0
- package/src/__tests__/postgresqlPoolSettings.test.ts +131 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +56 -0
- package/src/__tests__/resolverBase.refusalMessage.test.ts +88 -0
- package/src/__tests__/resolverBase.rls.test.ts +2 -0
- package/src/__tests__/translateBracketsToPG.test.ts +20 -0
- package/src/agentSessions/SessionManager.ts +24 -25
- package/src/agentSessions/SessionRunIDVerifier.ts +179 -0
- package/src/agentSessions/index.ts +1 -0
- package/src/auth/actionAuthorization.ts +119 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/scopeLimitedPrincipal.ts +50 -0
- package/src/config.ts +34 -1
- package/src/context.ts +25 -34
- package/src/generated/generated.ts +3262 -3273
- package/src/generic/CacheInvalidationResolver.ts +206 -15
- package/src/generic/FireAndForgetHeartbeat.ts +10 -4
- package/src/generic/PushStatusResolver.ts +79 -4
- package/src/generic/ResolverBase.ts +339 -31
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/index.ts +63 -50
- package/src/multiTenancy/index.ts +24 -2
- package/src/postgresqlCompat.ts +21 -0
- package/src/postgresqlPoolSettings.ts +213 -0
- package/src/realtimeWidget/widgetGuestElevation.ts +2 -2
- package/src/resolvers/ActionResolver.ts +85 -22
- package/src/resolvers/AdhocQueryResolver.ts +110 -177
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +24 -7
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +14 -1
- package/src/resolvers/MCPResolver.ts +13 -15
- package/src/resolvers/RealtimeClientSessionResolver.ts +82 -50
- package/src/resolvers/RemoteBrowserActionResolver.ts +30 -26
- package/src/resolvers/RunAIAgentResolver.ts +30 -40
- package/src/resolvers/RunTemplateResolver.ts +13 -12
- package/src/resolvers/RunTestResolver.ts +90 -112
- package/src/resolvers/SyncRolesUsersResolver.ts +11 -2
- package/src/resolvers/VersionHistoryResolver.ts +10 -13
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/__tests__/ReportResolver.test.ts +2 -0
- package/src/resolvers/adhocTimeout.ts +12 -0
- package/src/rest/ViewOperationsHandler.ts +37 -18
- package/src/services/TaskOrchestrator.ts +17 -17
- 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
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A realtime session's `Config` JSON is writable by the session owner: the UI and Widget Guest
|
|
3
|
+
* roles hold Create/Update on `MJ: AI Agent Sessions`, row-scoped only to their own sessions. These
|
|
4
|
+
* tests edit `Config` the way such an owner could and check that the relay mutations do not take
|
|
5
|
+
* authority from it: the target agent is re-authorized for the caller, and run ids are used only
|
|
6
|
+
* when they are records of the session.
|
|
7
|
+
*/
|
|
8
|
+
// type-graphql decorators on the resolver need the reflect-metadata polyfill loaded first.
|
|
9
|
+
import 'reflect-metadata';
|
|
10
|
+
|
|
11
|
+
import { describe, it, expect, vi, beforeEach, type Mock } from 'vitest';
|
|
12
|
+
|
|
13
|
+
const hasPermissionMock = vi.fn(async (_agentID: string, _user?: unknown, _permission?: string): Promise<boolean> => true);
|
|
14
|
+
vi.mock('@memberjunction/ai-engine-base', () => ({
|
|
15
|
+
AIAgentPermissionHelper: {
|
|
16
|
+
HasPermission: (agentID: string, user: unknown, permission: string) => hasPermissionMock(agentID, user, permission),
|
|
17
|
+
},
|
|
18
|
+
AIEngineBase: {
|
|
19
|
+
GetProviderInstance: vi.fn(),
|
|
20
|
+
},
|
|
21
|
+
}));
|
|
22
|
+
|
|
23
|
+
vi.mock('@memberjunction/aiengine', () => ({
|
|
24
|
+
AIEngine: {
|
|
25
|
+
Instance: {
|
|
26
|
+
Config: vi.fn(async () => undefined),
|
|
27
|
+
Agents: [],
|
|
28
|
+
AgentTypes: [],
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
}));
|
|
32
|
+
|
|
33
|
+
const executeRelayedToolMock = vi.fn();
|
|
34
|
+
const appendPromptRunMessageMock = vi.fn<RealtimeClientSessionService['AppendPromptRunMessage']>(async () => true);
|
|
35
|
+
const accumulatePromptRunUsageMock = vi.fn<RealtimeClientSessionService['AccumulatePromptRunUsage']>(async () => true);
|
|
36
|
+
vi.mock('@memberjunction/ai-agents', async (importOriginal) => {
|
|
37
|
+
const actual = await importOriginal<Record<string, unknown>>();
|
|
38
|
+
return {
|
|
39
|
+
...actual,
|
|
40
|
+
RealtimeClientSessionService: class {
|
|
41
|
+
ExecuteRelayedTool = executeRelayedToolMock;
|
|
42
|
+
AppendPromptRunMessage = appendPromptRunMessageMock;
|
|
43
|
+
AccumulatePromptRunUsage = accumulatePromptRunUsageMock;
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const heartbeatMock = vi.fn(async () => true);
|
|
49
|
+
vi.mock('../agentSessions/index.js', () => ({
|
|
50
|
+
SessionManager: class {
|
|
51
|
+
Heartbeat = heartbeatMock;
|
|
52
|
+
},
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
let currentProvider: unknown;
|
|
56
|
+
vi.mock('../util.js', () => ({
|
|
57
|
+
GetReadWriteProvider: () => currentProvider,
|
|
58
|
+
}));
|
|
59
|
+
|
|
60
|
+
const getSystemUserMock = vi.fn<() => UserInfo | undefined>();
|
|
61
|
+
vi.mock('@memberjunction/sqlserver-dataprovider', async (importOriginal) => {
|
|
62
|
+
const actual = await importOriginal<Record<string, unknown>>();
|
|
63
|
+
return {
|
|
64
|
+
...actual,
|
|
65
|
+
UserCache: {
|
|
66
|
+
get Instance() {
|
|
67
|
+
return { GetSystemUser: getSystemUserMock };
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
};
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
import { RealtimeClientSessionResolver } from '../resolvers/RealtimeClientSessionResolver.js';
|
|
74
|
+
import type { AppContext } from '../types.js';
|
|
75
|
+
import { IsPlatformSQL, type RunViewParams, type RunViewResult, type UserInfo } from '@memberjunction/core';
|
|
76
|
+
import type { ExecuteRelayedToolInput, RealtimeClientSessionService } from '@memberjunction/ai-agents';
|
|
77
|
+
import type { PubSubEngine } from 'type-graphql';
|
|
78
|
+
|
|
79
|
+
const SESSION_ID = 'c3c3c3c3-0000-4000-8000-000000000001';
|
|
80
|
+
const USER_ID = 'c3c3c3c3-0000-4000-8000-000000000002';
|
|
81
|
+
const TARGET_ID = 'c3c3c3c3-0000-4000-8000-000000000003';
|
|
82
|
+
const OTHER_AGENT_ID = 'c3c3c3c3-0000-4000-8000-000000000004';
|
|
83
|
+
const CO_RUN_ID = 'c3c3c3c3-0000-4000-8000-000000000005';
|
|
84
|
+
const PROMPT_RUN_ID = 'c3c3c3c3-0000-4000-8000-000000000006';
|
|
85
|
+
const STEP_ID = 'c3c3c3c3-0000-4000-8000-000000000007';
|
|
86
|
+
const PAUSED_RUN_ID = 'c3c3c3c3-0000-4000-8000-000000000008';
|
|
87
|
+
/** A run that exists, but belongs to someone else's session. */
|
|
88
|
+
const FOREIGN_RUN_ID = 'c3c3c3c3-0000-4000-8000-0000000000f1';
|
|
89
|
+
/** Harmless text that is not a UUID and would change the meaning of a quoted filter. */
|
|
90
|
+
const NOT_A_UUID = "x' OR '1'='1";
|
|
91
|
+
|
|
92
|
+
const USER = { ID: USER_ID, Email: 'owner@example.com' };
|
|
93
|
+
|
|
94
|
+
interface FakeEntity {
|
|
95
|
+
[key: string]: unknown;
|
|
96
|
+
Save: () => Promise<boolean>;
|
|
97
|
+
Load: (id: string) => Promise<boolean>;
|
|
98
|
+
NewRecord: () => void;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Rows the fake RunView returns, keyed by entity name. */
|
|
102
|
+
type FakeRows = Record<string, Array<Record<string, unknown>>>;
|
|
103
|
+
|
|
104
|
+
/** The fake provider's `RunView`: it returns the rows for the requested entity. */
|
|
105
|
+
type FakeRunView = (params: RunViewParams) => Promise<RunViewResult<Record<string, unknown>>>;
|
|
106
|
+
|
|
107
|
+
function makeSession(config: Record<string, unknown>, overrides: Partial<FakeEntity> = {}): FakeEntity {
|
|
108
|
+
return {
|
|
109
|
+
ID: SESSION_ID,
|
|
110
|
+
Status: 'Active',
|
|
111
|
+
UserID: USER_ID,
|
|
112
|
+
ConversationID: 'c3c3c3c3-0000-4000-8000-0000000000c1',
|
|
113
|
+
Config_: JSON.stringify(config),
|
|
114
|
+
Save: vi.fn(async () => true),
|
|
115
|
+
Load: vi.fn(async () => true),
|
|
116
|
+
NewRecord: vi.fn(),
|
|
117
|
+
LatestResult: null,
|
|
118
|
+
...overrides,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function makeDetail(): FakeEntity {
|
|
123
|
+
return { ID: 'c3c3c3c3-0000-4000-8000-0000000000d1', Save: vi.fn(async () => true), Load: vi.fn(async () => true), NewRecord: vi.fn() };
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** A provider serving the session plus view rows per entity name; `runView` records every view run. */
|
|
127
|
+
function makeProvider(session: FakeEntity, rows: FakeRows = {}): { runView: Mock<FakeRunView> } {
|
|
128
|
+
const runView = vi.fn<FakeRunView>(async (params) => {
|
|
129
|
+
const results = rows[params.EntityName ?? ''] ?? [];
|
|
130
|
+
return { Success: true, Results: results, RowCount: results.length, TotalRowCount: results.length, ExecutionTime: 0, ErrorMessage: '' };
|
|
131
|
+
});
|
|
132
|
+
currentProvider = {
|
|
133
|
+
GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Agent Sessions' ? session : makeDetail())),
|
|
134
|
+
RunView: runView,
|
|
135
|
+
RunViews: vi.fn(async (paramsList: RunViewParams[]) => Promise.all(paramsList.map((params) => runView(params)))),
|
|
136
|
+
};
|
|
137
|
+
return { runView };
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Rows that make every run id in `ids` a record of this session. */
|
|
141
|
+
function sessionRuns(...ids: string[]): FakeRows {
|
|
142
|
+
return { 'MJ: AI Agent Runs': ids.map((ID) => ({ ID })) };
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function makeResolver(user: object = USER): RealtimeClientSessionResolver {
|
|
146
|
+
const resolver = new RealtimeClientSessionResolver();
|
|
147
|
+
(resolver as unknown as { GetUserFromPayload: () => unknown }).GetUserFromPayload = () => user;
|
|
148
|
+
return resolver;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function makeCtx(): AppContext {
|
|
152
|
+
return { userPayload: { sessionId: 'c3-pubsub-session' }, providers: [] } as unknown as AppContext;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function makePubSub(): PubSubEngine {
|
|
156
|
+
return { publish: vi.fn(async () => undefined) } as unknown as PubSubEngine;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function relay(resolver: RealtimeClientSessionResolver): Promise<string> {
|
|
160
|
+
return relayTool(resolver, 'invoke-target-agent', '{"request":"hi"}');
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function relayTool(resolver: RealtimeClientSessionResolver, toolName: string, argsJson = '{}'): Promise<string> {
|
|
164
|
+
return resolver.ExecuteRealtimeSessionTool(SESSION_ID, 'call-1', toolName, argsJson, makeCtx(), makePubSub());
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/** A scoped anonymous magic-link caller: the relay runs its work as the system user. */
|
|
168
|
+
const SCOPED_ANONYMOUS = { ID: USER_ID, Email: 'guest@magic-link.local', IsMagicLinkAnonymous: true, MagicLinkScope: { ResourceID: 'c3-scope' } };
|
|
169
|
+
/** A public web-widget guest: anonymous, but not elevated. */
|
|
170
|
+
const WIDGET_GUEST = { ID: USER_ID, Email: 'guest@widget.local', IsMagicLinkAnonymous: true, WidgetGuestContext: { WidgetID: 'c3c3c3c3-0000-4000-8000-0000000000b1' } };
|
|
171
|
+
/** A resource-scoped magic-link session: a normal role pinned to one shared resource. */
|
|
172
|
+
const RESOURCE_SCOPED = { ID: USER_ID, Email: 'invitee@example.com', MagicLinkScope: { ResourceID: 'c3-shared-resource' } };
|
|
173
|
+
const SYSTEM_USER = { ID: 'c3c3c3c3-0000-4000-8000-0000000000aa', Email: 'system@example.com' } as UserInfo;
|
|
174
|
+
|
|
175
|
+
function relayInput(): ExecuteRelayedToolInput {
|
|
176
|
+
return executeRelayedToolMock.mock.calls[0][0] as ExecuteRelayedToolInput;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/** The SQL text of each filter the fake RunView received, with every variant of a platform-specific filter. */
|
|
180
|
+
function issuedFilters(runView: Mock<FakeRunView>): string[] {
|
|
181
|
+
return runView.mock.calls.map(([params]) => {
|
|
182
|
+
const filter = params.ExtraFilter;
|
|
183
|
+
return IsPlatformSQL(filter) ? [filter.default, filter.sqlserver, filter.postgresql].join('\n') : (filter ?? '');
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
beforeEach(() => {
|
|
188
|
+
hasPermissionMock.mockReset();
|
|
189
|
+
hasPermissionMock.mockImplementation(async () => true);
|
|
190
|
+
executeRelayedToolMock.mockReset();
|
|
191
|
+
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
|
|
192
|
+
appendPromptRunMessageMock.mockReset();
|
|
193
|
+
appendPromptRunMessageMock.mockResolvedValue(true);
|
|
194
|
+
accumulatePromptRunUsageMock.mockReset();
|
|
195
|
+
accumulatePromptRunUsageMock.mockResolvedValue(true);
|
|
196
|
+
heartbeatMock.mockClear();
|
|
197
|
+
getSystemUserMock.mockReset();
|
|
198
|
+
getSystemUserMock.mockReturnValue(undefined);
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
describe('ExecuteRealtimeSessionTool — target agent named in Config', () => {
|
|
202
|
+
it('refuses to relay when the caller cannot run the target agent in the session config', async () => {
|
|
203
|
+
hasPermissionMock.mockImplementation(async (agentID: string) => agentID !== OTHER_AGENT_ID);
|
|
204
|
+
makeProvider(makeSession({ targetAgentID: OTHER_AGENT_ID }));
|
|
205
|
+
|
|
206
|
+
await expect(relay(makeResolver())).rejects.toThrow(/not authorized/i);
|
|
207
|
+
expect(executeRelayedToolMock).not.toHaveBeenCalled();
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
it('refuses before elevating a scoped anonymous caller to the system user', async () => {
|
|
211
|
+
getSystemUserMock.mockReturnValue(SYSTEM_USER);
|
|
212
|
+
hasPermissionMock.mockImplementation(async (agentID: string) => agentID !== OTHER_AGENT_ID);
|
|
213
|
+
makeProvider(makeSession({ targetAgentID: OTHER_AGENT_ID }));
|
|
214
|
+
|
|
215
|
+
await expect(relay(makeResolver(SCOPED_ANONYMOUS))).rejects.toThrow(/not authorized/i);
|
|
216
|
+
expect(executeRelayedToolMock).not.toHaveBeenCalled();
|
|
217
|
+
// CanRun was asked about the caller, never the system user.
|
|
218
|
+
expect(hasPermissionMock).toHaveBeenCalledWith(OTHER_AGENT_ID, SCOPED_ANONYMOUS, 'run');
|
|
219
|
+
expect(hasPermissionMock.mock.calls.every(([, user]) => user === SCOPED_ANONYMOUS)).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('relays when the caller can run the target agent', async () => {
|
|
223
|
+
makeProvider(makeSession({ targetAgentID: TARGET_ID }));
|
|
224
|
+
|
|
225
|
+
await expect(relay(makeResolver())).resolves.toBe('{"ok":true}');
|
|
226
|
+
expect(relayInput().TargetAgentID).toBe(TARGET_ID);
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe('ExecuteRealtimeSessionTool — run ids in Config', () => {
|
|
231
|
+
it('drops run ids that are not UUIDs, and never puts them in a filter', async () => {
|
|
232
|
+
const { runView } = makeProvider(makeSession({ targetAgentID: TARGET_ID, coAgentRunID: NOT_A_UUID, pendingFeedbackRunID: NOT_A_UUID }));
|
|
233
|
+
|
|
234
|
+
await relay(makeResolver());
|
|
235
|
+
|
|
236
|
+
expect(relayInput().ParentRunID).toBeUndefined();
|
|
237
|
+
expect(relayInput().ResumeRunID).toBeUndefined();
|
|
238
|
+
expect(issuedFilters(runView).filter((f) => f.includes(NOT_A_UUID))).toEqual([]);
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
it('drops run ids that are not records of this session', async () => {
|
|
242
|
+
const { runView } = makeProvider(
|
|
243
|
+
makeSession({ targetAgentID: TARGET_ID, coAgentRunID: FOREIGN_RUN_ID, pendingFeedbackRunID: FOREIGN_RUN_ID }),
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
await relay(makeResolver());
|
|
247
|
+
|
|
248
|
+
expect(relayInput().ParentRunID).toBeUndefined();
|
|
249
|
+
expect(relayInput().ResumeRunID).toBeUndefined();
|
|
250
|
+
expect(issuedFilters(runView).some((f) => f.includes(`AgentSessionID='${SESSION_ID}'`))).toBe(true);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('passes run ids that are records of this session', async () => {
|
|
254
|
+
makeProvider(
|
|
255
|
+
makeSession({ targetAgentID: TARGET_ID, coAgentRunID: CO_RUN_ID, pendingFeedbackRunID: PAUSED_RUN_ID }),
|
|
256
|
+
sessionRuns(CO_RUN_ID, PAUSED_RUN_ID),
|
|
257
|
+
);
|
|
258
|
+
|
|
259
|
+
await relay(makeResolver());
|
|
260
|
+
|
|
261
|
+
expect(relayInput().ParentRunID).toBe(CO_RUN_ID);
|
|
262
|
+
expect(relayInput().ResumeRunID).toBe(PAUSED_RUN_ID);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it('stops passing run ids that stopped being records of this session since the last relay', async () => {
|
|
266
|
+
const rows = sessionRuns(CO_RUN_ID, PAUSED_RUN_ID);
|
|
267
|
+
makeProvider(makeSession({ targetAgentID: TARGET_ID, coAgentRunID: CO_RUN_ID, pendingFeedbackRunID: PAUSED_RUN_ID }), rows);
|
|
268
|
+
// The delegated run pauses again, so the session config still names it on the next relay.
|
|
269
|
+
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true, PausedRunID: PAUSED_RUN_ID });
|
|
270
|
+
const resolver = makeResolver();
|
|
271
|
+
|
|
272
|
+
await relay(resolver);
|
|
273
|
+
// Both runs are moved to another session between the relays.
|
|
274
|
+
rows['MJ: AI Agent Runs'] = [];
|
|
275
|
+
await relay(resolver);
|
|
276
|
+
|
|
277
|
+
const [first, second] = executeRelayedToolMock.mock.calls.map(([input]) => input as ExecuteRelayedToolInput);
|
|
278
|
+
expect(first.ParentRunID).toBe(CO_RUN_ID);
|
|
279
|
+
expect(first.ResumeRunID).toBe(PAUSED_RUN_ID);
|
|
280
|
+
expect(second.ParentRunID).toBeUndefined();
|
|
281
|
+
expect(second.ResumeRunID).toBeUndefined();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe('prompt-run mirrors — promptRunID in Config', () => {
|
|
286
|
+
const PROMPT_STEP_ROWS: FakeRows = {
|
|
287
|
+
...sessionRuns(CO_RUN_ID),
|
|
288
|
+
'MJ: AI Agent Run Steps': [{ ID: STEP_ID, StepType: 'Prompt', TargetLogID: PROMPT_RUN_ID }],
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
it('RelayRealtimeUsage does not write to a prompt run that is not this session\'s', async () => {
|
|
292
|
+
makeProvider(makeSession({ targetAgentID: TARGET_ID, coAgentRunID: CO_RUN_ID, promptRunID: FOREIGN_RUN_ID }), sessionRuns(CO_RUN_ID));
|
|
293
|
+
|
|
294
|
+
const out = await makeResolver().RelayRealtimeUsage(SESSION_ID, 10, 5, makeCtx());
|
|
295
|
+
|
|
296
|
+
expect(out).toBe(false);
|
|
297
|
+
expect(accumulatePromptRunUsageMock).not.toHaveBeenCalled();
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('RelayRealtimeTranscript does not append to a prompt run that is not this session\'s', async () => {
|
|
301
|
+
makeProvider(makeSession({ targetAgentID: TARGET_ID, coAgentRunID: CO_RUN_ID, promptRunID: FOREIGN_RUN_ID }), sessionRuns(CO_RUN_ID));
|
|
302
|
+
|
|
303
|
+
const out = await makeResolver().RelayRealtimeTranscript(SESSION_ID, 'User', 'hello', makeCtx());
|
|
304
|
+
|
|
305
|
+
expect(out).toBe(true);
|
|
306
|
+
expect(appendPromptRunMessageMock).not.toHaveBeenCalled();
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
it('RelayRealtimeUsage writes to the prompt run of this session\'s co-agent run', async () => {
|
|
310
|
+
makeProvider(makeSession({ targetAgentID: TARGET_ID, coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }), PROMPT_STEP_ROWS);
|
|
311
|
+
|
|
312
|
+
const out = await makeResolver().RelayRealtimeUsage(SESSION_ID, 10, 5, makeCtx());
|
|
313
|
+
|
|
314
|
+
expect(out).toBe(true);
|
|
315
|
+
expect(accumulatePromptRunUsageMock).toHaveBeenCalledTimes(1);
|
|
316
|
+
expect(accumulatePromptRunUsageMock.mock.calls[0][0]).toBe(PROMPT_RUN_ID);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
@@ -227,13 +227,44 @@ function makeSessionEntity(overrides: Partial<FakeSession> = {}): FakeSession {
|
|
|
227
227
|
};
|
|
228
228
|
}
|
|
229
229
|
|
|
230
|
-
/**
|
|
231
|
-
|
|
232
|
-
|
|
230
|
+
/** Co-agent observability and paused-run ids, as a voice session's Config holds them. */
|
|
231
|
+
const CO_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000001';
|
|
232
|
+
const PROMPT_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000002';
|
|
233
|
+
const PAUSED_RUN_ID = 'c0c0c0c0-0000-4000-8000-000000000003';
|
|
234
|
+
|
|
235
|
+
/** `RunView`/`RunViews` stubs for a provider. */
|
|
236
|
+
interface RunLookups {
|
|
237
|
+
RunView: ReturnType<typeof vi.fn>;
|
|
238
|
+
RunViews: ReturnType<typeof vi.fn>;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Answers the session run-id lookups: each id in `runIDs` is a run of the session, and
|
|
243
|
+
* `promptRunID` (when given) is the TargetLogID of the co-agent run's Prompt step.
|
|
244
|
+
*/
|
|
245
|
+
function sessionRunLookups(runIDs: string[], promptRunID?: string): RunLookups {
|
|
246
|
+
const runView = vi.fn(async (params: { EntityName?: string }) => {
|
|
247
|
+
if (params.EntityName === 'MJ: AI Agent Runs') {
|
|
248
|
+
return { Success: true, Results: runIDs.map((ID) => ({ ID })) };
|
|
249
|
+
}
|
|
250
|
+
if (params.EntityName === 'MJ: AI Agent Run Steps' && promptRunID) {
|
|
251
|
+
return { Success: true, Results: [{ ID: 'c0c0c0c0-0000-4000-8000-0000000000a1', StepType: 'Prompt', TargetLogID: promptRunID }] };
|
|
252
|
+
}
|
|
253
|
+
return { Success: true, Results: [] };
|
|
254
|
+
});
|
|
255
|
+
const runViews = vi.fn(async (paramsList: Array<{ EntityName?: string }>) => Promise.all(paramsList.map((params) => runView(params))));
|
|
256
|
+
return { RunView: runView, RunViews: runViews };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/** Provider whose GetEntityObject returns a fixed entity per entity name, plus optional run-id lookups. */
|
|
260
|
+
function makeProvider(factory: (entityName: string) => FakeSession, lookups?: RunLookups): unknown {
|
|
261
|
+
return { GetEntityObject: vi.fn(async (name: string) => factory(name)), ...lookups };
|
|
233
262
|
}
|
|
234
263
|
|
|
235
264
|
beforeEach(() => {
|
|
236
265
|
hasPermissionMock.mockReset();
|
|
266
|
+
// Callers may run the agents in play unless a test says otherwise; relays re-check CanRun on the target.
|
|
267
|
+
hasPermissionMock.mockResolvedValue(true);
|
|
237
268
|
engineCoAgentsMock.mockReset();
|
|
238
269
|
engineCoAgentsMock.mockReturnValue([]);
|
|
239
270
|
engineChannelsMock.mockReset();
|
|
@@ -670,10 +701,12 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
670
701
|
});
|
|
671
702
|
|
|
672
703
|
it('reads the target + co-agent run id from the session config and returns ResultJson', async () => {
|
|
673
|
-
currentProvider = makeProvider(
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
704
|
+
currentProvider = makeProvider(
|
|
705
|
+
() =>
|
|
706
|
+
makeSessionEntity({
|
|
707
|
+
Config_: JSON.stringify({ targetAgentID: 'target-from-session', coAgentRunID: CO_RUN_ID }),
|
|
708
|
+
}),
|
|
709
|
+
sessionRunLookups([CO_RUN_ID]),
|
|
677
710
|
);
|
|
678
711
|
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"ok":true}', Success: true });
|
|
679
712
|
const resolver = makeResolver();
|
|
@@ -696,7 +729,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
696
729
|
// Target comes from the session, NOT the client.
|
|
697
730
|
expect(relayArg.TargetAgentID).toBe('target-from-session');
|
|
698
731
|
// Delegated run nests under the co-agent observability run from the session config.
|
|
699
|
-
expect(relayArg.ParentRunID).toBe(
|
|
732
|
+
expect(relayArg.ParentRunID).toBe(CO_RUN_ID);
|
|
700
733
|
expect(relayArg.Call).toEqual({ CallID: 'call-1', ToolName: 'invoke-target-agent', Arguments: '{"request":"do it"}' });
|
|
701
734
|
expect(heartbeatMock).toHaveBeenCalledWith('session-1', USER, currentProvider);
|
|
702
735
|
});
|
|
@@ -760,9 +793,9 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
760
793
|
|
|
761
794
|
it('consumes pendingFeedbackRunID as ResumeRunID and clears it on the next call', async () => {
|
|
762
795
|
const session = makeSessionEntity({
|
|
763
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID:
|
|
796
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
|
|
764
797
|
});
|
|
765
|
-
currentProvider = makeProvider(() => session);
|
|
798
|
+
currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
|
|
766
799
|
// Resumed run completes (no new pause).
|
|
767
800
|
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"done":1}', Success: true });
|
|
768
801
|
const resolver = makeResolver();
|
|
@@ -771,7 +804,7 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
771
804
|
|
|
772
805
|
// The paused id was passed in to resume the run...
|
|
773
806
|
const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
|
|
774
|
-
expect(relayArg.ResumeRunID).toBe(
|
|
807
|
+
expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
|
|
775
808
|
// ...and cleared from the session config afterward.
|
|
776
809
|
expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBeUndefined();
|
|
777
810
|
expect(session.Save).toHaveBeenCalled();
|
|
@@ -779,16 +812,16 @@ describe('RealtimeClientSessionResolver.ExecuteRealtimeSessionTool', () => {
|
|
|
779
812
|
|
|
780
813
|
it('re-stores a new pendingFeedbackRunID when a resumed run pauses again', async () => {
|
|
781
814
|
const session = makeSessionEntity({
|
|
782
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID:
|
|
815
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', pendingFeedbackRunID: PAUSED_RUN_ID }),
|
|
783
816
|
});
|
|
784
|
-
currentProvider = makeProvider(() => session);
|
|
817
|
+
currentProvider = makeProvider(() => session, sessionRunLookups([PAUSED_RUN_ID]));
|
|
785
818
|
executeRelayedToolMock.mockResolvedValue({ ResultJson: '{"q":2}', Success: true, PausedRunID: 'paused-2' });
|
|
786
819
|
const resolver = makeResolver();
|
|
787
820
|
|
|
788
821
|
await resolver.ExecuteRealtimeSessionTool('session-1', 'call-3', 'invoke-target-agent', '{"request":"yes"}', makeCtx(), makePubSub());
|
|
789
822
|
|
|
790
823
|
const relayArg = executeRelayedToolMock.mock.calls[0][0] as { ResumeRunID?: string };
|
|
791
|
-
expect(relayArg.ResumeRunID).toBe(
|
|
824
|
+
expect(relayArg.ResumeRunID).toBe(PAUSED_RUN_ID);
|
|
792
825
|
expect(JSON.parse(session.Config_ as string).pendingFeedbackRunID).toBe('paused-2');
|
|
793
826
|
});
|
|
794
827
|
|
|
@@ -2192,10 +2225,11 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
|
|
|
2192
2225
|
}): { provider: unknown; promptRun: FakeSession } {
|
|
2193
2226
|
const session =
|
|
2194
2227
|
opts.session ??
|
|
2195
|
-
makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
2196
|
-
const promptRun = opts.promptRun ?? makeSessionEntity({ ID:
|
|
2228
|
+
makeSessionEntity({ Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }) });
|
|
2229
|
+
const promptRun = opts.promptRun ?? makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
|
|
2197
2230
|
const provider = {
|
|
2198
2231
|
GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
|
|
2232
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
2199
2233
|
};
|
|
2200
2234
|
return { provider, promptRun };
|
|
2201
2235
|
}
|
|
@@ -2221,7 +2255,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
|
|
|
2221
2255
|
const ok = await resolver.RelayRealtimeUsage('session-1', 30, 5, makeCtx());
|
|
2222
2256
|
|
|
2223
2257
|
expect(ok).toBe(true);
|
|
2224
|
-
expect(promptRun.Load).toHaveBeenCalledWith(
|
|
2258
|
+
expect(promptRun.Load).toHaveBeenCalledWith(PROMPT_RUN_ID);
|
|
2225
2259
|
expect(promptRun.TokensPrompt).toBe(130);
|
|
2226
2260
|
expect(promptRun.TokensCompletion).toBe(45);
|
|
2227
2261
|
expect(promptRun.TokensUsed).toBe(175);
|
|
@@ -2318,7 +2352,7 @@ describe('RealtimeClientSessionResolver.RelayRealtimeUsage', () => {
|
|
|
2318
2352
|
const { provider, promptRun } = makeUsageProvider({
|
|
2319
2353
|
session: makeSessionEntity({
|
|
2320
2354
|
Status: 'Closed',
|
|
2321
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
2355
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
2322
2356
|
}),
|
|
2323
2357
|
});
|
|
2324
2358
|
currentProvider = provider;
|
|
@@ -2808,11 +2842,12 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2808
2842
|
it('accumulates relayed usage onto the prompt run under the SYSTEM user', async () => {
|
|
2809
2843
|
const session = makeSessionEntity({
|
|
2810
2844
|
UserID: 'anon-1',
|
|
2811
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
2845
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
2812
2846
|
});
|
|
2813
|
-
const promptRun = makeSessionEntity({ ID:
|
|
2847
|
+
const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, TokensPrompt: null, TokensCompletion: null, TokensUsed: null });
|
|
2814
2848
|
currentProvider = {
|
|
2815
2849
|
GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
|
|
2850
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
2816
2851
|
};
|
|
2817
2852
|
const resolver = makeAnonResolver();
|
|
2818
2853
|
|
|
@@ -2827,9 +2862,9 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2827
2862
|
it('mirrors the transcript turn onto the prompt run as SYSTEM while the Conversation Detail stays the caller', async () => {
|
|
2828
2863
|
const session = makeSessionEntity({
|
|
2829
2864
|
UserID: 'anon-1',
|
|
2830
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
2865
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
2831
2866
|
});
|
|
2832
|
-
const promptRun = makeSessionEntity({ ID:
|
|
2867
|
+
const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
|
|
2833
2868
|
const detail = makeSessionEntity({ ID: 'detail-1' });
|
|
2834
2869
|
currentProvider = {
|
|
2835
2870
|
GetEntityObject: vi.fn(async (name: string) => {
|
|
@@ -2837,6 +2872,7 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2837
2872
|
if (name === 'MJ: Conversation Details') return detail;
|
|
2838
2873
|
return session;
|
|
2839
2874
|
}),
|
|
2875
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
2840
2876
|
};
|
|
2841
2877
|
const resolver = makeAnonResolver();
|
|
2842
2878
|
|
|
@@ -2853,11 +2889,12 @@ describe('RealtimeClientSessionResolver — scoped-anonymous elevation (issue #3
|
|
|
2853
2889
|
it('records the co-agent tool turn (RelayRealtimeToolTurn) under the SYSTEM user', async () => {
|
|
2854
2890
|
const session = makeSessionEntity({
|
|
2855
2891
|
UserID: 'anon-1',
|
|
2856
|
-
Config_: JSON.stringify({ targetAgentID: 'target-1', promptRunID:
|
|
2892
|
+
Config_: JSON.stringify({ targetAgentID: 'target-1', coAgentRunID: CO_RUN_ID, promptRunID: PROMPT_RUN_ID }),
|
|
2857
2893
|
});
|
|
2858
|
-
const promptRun = makeSessionEntity({ ID:
|
|
2894
|
+
const promptRun = makeSessionEntity({ ID: PROMPT_RUN_ID, Messages: null });
|
|
2859
2895
|
currentProvider = {
|
|
2860
2896
|
GetEntityObject: vi.fn(async (name: string) => (name === 'MJ: AI Prompt Runs' ? promptRun : session)),
|
|
2897
|
+
...sessionRunLookups([CO_RUN_ID], PROMPT_RUN_ID),
|
|
2861
2898
|
};
|
|
2862
2899
|
const resolver = makeAnonResolver();
|
|
2863
2900
|
|
|
@@ -3070,7 +3107,8 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
|
|
|
3070
3107
|
|
|
3071
3108
|
it('excludes every colleague when the caller can run none of them', async () => {
|
|
3072
3109
|
currentProvider = makeProvider(() => makeMultiTargetSession('user-1'));
|
|
3073
|
-
|
|
3110
|
+
// The caller can run the lead target, and none of the colleagues.
|
|
3111
|
+
hasPermissionMock.mockImplementation((...args: unknown[]) => Promise.resolve(args[0] === 'target-1'));
|
|
3074
3112
|
|
|
3075
3113
|
await makeResolver().ExecuteRealtimeSessionTool(
|
|
3076
3114
|
'session-1', 'call-1', 'invoke-target-agent', '{}', makeCtx(), makePubSub(),
|
|
@@ -3079,7 +3117,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
|
|
|
3079
3117
|
expect(dispatchedAgentIDs()).toEqual([]);
|
|
3080
3118
|
});
|
|
3081
3119
|
|
|
3082
|
-
it('passes a single-target session through with
|
|
3120
|
+
it('passes a single-target session through with only the target permission check', async () => {
|
|
3083
3121
|
currentProvider = makeProvider(() => makeSessionEntity());
|
|
3084
3122
|
hasPermissionMock.mockResolvedValue(true);
|
|
3085
3123
|
|
|
@@ -3089,6 +3127,7 @@ describe('RealtimeClientSessionResolver — colleague authorization (allowedAgen
|
|
|
3089
3127
|
|
|
3090
3128
|
const input = executeRelayedToolMock.mock.calls[0][0] as { AllowedAgents?: unknown };
|
|
3091
3129
|
expect(input.AllowedAgents).toBeUndefined();
|
|
3092
|
-
expect(hasPermissionMock).
|
|
3130
|
+
expect(hasPermissionMock).toHaveBeenCalledTimes(1);
|
|
3131
|
+
expect((hasPermissionMock.mock.calls[0] as unknown[])[0]).toBe('target-1');
|
|
3093
3132
|
});
|
|
3094
3133
|
});
|