@memberjunction/server 6.2.0-edge.4 → 6.2.0-edge.5
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 +20 -0
- package/dist/.tsbuildinfo +1 -1
- package/dist/agentSessions/SessionJanitor.d.ts +91 -30
- package/dist/agentSessions/SessionJanitor.d.ts.map +1 -1
- package/dist/agentSessions/SessionJanitor.js +311 -81
- package/dist/agentSessions/SessionJanitor.js.map +1 -1
- package/dist/agentSessions/SessionManager.d.ts +5 -1
- package/dist/agentSessions/SessionManager.d.ts.map +1 -1
- package/dist/agentSessions/SessionManager.js +13 -1
- package/dist/agentSessions/SessionManager.js.map +1 -1
- package/dist/apiRoot.d.ts +32 -0
- package/dist/apiRoot.d.ts.map +1 -0
- package/dist/apiRoot.js +67 -0
- package/dist/apiRoot.js.map +1 -0
- package/dist/auth/AuthProviderCatalogRouter.d.ts.map +1 -1
- package/dist/auth/AuthProviderCatalogRouter.js +25 -0
- package/dist/auth/AuthProviderCatalogRouter.js.map +1 -1
- package/dist/auth/LoginScreenEngine.d.ts +22 -0
- package/dist/auth/LoginScreenEngine.d.ts.map +1 -0
- package/dist/auth/LoginScreenEngine.js +57 -0
- package/dist/auth/LoginScreenEngine.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/magicLink/MagicLinkRouter.d.ts.map +1 -1
- package/dist/auth/magicLink/MagicLinkRouter.js +3 -2
- package/dist/auth/magicLink/MagicLinkRouter.js.map +1 -1
- package/dist/config.d.ts +136 -22
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +43 -4
- package/dist/config.js.map +1 -1
- package/dist/coreRoutes.d.ts +29 -0
- package/dist/coreRoutes.d.ts.map +1 -0
- package/dist/coreRoutes.js +142 -0
- package/dist/coreRoutes.js.map +1 -0
- package/dist/generated/graphql-schemas/__mj.d.ts +369 -3
- package/dist/generated/graphql-schemas/__mj.d.ts.map +1 -1
- package/dist/generated/graphql-schemas/__mj.js +1750 -7
- package/dist/generated/graphql-schemas/__mj.js.map +1 -1
- package/dist/generic/CacheInvalidationResolver.d.ts +43 -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/PushStatusResolver.js +1 -1
- package/dist/generic/PushStatusResolver.js.map +1 -1
- package/dist/generic/ResolverBase.d.ts +103 -44
- package/dist/generic/ResolverBase.d.ts.map +1 -1
- package/dist/generic/ResolverBase.js +264 -305
- 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/globalCors.d.ts +33 -0
- package/dist/globalCors.d.ts.map +1 -0
- package/dist/globalCors.js +58 -0
- package/dist/globalCors.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +112 -160
- 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/realtimeSessions/RealtimeSessionVerificationService.d.ts.map +1 -1
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js +2 -1
- package/dist/realtimeSessions/RealtimeSessionVerificationService.js.map +1 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.d.ts.map +1 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.js +2 -1
- package/dist/realtimeSessions/RealtimeVerifyRouter.js.map +1 -1
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts +100 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.d.ts.map +1 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js +173 -0
- package/dist/realtimeSessions/RecordingAssemblyScheduler.js.map +1 -0
- package/dist/realtimeSessions/index.d.ts +2 -0
- package/dist/realtimeSessions/index.d.ts.map +1 -1
- package/dist/realtimeSessions/index.js +2 -0
- package/dist/realtimeSessions/index.js.map +1 -1
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts +30 -0
- package/dist/realtimeSessions/ownedRealtimeSession.d.ts.map +1 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js +47 -0
- package/dist/realtimeSessions/ownedRealtimeSession.js.map +1 -0
- package/dist/realtimeSessions/sessionAccess.d.ts +6 -0
- package/dist/realtimeSessions/sessionAccess.d.ts.map +1 -1
- package/dist/realtimeSessions/sessionAccess.js +7 -3
- package/dist/realtimeSessions/sessionAccess.js.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts +5 -1
- package/dist/resolvers/ArtifactFileResolver.d.ts.map +1 -1
- package/dist/resolvers/ArtifactFileResolver.js +9 -7
- package/dist/resolvers/ArtifactFileResolver.js.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.d.ts +8 -4
- package/dist/resolvers/ConversationRunEventsResolver.d.ts.map +1 -1
- package/dist/resolvers/ConversationRunEventsResolver.js +6 -6
- package/dist/resolvers/ConversationRunEventsResolver.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 +2 -1
- package/dist/resolvers/EntityRecordNameResolver.d.ts.map +1 -1
- package/dist/resolvers/EntityRecordNameResolver.js +3 -2
- package/dist/resolvers/EntityRecordNameResolver.js.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts +18 -3
- package/dist/resolvers/FetchEntityVectorsResolver.d.ts.map +1 -1
- package/dist/resolvers/FetchEntityVectorsResolver.js +100 -35
- package/dist/resolvers/FetchEntityVectorsResolver.js.map +1 -1
- package/dist/resolvers/FileResolver.d.ts +82 -11
- package/dist/resolvers/FileResolver.d.ts.map +1 -1
- package/dist/resolvers/FileResolver.js +279 -101
- package/dist/resolvers/FileResolver.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 +6 -3
- package/dist/resolvers/GetDataResolver.js.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.d.ts.map +1 -1
- package/dist/resolvers/IntegrationDiscoveryResolver.js +4 -2
- package/dist/resolvers/IntegrationDiscoveryResolver.js.map +1 -1
- package/dist/resolvers/QueryResolver.d.ts +2 -0
- package/dist/resolvers/QueryResolver.d.ts.map +1 -1
- package/dist/resolvers/QueryResolver.js +13 -0
- package/dist/resolvers/QueryResolver.js.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts +2 -8
- package/dist/resolvers/RealtimeClientSessionResolver.d.ts.map +1 -1
- package/dist/resolvers/RealtimeClientSessionResolver.js +10 -26
- package/dist/resolvers/RealtimeClientSessionResolver.js.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.d.ts.map +1 -1
- package/dist/resolvers/RunAIAgentResolver.js +35 -6
- package/dist/resolvers/RunAIAgentResolver.js.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts +12 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.d.ts.map +1 -1
- package/dist/resolvers/SearchKnowledgeStreamResolver.js +15 -2
- package/dist/resolvers/SearchKnowledgeStreamResolver.js.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.d.ts.map +1 -1
- package/dist/resolvers/SqlLoggingConfigResolver.js +2 -3
- package/dist/resolvers/SqlLoggingConfigResolver.js.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts +2 -1
- package/dist/resolvers/TaskGraphFrameResolver.d.ts.map +1 -1
- package/dist/resolvers/TaskGraphFrameResolver.js +28 -1
- package/dist/resolvers/TaskGraphFrameResolver.js.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.d.ts +24 -0
- package/dist/resolvers/TestQuerySQLResolver.d.ts.map +1 -1
- package/dist/resolvers/TestQuerySQLResolver.js +65 -13
- package/dist/resolvers/TestQuerySQLResolver.js.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.d.ts +19 -2
- package/dist/resolvers/TransactionGroupResolver.d.ts.map +1 -1
- package/dist/resolvers/TransactionGroupResolver.js +102 -24
- package/dist/resolvers/TransactionGroupResolver.js.map +1 -1
- package/dist/resolvers/agent-run-data-guard.d.ts +31 -0
- package/dist/resolvers/agent-run-data-guard.d.ts.map +1 -0
- package/dist/resolvers/agent-run-data-guard.js +72 -0
- package/dist/resolvers/agent-run-data-guard.js.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts +49 -0
- package/dist/rest/RealtimeRecordingHandler.d.ts.map +1 -0
- package/dist/rest/RealtimeRecordingHandler.js +383 -0
- package/dist/rest/RealtimeRecordingHandler.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 +21 -12
- package/dist/rest/ViewOperationsHandler.js.map +1 -1
- package/dist/rest/setupRESTEndpoints.d.ts +6 -0
- package/dist/rest/setupRESTEndpoints.d.ts.map +1 -1
- package/dist/rest/setupRESTEndpoints.js +6 -0
- package/dist/rest/setupRESTEndpoints.js.map +1 -1
- package/dist/serverExtensionReservedRoots.d.ts +12 -10
- package/dist/serverExtensionReservedRoots.d.ts.map +1 -1
- package/dist/serverExtensionReservedRoots.js +14 -17
- package/dist/serverExtensionReservedRoots.js.map +1 -1
- package/dist/services/MJStorageBlobStore.d.ts +8 -1
- package/dist/services/MJStorageBlobStore.d.ts.map +1 -1
- package/dist/services/MJStorageBlobStore.js +25 -19
- package/dist/services/MJStorageBlobStore.js.map +1 -1
- package/dist/services/StartTaskGraphDispatcher.js +2 -2
- package/dist/services/StartTaskGraphDispatcher.js.map +1 -1
- package/dist/services/TaskGraphActionRunner.d.ts +5 -3
- package/dist/services/TaskGraphActionRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphActionRunner.js +27 -5
- package/dist/services/TaskGraphActionRunner.js.map +1 -1
- package/dist/services/TaskGraphAgentRunner.d.ts +14 -2
- package/dist/services/TaskGraphAgentRunner.d.ts.map +1 -1
- package/dist/services/TaskGraphAgentRunner.js +157 -9
- package/dist/services/TaskGraphAgentRunner.js.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.d.ts +54 -2
- package/dist/services/TaskGraphContinuationDeliverer.d.ts.map +1 -1
- package/dist/services/TaskGraphContinuationDeliverer.js +598 -15
- package/dist/services/TaskGraphContinuationDeliverer.js.map +1 -1
- package/dist/sharedCache.d.ts.map +1 -1
- package/dist/sharedCache.js +3 -0
- package/dist/sharedCache.js.map +1 -1
- package/package.json +99 -98
- package/src/__tests__/DatasetResolver.itemFilters.test.ts +201 -0
- package/src/__tests__/EntityRecordNameResolver.rowSecurity.test.ts +152 -0
- package/src/__tests__/FetchEntityVectorsResolver.test.ts +163 -15
- package/src/__tests__/FileResolver.storageAccess.test.ts +562 -0
- package/src/__tests__/GetDataContextDataResolver.sqlItems.test.ts +416 -0
- package/src/__tests__/ListOperationsResolver.adhocScreen.test.ts +137 -0
- package/src/__tests__/LoginScreenEngine.test.ts +86 -0
- package/src/__tests__/MJStorageBlobStore.storageGate.test.ts +63 -0
- package/src/__tests__/RecordingAssemblyScheduler.test.ts +178 -0
- package/src/__tests__/ResolverBase.adhocReadScope.test.ts +133 -2
- package/src/__tests__/ResolverBase.balancedClause.test.ts +155 -0
- package/src/__tests__/ResolverBase.clauseUnscopedRead.test.ts +127 -0
- package/src/__tests__/ResolverBase.clientOldValues.test.ts +209 -6
- package/src/__tests__/ResolverBase.filterEscaping.test.ts +5 -2
- package/src/__tests__/RunAIAgentResolver.branch.test.ts +1 -0
- package/src/__tests__/RunAIAgentResolver.historyFrom.test.ts +5 -3
- package/src/__tests__/RunAIAgentResolver.runDataGuard.test.ts +258 -0
- package/src/__tests__/RunViewResolver.cacheCheckScreen.test.ts +165 -0
- package/src/__tests__/SearchKnowledgeStreamResolver.test.ts +161 -0
- package/src/__tests__/SessionJanitor.test.ts +365 -10
- package/src/__tests__/SessionManager.test.ts +27 -0
- package/src/__tests__/TaskGraphActionRunner.durablePayload.test.ts +25 -0
- package/src/__tests__/TaskGraphAgentRunner.test.ts +266 -0
- package/src/__tests__/TaskGraphContinuationDeliverer.test.ts +356 -22
- package/src/__tests__/TaskGraphFrameResolver.test.ts +22 -0
- package/src/__tests__/TestQuerySQLResolver.test.ts +274 -0
- package/src/__tests__/TransactionGroupResolver.refusals.test.ts +98 -1
- package/src/__tests__/UpdateMJDataContextItem.oldValues.test.ts +220 -0
- package/src/__tests__/ViewOperationsHandler.clauseScreen.test.ts +157 -0
- package/src/__tests__/agent-run-data-guard.test.ts +130 -0
- package/src/__tests__/apiRootBoot.test.ts +202 -0
- package/src/__tests__/cacheInvalidationRecordData.test.ts +64 -0
- package/src/__tests__/conversationRunEvents.test.ts +56 -14
- package/src/__tests__/globalCors.test.ts +217 -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__/ownedRealtimeSession.test.ts +39 -0
- package/src/__tests__/pushStatusResolver.filter.test.ts +7 -0
- package/src/__tests__/realtimeRecordingHandler.test.ts +652 -0
- package/src/__tests__/serverExtensionHostOverlay.test.ts +66 -0
- package/src/__tests__/serverExtensionReservedRoots.test.ts +11 -98
- package/src/__tests__/sharedCache.test.ts +5 -3
- package/src/agentSessions/SessionJanitor.ts +369 -93
- package/src/agentSessions/SessionManager.ts +13 -1
- package/src/apiRoot.ts +78 -0
- package/src/auth/AuthProviderCatalogRouter.ts +26 -0
- package/src/auth/LoginScreenEngine.ts +50 -0
- package/src/auth/index.ts +1 -0
- package/src/auth/magicLink/MagicLinkRouter.ts +3 -2
- package/src/config.ts +46 -5
- package/src/coreRoutes.ts +166 -0
- package/src/generated/graphql-schemas/__mj.ts +1273 -10
- package/src/generic/CacheInvalidationResolver.ts +73 -18
- package/src/generic/PushStatusResolver.ts +1 -1
- package/src/generic/ResolverBase.ts +304 -325
- package/src/generic/RunViewResolver.ts +17 -0
- package/src/globalCors.ts +71 -0
- package/src/index.ts +116 -174
- package/src/multiTenancy/index.ts +24 -2
- package/src/realtimeSessions/RealtimeSessionVerificationService.ts +2 -1
- package/src/realtimeSessions/RealtimeVerifyRouter.ts +2 -1
- package/src/realtimeSessions/RecordingAssemblyScheduler.ts +234 -0
- package/src/realtimeSessions/index.ts +2 -0
- package/src/realtimeSessions/ownedRealtimeSession.ts +55 -0
- package/src/realtimeSessions/sessionAccess.ts +7 -3
- package/src/resolvers/ArtifactFileResolver.ts +9 -8
- package/src/resolvers/ConversationRunEventsResolver.ts +15 -11
- package/src/resolvers/DatasetResolver.ts +70 -10
- package/src/resolvers/EntityRecordNameResolver.ts +3 -2
- package/src/resolvers/FetchEntityVectorsResolver.ts +121 -39
- package/src/resolvers/FileResolver.ts +321 -106
- package/src/resolvers/GetDataContextDataResolver.ts +139 -41
- package/src/resolvers/GetDataResolver.ts +6 -3
- package/src/resolvers/IntegrationDiscoveryResolver.ts +4 -2
- package/src/resolvers/QueryResolver.ts +11 -0
- package/src/resolvers/RealtimeClientSessionResolver.ts +10 -33
- package/src/resolvers/RunAIAgentResolver.ts +39 -4
- package/src/resolvers/SearchKnowledgeStreamResolver.ts +19 -3
- package/src/resolvers/SqlLoggingConfigResolver.ts +2 -4
- package/src/resolvers/TaskGraphFrameResolver.ts +33 -1
- package/src/resolvers/TestQuerySQLResolver.ts +68 -13
- package/src/resolvers/TransactionGroupResolver.ts +131 -27
- package/src/resolvers/__tests__/GetDataResolver.validation.test.ts +95 -0
- package/src/resolvers/agent-run-data-guard.ts +89 -0
- package/src/rest/RealtimeRecordingHandler.ts +464 -0
- package/src/rest/ViewOperationsHandler.ts +29 -13
- package/src/rest/setupRESTEndpoints.ts +6 -0
- package/src/serverExtensionReservedRoots.ts +14 -17
- package/src/services/MJStorageBlobStore.ts +31 -30
- package/src/services/StartTaskGraphDispatcher.ts +2 -2
- package/src/services/TaskGraphActionRunner.ts +25 -5
- package/src/services/TaskGraphAgentRunner.ts +188 -11
- package/src/services/TaskGraphContinuationDeliverer.ts +704 -14
- package/src/sharedCache.ts +3 -0
|
@@ -6,12 +6,20 @@ vi.mock('@memberjunction/ai-engine-base', () => ({
|
|
|
6
6
|
}));
|
|
7
7
|
|
|
8
8
|
// Capture the ExtraFilter passed to each sweep page and return controllable rows.
|
|
9
|
-
const { runViewMock, logErrorMock, logStatusMock, recoverMock, resolveAccountMock } = vi.hoisted(() => ({
|
|
9
|
+
const { runViewMock, logErrorMock, logStatusMock, recoverMock, resolveAccountMock, assembleMock, ledgerMock } = vi.hoisted(() => ({
|
|
10
10
|
runViewMock: vi.fn(),
|
|
11
11
|
logErrorMock: vi.fn(),
|
|
12
12
|
logStatusMock: vi.fn(),
|
|
13
13
|
recoverMock: vi.fn(),
|
|
14
14
|
resolveAccountMock: vi.fn(),
|
|
15
|
+
assembleMock: vi.fn(),
|
|
16
|
+
/** The one fake shard ledger every `new SqlRealtimeRecordingLedger(...)` returns. */
|
|
17
|
+
ledgerMock: {
|
|
18
|
+
ClaimAssembly: vi.fn(),
|
|
19
|
+
PurgeClosedBefore: vi.fn(),
|
|
20
|
+
LoadShardMetadata: vi.fn(),
|
|
21
|
+
StampTerminal: vi.fn(),
|
|
22
|
+
},
|
|
15
23
|
}));
|
|
16
24
|
vi.mock('@memberjunction/core', async (importOriginal) => {
|
|
17
25
|
const actual = await importOriginal<typeof import('@memberjunction/core')>();
|
|
@@ -34,6 +42,8 @@ vi.mock('@memberjunction/ai-agents', async (importOriginal) => {
|
|
|
34
42
|
...actual,
|
|
35
43
|
RecoverRealtimeRecordingFromSegments: recoverMock,
|
|
36
44
|
ResolveRecordingStorageAccountID: resolveAccountMock,
|
|
45
|
+
AssembleRealtimeRecordingFromLedger: assembleMock,
|
|
46
|
+
SqlRealtimeRecordingLedger: vi.fn(function FakeLedger() { return ledgerMock; }),
|
|
37
47
|
};
|
|
38
48
|
});
|
|
39
49
|
|
|
@@ -44,7 +54,7 @@ import {
|
|
|
44
54
|
HostInstanceIdentity,
|
|
45
55
|
SetHostInstancePort,
|
|
46
56
|
} from '../agentSessions/HostInstance.js';
|
|
47
|
-
import type { UserInfo, IMetadataProvider } from '@memberjunction/core';
|
|
57
|
+
import type { UserInfo, IMetadataProvider, DatabaseProviderBase } from '@memberjunction/core';
|
|
48
58
|
import type { MJAIAgentSessionEntity } from '@memberjunction/core-entities';
|
|
49
59
|
|
|
50
60
|
/** The port this test process "serves" on, set the way `Serve` sets it, before anything reads the identity. */
|
|
@@ -53,6 +63,16 @@ const OUR_PORT = 4000;
|
|
|
53
63
|
const OTHER_PORT = 4100;
|
|
54
64
|
SetHostInstancePort(OUR_PORT);
|
|
55
65
|
|
|
66
|
+
/**
|
|
67
|
+
* The janitor builds the shard-ledger view name from the provider's core schema and dialect quoting;
|
|
68
|
+
* fakes carry SQL Server's so filter assertions read naturally.
|
|
69
|
+
*/
|
|
70
|
+
const LEDGER_SCHEMA_PROVIDER_FIELDS = {
|
|
71
|
+
MJCoreSchemaName: '__mj',
|
|
72
|
+
QuoteSchemaAndView: (schema: string, view: string) => `[${schema}].[${view}]`,
|
|
73
|
+
};
|
|
74
|
+
const LEDGER_VIEW = '[__mj].[vwAIAgentSessionRecordingShards]';
|
|
75
|
+
|
|
56
76
|
interface FakeSession {
|
|
57
77
|
ID: string;
|
|
58
78
|
Status: 'Active' | 'Idle' | 'Closed';
|
|
@@ -73,7 +93,7 @@ function makeUser(): UserInfo {
|
|
|
73
93
|
* inside CloseSession uses runViewMock, which we default to an empty channel list.
|
|
74
94
|
*/
|
|
75
95
|
function makeProvider(store: Map<string, 'Active' | 'Idle' | 'Closed'>): {
|
|
76
|
-
provider:
|
|
96
|
+
provider: DatabaseProviderBase;
|
|
77
97
|
closedIds: string[];
|
|
78
98
|
closedReasons: Map<string, string | null>;
|
|
79
99
|
} {
|
|
@@ -105,7 +125,8 @@ function makeProvider(store: Map<string, 'Active' | 'Idle' | 'Closed'>): {
|
|
|
105
125
|
};
|
|
106
126
|
return entity as unknown as Awaited<ReturnType<IMetadataProvider['GetEntityObject']>>;
|
|
107
127
|
}),
|
|
108
|
-
|
|
128
|
+
...LEDGER_SCHEMA_PROVIDER_FIELDS,
|
|
129
|
+
} as unknown as DatabaseProviderBase;
|
|
109
130
|
return { provider, closedIds, closedReasons };
|
|
110
131
|
}
|
|
111
132
|
|
|
@@ -129,6 +150,11 @@ beforeEach(() => {
|
|
|
129
150
|
logStatusMock.mockReset();
|
|
130
151
|
recoverMock.mockReset();
|
|
131
152
|
resolveAccountMock.mockReset();
|
|
153
|
+
assembleMock.mockReset();
|
|
154
|
+
for (const method of Object.values(ledgerMock)) method.mockReset();
|
|
155
|
+
// Quiet ledger by default: nothing to purge, every claim lost.
|
|
156
|
+
ledgerMock.PurgeClosedBefore.mockResolvedValue(0);
|
|
157
|
+
ledgerMock.ClaimAssembly.mockResolvedValue(null);
|
|
132
158
|
sessionPages = [];
|
|
133
159
|
runViewMock.mockImplementation(async (params: { EntityName: string }) => {
|
|
134
160
|
if (params.EntityName === 'MJ: AI Agent Session Channels') {
|
|
@@ -379,7 +405,7 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
|
|
|
379
405
|
let nextSessionNumber = 0;
|
|
380
406
|
|
|
381
407
|
interface RecoveryHarness {
|
|
382
|
-
provider:
|
|
408
|
+
provider: DatabaseProviderBase;
|
|
383
409
|
/** Session id -> RecordingFileID the reload will report (default null). */
|
|
384
410
|
fileIds: Map<string, string | null>;
|
|
385
411
|
agentLoads: string[];
|
|
@@ -414,7 +440,8 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
|
|
|
414
440
|
};
|
|
415
441
|
return entity;
|
|
416
442
|
}),
|
|
417
|
-
|
|
443
|
+
...LEDGER_SCHEMA_PROVIDER_FIELDS,
|
|
444
|
+
} as unknown as DatabaseProviderBase;
|
|
418
445
|
return { provider, fileIds, agentLoads, failingAgents };
|
|
419
446
|
}
|
|
420
447
|
|
|
@@ -425,7 +452,7 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
|
|
|
425
452
|
}
|
|
426
453
|
|
|
427
454
|
/** Runs one sweep whose candidate page holds exactly `rows`. */
|
|
428
|
-
async function sweepWith(provider:
|
|
455
|
+
async function sweepWith(provider: DatabaseProviderBase, rows: unknown[]): Promise<number> {
|
|
429
456
|
sessionPages = [{ Success: true, Results: rows }];
|
|
430
457
|
return SessionJanitor.Instance.RunRecordingRecoverySweep(provider, makeUser());
|
|
431
458
|
}
|
|
@@ -455,6 +482,14 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
|
|
|
455
482
|
expect(filter).toContain(`ClosedAt >= '${new Date(NOW.getTime() - 168 * HOUR).toISOString()}'`);
|
|
456
483
|
});
|
|
457
484
|
|
|
485
|
+
it('leaves sessions on the shard ledger to the ledger sweep', async () => {
|
|
486
|
+
const { provider } = makeRecoveryProvider();
|
|
487
|
+
await sweepWith(provider, []);
|
|
488
|
+
|
|
489
|
+
const filter = recoveryFilter();
|
|
490
|
+
expect(filter).toContain(`RecordingStatus IS NULL AND ID NOT IN (SELECT AgentSessionID FROM ${LEDGER_VIEW})`);
|
|
491
|
+
});
|
|
492
|
+
|
|
458
493
|
it('honours Configure() overrides for grace and lookback', async () => {
|
|
459
494
|
SessionJanitor.Instance.Configure({ recordingRecoveryGraceMinutes: 30, recordingRecoveryLookbackHours: 24 });
|
|
460
495
|
const { provider } = makeRecoveryProvider();
|
|
@@ -878,7 +913,7 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
|
|
|
878
913
|
}
|
|
879
914
|
});
|
|
880
915
|
|
|
881
|
-
it('runs recovery after the close sweeps', async () => {
|
|
916
|
+
it('runs recovery after the close sweeps: ledger assembly first, then legacy segment recovery', async () => {
|
|
882
917
|
const { provider } = makeRecoveryProvider();
|
|
883
918
|
await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
|
|
884
919
|
runViewMock.mockClear();
|
|
@@ -886,10 +921,69 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
|
|
|
886
921
|
await vi.advanceTimersByTimeAsync(60_000);
|
|
887
922
|
|
|
888
923
|
const filters = sessionSweepCalls().map((c) => c[0].ExtraFilter);
|
|
889
|
-
expect(filters).toHaveLength(
|
|
924
|
+
expect(filters).toHaveLength(4);
|
|
890
925
|
expect(filters[0]).toContain('LastActiveAt <');
|
|
891
926
|
expect(filters[1]).toContain('maxSessionDeadlineIso');
|
|
892
|
-
expect(filters[2]).toContain('
|
|
927
|
+
expect(filters[2]).toContain("RecordingStatus IN ('Pending','Assembling')");
|
|
928
|
+
expect(filters[3]).toContain('RecordingStartedAt');
|
|
929
|
+
});
|
|
930
|
+
|
|
931
|
+
it('purges ledger shards past the lookback window once per pass', async () => {
|
|
932
|
+
const { provider } = makeRecoveryProvider();
|
|
933
|
+
await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
|
|
934
|
+
ledgerMock.PurgeClosedBefore.mockResolvedValueOnce(42);
|
|
935
|
+
|
|
936
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
937
|
+
|
|
938
|
+
expect(ledgerMock.PurgeClosedBefore).toHaveBeenCalledTimes(1);
|
|
939
|
+
const [cutoff, maxRows] = ledgerMock.PurgeClosedBefore.mock.calls[0] as [Date, number];
|
|
940
|
+
expect(cutoff.toISOString()).toBe(new Date(Date.now() - 168 * HOUR).toISOString()); // now = the tick
|
|
941
|
+
expect(maxRows).toBe(500);
|
|
942
|
+
expect(logStatusMock).toHaveBeenCalledWith(expect.stringMatching(/purged 42 recording shard/i));
|
|
943
|
+
});
|
|
944
|
+
|
|
945
|
+
it('a Stop() during the pass ends it before the next step', async () => {
|
|
946
|
+
const { provider } = makeRecoveryProvider();
|
|
947
|
+
await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
|
|
948
|
+
runViewMock.mockClear();
|
|
949
|
+
let releasePurge: () => void = () => undefined;
|
|
950
|
+
ledgerMock.PurgeClosedBefore.mockImplementationOnce(
|
|
951
|
+
() => new Promise<number>((resolve) => { releasePurge = () => resolve(0); }),
|
|
952
|
+
);
|
|
953
|
+
|
|
954
|
+
await vi.advanceTimersByTimeAsync(60_000); // the pass parks in the purge
|
|
955
|
+
SessionJanitor.Instance.Stop();
|
|
956
|
+
releasePurge();
|
|
957
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
958
|
+
|
|
959
|
+
expect(sessionSweepCalls().filter((c) => c[0].ExtraFilter.includes('RecordingStatus'))).toHaveLength(0);
|
|
960
|
+
});
|
|
961
|
+
|
|
962
|
+
it('a failed purge is logged with context and the sweeps still run', async () => {
|
|
963
|
+
const { provider } = makeRecoveryProvider();
|
|
964
|
+
await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
|
|
965
|
+
runViewMock.mockClear();
|
|
966
|
+
ledgerMock.PurgeClosedBefore.mockRejectedValueOnce(new Error('purge deadlock'));
|
|
967
|
+
|
|
968
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
969
|
+
|
|
970
|
+
expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(/purge.*purge deadlock/i));
|
|
971
|
+
expect(sessionSweepCalls().some((c) => c[0].ExtraFilter.includes('RecordingStartedAt'))).toBe(true);
|
|
972
|
+
});
|
|
973
|
+
|
|
974
|
+
it('a failed ledger sweep is logged and legacy recovery still runs', async () => {
|
|
975
|
+
const { provider } = makeRecoveryProvider();
|
|
976
|
+
await SessionJanitor.Instance.Start(provider, makeUser(), 60_000);
|
|
977
|
+
runViewMock.mockClear();
|
|
978
|
+
runViewMock.mockImplementation(async (params: { ExtraFilter?: string }) => {
|
|
979
|
+
if (params.ExtraFilter?.includes("RecordingStatus IN ('Pending','Assembling')")) throw new Error('ledger query blew up');
|
|
980
|
+
return { Success: true, Results: [] };
|
|
981
|
+
});
|
|
982
|
+
|
|
983
|
+
await vi.advanceTimersByTimeAsync(60_000);
|
|
984
|
+
|
|
985
|
+
expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(/ledger.*ledger query blew up/i));
|
|
986
|
+
expect(sessionSweepCalls().some((c) => c[0].ExtraFilter.includes('RecordingStartedAt'))).toBe(true);
|
|
893
987
|
});
|
|
894
988
|
|
|
895
989
|
it('a slow recovery does not block the close sweeps on later ticks and is not started twice', async () => {
|
|
@@ -1018,6 +1112,267 @@ describe('SessionJanitor.RunRecordingRecoverySweep', () => {
|
|
|
1018
1112
|
});
|
|
1019
1113
|
});
|
|
1020
1114
|
|
|
1115
|
+
describe('SessionJanitor.RunRecordingLedgerSweep', () => {
|
|
1116
|
+
const NOW = new Date('2026-10-12T12:00:00.000Z');
|
|
1117
|
+
const MINUTE = 60_000;
|
|
1118
|
+
const HOUR = 60 * MINUTE;
|
|
1119
|
+
const STALE_CUTOFF = new Date(NOW.getTime() - 10 * MINUTE); // recordingRecoveryAttemptTimeoutMinutes
|
|
1120
|
+
let nextSessionNumber = 0;
|
|
1121
|
+
|
|
1122
|
+
type LedgerCandidate = {
|
|
1123
|
+
ID: string;
|
|
1124
|
+
AgentID: string;
|
|
1125
|
+
RecordingStatus: 'Pending' | 'Assembling' | null;
|
|
1126
|
+
RecordingExpectedShards: number | null;
|
|
1127
|
+
RecordingCapturedSamples: number | null;
|
|
1128
|
+
__mj_UpdatedAt: Date;
|
|
1129
|
+
};
|
|
1130
|
+
|
|
1131
|
+
/** Session ids are unique per test: the janitor is a singleton whose backoff outlives a test. */
|
|
1132
|
+
function ledgerCandidate(overrides: Partial<LedgerCandidate> = {}): LedgerCandidate {
|
|
1133
|
+
return {
|
|
1134
|
+
ID: `ledger-session-${nextSessionNumber++}`,
|
|
1135
|
+
AgentID: 'agent-1',
|
|
1136
|
+
RecordingStatus: 'Pending',
|
|
1137
|
+
RecordingExpectedShards: null,
|
|
1138
|
+
RecordingCapturedSamples: null,
|
|
1139
|
+
__mj_UpdatedAt: new Date(NOW.getTime() - HOUR),
|
|
1140
|
+
...overrides,
|
|
1141
|
+
};
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function ledgerProvider(): DatabaseProviderBase {
|
|
1145
|
+
return { GetEntityObject: vi.fn(), ...LEDGER_SCHEMA_PROVIDER_FIELDS } as unknown as DatabaseProviderBase;
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
/** Runs one ledger sweep whose candidate page holds exactly `rows`. */
|
|
1149
|
+
async function ledgerSweepWith(rows: LedgerCandidate[], provider = ledgerProvider()): Promise<number> {
|
|
1150
|
+
sessionPages = [{ Success: true, Results: rows }];
|
|
1151
|
+
return SessionJanitor.Instance.RunRecordingLedgerSweep(provider, makeUser());
|
|
1152
|
+
}
|
|
1153
|
+
|
|
1154
|
+
function ledgerFilter(): string {
|
|
1155
|
+
const call = sessionSweepCalls().find((c) => c[0].ExtraFilter.includes('RecordingStatus'));
|
|
1156
|
+
if (!call) throw new Error('ledger sweep never queried sessions');
|
|
1157
|
+
return call[0].ExtraFilter;
|
|
1158
|
+
}
|
|
1159
|
+
|
|
1160
|
+
function claimedIDs(): string[] {
|
|
1161
|
+
return ledgerMock.ClaimAssembly.mock.calls.map((c) => c[0] as string);
|
|
1162
|
+
}
|
|
1163
|
+
|
|
1164
|
+
beforeEach(() => {
|
|
1165
|
+
vi.useFakeTimers();
|
|
1166
|
+
vi.setSystemTime(NOW);
|
|
1167
|
+
SessionJanitor.Instance.Configure({ recordingRecoveryGraceMinutes: 10, recordingRecoveryLookbackHours: 168, recordingRecoveryAttemptTimeoutMinutes: 10 });
|
|
1168
|
+
ledgerMock.ClaimAssembly.mockResolvedValue(7);
|
|
1169
|
+
assembleMock.mockResolvedValue('Partial');
|
|
1170
|
+
});
|
|
1171
|
+
|
|
1172
|
+
afterEach(() => {
|
|
1173
|
+
SessionJanitor.Instance.Stop();
|
|
1174
|
+
vi.useRealTimers();
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
it('queries closed sessions past the grace and inside the lookback that are Pending, Assembling, or lost their status with shards on the ledger', async () => {
|
|
1178
|
+
await ledgerSweepWith([]);
|
|
1179
|
+
|
|
1180
|
+
const filter = ledgerFilter();
|
|
1181
|
+
expect(filter).toContain("Status = 'Closed'");
|
|
1182
|
+
expect(filter).toContain(`ClosedAt < '${new Date(NOW.getTime() - 10 * MINUTE).toISOString()}'`);
|
|
1183
|
+
expect(filter).toContain(`ClosedAt >= '${new Date(NOW.getTime() - 168 * HOUR).toISOString()}'`);
|
|
1184
|
+
expect(filter).toContain("RecordingStatus IN ('Pending','Assembling')");
|
|
1185
|
+
expect(filter).toContain(`RecordingStatus IS NULL AND ID IN (SELECT AgentSessionID FROM ${LEDGER_VIEW})`);
|
|
1186
|
+
expect(filter).not.toContain('RecordingStartedAt');
|
|
1187
|
+
});
|
|
1188
|
+
|
|
1189
|
+
it('assembles a closed Pending session with no terminal signal as Partial: stale-aware claim, then a forced assembly', async () => {
|
|
1190
|
+
const session = ledgerCandidate();
|
|
1191
|
+
|
|
1192
|
+
expect(await ledgerSweepWith([session])).toBe(1);
|
|
1193
|
+
|
|
1194
|
+
expect(ledgerMock.ClaimAssembly).toHaveBeenCalledWith(session.ID, STALE_CUTOFF);
|
|
1195
|
+
expect(assembleMock).toHaveBeenCalledTimes(1);
|
|
1196
|
+
expect(assembleMock).toHaveBeenCalledWith(expect.objectContaining({
|
|
1197
|
+
SessionID: session.ID, Version: 7, Ledger: ledgerMock, ForcePartial: true,
|
|
1198
|
+
}));
|
|
1199
|
+
expect(logStatusMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${session.ID}.*Partial`)));
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
it('re-claims an Assembling session whose assembler went quiet past the attempt timeout, but not a fresh one', async () => {
|
|
1203
|
+
const stale = ledgerCandidate({ RecordingStatus: 'Assembling', __mj_UpdatedAt: new Date(STALE_CUTOFF.getTime() - 1) });
|
|
1204
|
+
const fresh = ledgerCandidate({ RecordingStatus: 'Assembling', __mj_UpdatedAt: new Date(NOW.getTime() - MINUTE) });
|
|
1205
|
+
|
|
1206
|
+
expect(await ledgerSweepWith([fresh, stale])).toBe(1);
|
|
1207
|
+
|
|
1208
|
+
expect(claimedIDs()).toEqual([stale.ID]);
|
|
1209
|
+
expect(ledgerMock.ClaimAssembly).toHaveBeenCalledWith(stale.ID, STALE_CUTOFF);
|
|
1210
|
+
});
|
|
1211
|
+
|
|
1212
|
+
it('skips a session whose claim another assembler won, without backing it off', async () => {
|
|
1213
|
+
const session = ledgerCandidate();
|
|
1214
|
+
ledgerMock.ClaimAssembly.mockResolvedValueOnce(null);
|
|
1215
|
+
|
|
1216
|
+
expect(await ledgerSweepWith([session])).toBe(0);
|
|
1217
|
+
expect(assembleMock).not.toHaveBeenCalled();
|
|
1218
|
+
expect(logErrorMock).not.toHaveBeenCalled();
|
|
1219
|
+
|
|
1220
|
+
await ledgerSweepWith([session]); // not backed off: the next sweep tries the claim again
|
|
1221
|
+
expect(claimedIDs()).toEqual([session.ID, session.ID]);
|
|
1222
|
+
});
|
|
1223
|
+
|
|
1224
|
+
it('backs off a session whose assembly hit a transient error (Retry), and retries after the backoff', async () => {
|
|
1225
|
+
const session = ledgerCandidate();
|
|
1226
|
+
assembleMock.mockResolvedValueOnce('Retry');
|
|
1227
|
+
|
|
1228
|
+
expect(await ledgerSweepWith([session])).toBe(0);
|
|
1229
|
+
await ledgerSweepWith([session]);
|
|
1230
|
+
expect(claimedIDs()).toEqual([session.ID]);
|
|
1231
|
+
|
|
1232
|
+
vi.setSystemTime(NOW.getTime() + 11 * MINUTE); // first backoff is 2^1 * 5 min
|
|
1233
|
+
await ledgerSweepWith([session]);
|
|
1234
|
+
expect(claimedIDs()).toEqual([session.ID, session.ID]);
|
|
1235
|
+
});
|
|
1236
|
+
|
|
1237
|
+
it('gives up on a session that keeps hitting Retry after 8 attempts, with one LogError', async () => {
|
|
1238
|
+
const session = ledgerCandidate();
|
|
1239
|
+
assembleMock.mockResolvedValue('Retry');
|
|
1240
|
+
|
|
1241
|
+
for (let i = 0; i < 12; i++) {
|
|
1242
|
+
await ledgerSweepWith([session]);
|
|
1243
|
+
vi.setSystemTime(Date.now() + 7 * HOUR); // past the longest (6 h) backoff
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
expect(assembleMock).toHaveBeenCalledTimes(8);
|
|
1247
|
+
expect(logErrorMock.mock.calls.filter((c) => /Giving up/.test(String(c[0])) && String(c[0]).includes(session.ID))).toHaveLength(1);
|
|
1248
|
+
});
|
|
1249
|
+
|
|
1250
|
+
it('LogErrors a Failed assembly with the session id and counts it as a failure', async () => {
|
|
1251
|
+
const session = ledgerCandidate();
|
|
1252
|
+
assembleMock.mockResolvedValueOnce('Failed');
|
|
1253
|
+
|
|
1254
|
+
expect(await ledgerSweepWith([session])).toBe(0);
|
|
1255
|
+
expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${session.ID}.*Failed`)));
|
|
1256
|
+
await ledgerSweepWith([session]);
|
|
1257
|
+
expect(claimedIDs()).toEqual([session.ID]); // backed off
|
|
1258
|
+
});
|
|
1259
|
+
|
|
1260
|
+
it('logs a Superseded assembly without treating it as a failure', async () => {
|
|
1261
|
+
const session = ledgerCandidate();
|
|
1262
|
+
assembleMock.mockResolvedValueOnce('Superseded');
|
|
1263
|
+
|
|
1264
|
+
expect(await ledgerSweepWith([session])).toBe(0);
|
|
1265
|
+
expect(logErrorMock).not.toHaveBeenCalled();
|
|
1266
|
+
expect(logStatusMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${session.ID}.*Superseded`)));
|
|
1267
|
+
await ledgerSweepWith([session]);
|
|
1268
|
+
expect(claimedIDs()).toEqual([session.ID, session.ID]);
|
|
1269
|
+
});
|
|
1270
|
+
|
|
1271
|
+
it('LogErrors an unexpected Deferred from a forced assembly and backs off', async () => {
|
|
1272
|
+
const session = ledgerCandidate();
|
|
1273
|
+
assembleMock.mockResolvedValueOnce('Deferred');
|
|
1274
|
+
|
|
1275
|
+
await ledgerSweepWith([session]);
|
|
1276
|
+
expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${session.ID}.*Deferred`)));
|
|
1277
|
+
await ledgerSweepWith([session]);
|
|
1278
|
+
expect(claimedIDs()).toEqual([session.ID]);
|
|
1279
|
+
});
|
|
1280
|
+
|
|
1281
|
+
it('logs and backs off a claim that throws, then moves on to the next candidate', async () => {
|
|
1282
|
+
const [bad, good] = [ledgerCandidate(), ledgerCandidate()];
|
|
1283
|
+
ledgerMock.ClaimAssembly.mockRejectedValueOnce(new Error('claim deadlock'));
|
|
1284
|
+
|
|
1285
|
+
expect(await ledgerSweepWith([bad, good])).toBe(1);
|
|
1286
|
+
expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${bad.ID}.*claim deadlock`)));
|
|
1287
|
+
});
|
|
1288
|
+
|
|
1289
|
+
it('restores the status a racing session save wiped with a (0, 0) stamp, never counts invented from the ledger, before claiming', async () => {
|
|
1290
|
+
// No terminal counts survive: stamping 0/0 sets NULL -> Pending, and the assembly (shard count != 0)
|
|
1291
|
+
// is Partial and keeps the shards, instead of a made-up terminal labelling it Complete.
|
|
1292
|
+
const session = ledgerCandidate({ RecordingStatus: null });
|
|
1293
|
+
ledgerMock.StampTerminal.mockResolvedValueOnce({ Outcome: 'Stamped', RecordingStatus: 'Pending' });
|
|
1294
|
+
|
|
1295
|
+
expect(await ledgerSweepWith([session])).toBe(1);
|
|
1296
|
+
|
|
1297
|
+
expect(ledgerMock.StampTerminal).toHaveBeenCalledWith(session.ID, 0, 0);
|
|
1298
|
+
expect(ledgerMock.LoadShardMetadata).not.toHaveBeenCalled();
|
|
1299
|
+
expect(ledgerMock.StampTerminal.mock.invocationCallOrder[0]).toBeLessThan(ledgerMock.ClaimAssembly.mock.invocationCallOrder[0]);
|
|
1300
|
+
expect(assembleMock).toHaveBeenCalledWith(expect.objectContaining({ SessionID: session.ID, ForcePartial: true }));
|
|
1301
|
+
});
|
|
1302
|
+
|
|
1303
|
+
it('restores a lost status with the counts the session still stores', async () => {
|
|
1304
|
+
const session = ledgerCandidate({ RecordingStatus: null, RecordingExpectedShards: 9, RecordingCapturedSamples: 2_000_000 });
|
|
1305
|
+
ledgerMock.StampTerminal.mockResolvedValueOnce({ Outcome: 'Stamped', RecordingStatus: 'Pending' });
|
|
1306
|
+
|
|
1307
|
+
await ledgerSweepWith([session]);
|
|
1308
|
+
|
|
1309
|
+
expect(ledgerMock.StampTerminal).toHaveBeenCalledWith(session.ID, 9, 2_000_000);
|
|
1310
|
+
});
|
|
1311
|
+
|
|
1312
|
+
it('stamps a BIGINT capture count that the candidate page delivers as a string as a number', async () => {
|
|
1313
|
+
// A RunView entity page keeps the driver's value for a BIGINT column: the SQL Server driver
|
|
1314
|
+
// returns it as a string, and the ledger's integer guard refused it (#5294 gauntlet, row S13 J3).
|
|
1315
|
+
const session = ledgerCandidate({ RecordingStatus: null, RecordingExpectedShards: 5, RecordingCapturedSamples: '1137600' as unknown as number });
|
|
1316
|
+
ledgerMock.StampTerminal.mockResolvedValueOnce({ Outcome: 'Stamped', RecordingStatus: 'Pending' });
|
|
1317
|
+
|
|
1318
|
+
await ledgerSweepWith([session]);
|
|
1319
|
+
|
|
1320
|
+
expect(ledgerMock.StampTerminal).toHaveBeenCalledWith(session.ID, 5, 1_137_600);
|
|
1321
|
+
});
|
|
1322
|
+
|
|
1323
|
+
it('LogErrors and backs off a lost status the stamp could not restore', async () => {
|
|
1324
|
+
const session = ledgerCandidate({ RecordingStatus: null });
|
|
1325
|
+
ledgerMock.StampTerminal.mockResolvedValueOnce({ Outcome: 'Ignored', RecordingStatus: null });
|
|
1326
|
+
|
|
1327
|
+
expect(await ledgerSweepWith([session])).toBe(0);
|
|
1328
|
+
expect(ledgerMock.ClaimAssembly).not.toHaveBeenCalled();
|
|
1329
|
+
expect(logErrorMock).toHaveBeenCalledWith(expect.stringContaining(session.ID));
|
|
1330
|
+
await ledgerSweepWith([session]);
|
|
1331
|
+
expect(ledgerMock.StampTerminal).toHaveBeenCalledTimes(1); // backed off
|
|
1332
|
+
});
|
|
1333
|
+
|
|
1334
|
+
it('counts failed status restores toward the per-sweep cap', async () => {
|
|
1335
|
+
const sessions = Array.from({ length: 7 }, () => ledgerCandidate({ RecordingStatus: null }));
|
|
1336
|
+
ledgerMock.StampTerminal.mockRejectedValue(new Error('stamp deadlock'));
|
|
1337
|
+
|
|
1338
|
+
await ledgerSweepWith(sessions);
|
|
1339
|
+
|
|
1340
|
+
expect(ledgerMock.StampTerminal).toHaveBeenCalledTimes(5);
|
|
1341
|
+
});
|
|
1342
|
+
|
|
1343
|
+
it('attempts at most 5 assemblies per sweep', async () => {
|
|
1344
|
+
const sessions = Array.from({ length: 7 }, () => ledgerCandidate());
|
|
1345
|
+
|
|
1346
|
+
expect(await ledgerSweepWith(sessions)).toBe(5);
|
|
1347
|
+
expect(assembleMock).toHaveBeenCalledTimes(5);
|
|
1348
|
+
});
|
|
1349
|
+
|
|
1350
|
+
it('abandons an assembly that outlives the attempt timeout, backs it off, and continues with the next candidate', async () => {
|
|
1351
|
+
const [hung, next] = [ledgerCandidate(), ledgerCandidate()];
|
|
1352
|
+
let release: () => void = () => undefined;
|
|
1353
|
+
assembleMock.mockImplementation(async (input: { SessionID: string }) => {
|
|
1354
|
+
if (input.SessionID === hung.ID) {
|
|
1355
|
+
await new Promise<void>((resolve) => { release = resolve; });
|
|
1356
|
+
return 'Retry';
|
|
1357
|
+
}
|
|
1358
|
+
return 'Complete';
|
|
1359
|
+
});
|
|
1360
|
+
try {
|
|
1361
|
+
const sweep = ledgerSweepWith([hung, next]);
|
|
1362
|
+
await vi.advanceTimersByTimeAsync(10 * MINUTE);
|
|
1363
|
+
expect(await sweep).toBe(1);
|
|
1364
|
+
expect(logErrorMock).toHaveBeenCalledWith(expect.stringMatching(new RegExp(`${hung.ID}.*did not finish within 10 minutes`)));
|
|
1365
|
+
|
|
1366
|
+
vi.setSystemTime(Date.now() + 11 * MINUTE); // past the backoff, but the abandoned call is still running
|
|
1367
|
+
await ledgerSweepWith([hung]);
|
|
1368
|
+
expect(assembleMock.mock.calls.map((c) => (c[0] as { SessionID: string }).SessionID)).toEqual([hung.ID, next.ID]);
|
|
1369
|
+
} finally {
|
|
1370
|
+
release();
|
|
1371
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
1372
|
+
}
|
|
1373
|
+
});
|
|
1374
|
+
});
|
|
1375
|
+
|
|
1021
1376
|
describe('SessionJanitor.Configure', () => {
|
|
1022
1377
|
const DEFAULTS: SessionJanitorConfig = {
|
|
1023
1378
|
closeThresholdMinutes: 15, sweepIntervalMs: 60_000, recordingRecoveryGraceMinutes: 10, recordingRecoveryLookbackHours: 168,
|
|
@@ -29,6 +29,16 @@ vi.mock('@memberjunction/ai-agents', () => ({
|
|
|
29
29
|
},
|
|
30
30
|
}));
|
|
31
31
|
|
|
32
|
+
// --- Mock the recording assembly scheduler so a close's assembly request is observable (#5294) ---
|
|
33
|
+
const scheduleAssemblyMock = vi.fn();
|
|
34
|
+
vi.mock('../realtimeSessions/RecordingAssemblyScheduler.js', () => ({
|
|
35
|
+
RecordingAssemblyScheduler: {
|
|
36
|
+
get Instance() {
|
|
37
|
+
return { ScheduleAssembly: scheduleAssemblyMock };
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
}));
|
|
41
|
+
|
|
32
42
|
// --- Mock RunView.FromMetadataProvider so channel sweeps and run-id lookups are controllable,
|
|
33
43
|
// while leaving the rest of @memberjunction/core intact. RunViews runs each view through runViewMock. ---
|
|
34
44
|
const runViewMock = vi.fn();
|
|
@@ -135,6 +145,7 @@ beforeEach(() => {
|
|
|
135
145
|
hostSessionClosedMock.mockResolvedValue(undefined);
|
|
136
146
|
runViewMock.mockReset();
|
|
137
147
|
runViewMock.mockResolvedValue({ Success: true, Results: [] });
|
|
148
|
+
scheduleAssemblyMock.mockReset();
|
|
138
149
|
getSystemUserMock.mockReset();
|
|
139
150
|
getSystemUserMock.mockReturnValue(undefined);
|
|
140
151
|
});
|
|
@@ -566,6 +577,22 @@ describe('SessionManager.Heartbeat', () => {
|
|
|
566
577
|
});
|
|
567
578
|
});
|
|
568
579
|
|
|
580
|
+
describe('SessionManager.CloseSession — recording assembly (#5294)', () => {
|
|
581
|
+
it('CloseSession of a recorded session asks for its assembly now (the hidden-tab deferral ends with the call)', async () => {
|
|
582
|
+
const session = makeSessionEntity({ ID: 'session-1', Status: 'Active', RecordingStartedAt: new Date(0) });
|
|
583
|
+
const { provider } = makeProvider(() => session);
|
|
584
|
+
await new SessionManager().CloseSession('session-1', makeUser(), provider);
|
|
585
|
+
expect(scheduleAssemblyMock).toHaveBeenCalledWith('session-1', 0, false);
|
|
586
|
+
});
|
|
587
|
+
|
|
588
|
+
it('CloseSession of a session that was not recorded asks for no assembly', async () => {
|
|
589
|
+
const session = makeSessionEntity({ ID: 'session-1', Status: 'Active', RecordingStartedAt: null });
|
|
590
|
+
const { provider } = makeProvider(() => session);
|
|
591
|
+
await new SessionManager().CloseSession('session-1', makeUser(), provider);
|
|
592
|
+
expect(scheduleAssemblyMock).not.toHaveBeenCalled();
|
|
593
|
+
});
|
|
594
|
+
});
|
|
595
|
+
|
|
569
596
|
describe('SessionManager.Heartbeat — the session\'s co-agent run stays alive', () => {
|
|
570
597
|
beforeEach(() => {
|
|
571
598
|
keepCoAgentRunAliveMock.mockClear();
|
|
@@ -169,4 +169,29 @@ describe('TaskGraphActionRunner: outputs must be storable', () => {
|
|
|
169
169
|
|
|
170
170
|
expect(result).toMatchObject({ Success: true, Output: { AgentRunID: 'RUN-1' }, ActionLogID: 'LOG-2' });
|
|
171
171
|
});
|
|
172
|
+
|
|
173
|
+
it('merges structured JSON Message properties into Output and aliases handle/Handle', async () => {
|
|
174
|
+
vi.mocked(ActionEngineServer.Instance.RunAction).mockResolvedValue({
|
|
175
|
+
Success: true,
|
|
176
|
+
Message: JSON.stringify({
|
|
177
|
+
message: 'Created new PDF document.',
|
|
178
|
+
handle: 'doc-handle-99',
|
|
179
|
+
documentType: 'pdf',
|
|
180
|
+
fileName: 'Sales_Demo_Report.pdf',
|
|
181
|
+
}),
|
|
182
|
+
Params: [],
|
|
183
|
+
LogEntry: { ID: 'LOG-3' },
|
|
184
|
+
} as never);
|
|
185
|
+
|
|
186
|
+
const result = await new TaskGraphActionRunner().RunActionForTask(baseRunParams({ DocumentType: 'pdf' }));
|
|
187
|
+
|
|
188
|
+
expect(result.Success).toBe(true);
|
|
189
|
+
expect(result.Output).toMatchObject({
|
|
190
|
+
handle: 'doc-handle-99',
|
|
191
|
+
Handle: 'doc-handle-99',
|
|
192
|
+
documentType: 'pdf',
|
|
193
|
+
fileName: 'Sales_Demo_Report.pdf',
|
|
194
|
+
});
|
|
195
|
+
});
|
|
172
196
|
});
|
|
197
|
+
|