@wabot-dev/framework 0.9.26 → 0.9.80
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 +27 -0
- package/dist/src/feature/chat-bot/stripAnsweredMedia.js +19 -0
- package/dist/src/feature/chat-controller/runChatControllers.js +4 -1
- package/dist/src/feature/rest-controller/runRestControllers.js +11 -6
- package/dist/src/index.d.ts +9 -1
- package/dist/src/index.js +2 -2
- package/dist/src/testing/LlmJudge.js +93 -0
- package/dist/src/testing/MockChatAdapter.js +68 -0
- package/dist/src/testing/TestChatMemory.js +73 -0
- package/dist/src/testing/asyncHarness.js +66 -0
- package/dist/src/testing/auth.js +114 -0
- package/dist/src/testing/chatBotHarness.js +88 -0
- package/dist/src/testing/chatControllerHarness.js +94 -0
- package/dist/src/testing/conformance/chatAdapterConformanceCases.js +656 -0
- package/dist/src/testing/fixtures.js +53 -0
- package/dist/src/testing/helpers.js +42 -0
- package/dist/src/testing/index.d.ts +776 -0
- package/dist/src/testing/index.js +13 -0
- package/dist/src/testing/repositories.js +34 -0
- package/dist/src/testing/restHarness.js +127 -0
- package/dist/src/testing/testImageBase64.js +5 -0
- package/dist/src/testing/validation.js +66 -0
- package/package.json +19 -2
package/README.md
CHANGED
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
- 🤖 **Soporte para bots con IA** - Inteligencia artificial lista para usar
|
|
29
29
|
- 🧠 **Múltiples proveedores de IA** - OpenAI, Google Gemini, Anthropic Claude
|
|
30
30
|
- 💬 **Múltiples canales de chat** - Telegram, WhatsApp, Socket y más
|
|
31
|
+
- 🧪 **Sistema de testing integrado** - Prueba tus chatbots sin API keys ni base de datos
|
|
31
32
|
- 📖 **Documentación clara y en español** - Aprende sin barreras de idioma
|
|
32
33
|
|
|
33
34
|
---
|
|
@@ -103,6 +104,32 @@ Los addons opcionales (`pg`, SDKs de IA) se mantienen como `peerDependencies` y
|
|
|
103
104
|
|
|
104
105
|
---
|
|
105
106
|
|
|
107
|
+
## 🧪 Testing
|
|
108
|
+
|
|
109
|
+
El framework incluye un sistema de testing en `@wabot-dev/framework/testing`, agnóstico al runner (node:test, vitest, bun test). Permite probar chatbots de forma determinista —sin API keys ni base de datos— y también evaluar el comportamiento real con un juez LLM.
|
|
110
|
+
|
|
111
|
+
```ts
|
|
112
|
+
import { createChatBotHarness, LlmJudge } from '@wabot-dev/framework/testing'
|
|
113
|
+
|
|
114
|
+
// Determinista: el LLM se simula, tus tools se ejecutan de verdad
|
|
115
|
+
const harness = createChatBotHarness({ mindset: EliaMindset })
|
|
116
|
+
harness.adapter.callTool('saveEvent', { title: 'Demo', ... }).reply('¡Agendado!')
|
|
117
|
+
|
|
118
|
+
const turn = await harness.send('agenda una demo mañana')
|
|
119
|
+
// turn.replies, turn.toolCalls (con resultados reales), harness.history()
|
|
120
|
+
|
|
121
|
+
// Evals: juzga conversaciones reales con un LLM
|
|
122
|
+
const judge = new LlmJudge({ adapter, models: [{ model: 'claude-haiku-4-5' }] })
|
|
123
|
+
await judge.assert({
|
|
124
|
+
transcript: harness.history(),
|
|
125
|
+
criteria: 'Responde en español y confirma la fecha del evento',
|
|
126
|
+
})
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
También incluye harnesses para controllers de chat (`createChatControllerHarness`), endpoints REST con guards JWT/API-Key reales (`createRestHarness`), commands y crons (`createAsyncHarness`), repositorios en memoria (`useMemoryRepositories`), aserciones de validación y una suite de conformance para adapters LLM propios (`chatAdapterConformanceCases`).
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
106
133
|
## 💬 Plataformas Soportadas
|
|
107
134
|
|
|
108
135
|
Wabot se integra nativamente con las principales plataformas de mensajería:
|
|
@@ -10,8 +10,27 @@ function stripAnsweredMedia(items) {
|
|
|
10
10
|
const humanMessage = { ...item.humanMessage };
|
|
11
11
|
delete humanMessage.images;
|
|
12
12
|
delete humanMessage.documents;
|
|
13
|
+
// A media-only message becomes empty once its binaries are stripped, which
|
|
14
|
+
// makes the provider adapters reject it as empty content. Leave a short
|
|
15
|
+
// placeholder so the answered turn stays in the history coherently.
|
|
16
|
+
if (isStrippedMessageEmpty(humanMessage)) {
|
|
17
|
+
humanMessage.text = describeStrippedMedia(item.humanMessage);
|
|
18
|
+
}
|
|
13
19
|
return { type: 'humanMessage', humanMessage };
|
|
14
20
|
});
|
|
15
21
|
}
|
|
22
|
+
function isStrippedMessageEmpty(message) {
|
|
23
|
+
return !message.text && !message.object;
|
|
24
|
+
}
|
|
25
|
+
function describeStrippedMedia(message) {
|
|
26
|
+
const parts = [];
|
|
27
|
+
const images = message.images?.length ?? 0;
|
|
28
|
+
const documents = message.documents?.length ?? 0;
|
|
29
|
+
if (images > 0)
|
|
30
|
+
parts.push(`${images} image${images > 1 ? 's' : ''}`);
|
|
31
|
+
if (documents > 0)
|
|
32
|
+
parts.push(`${documents} document${documents > 1 ? 's' : ''}`);
|
|
33
|
+
return `[sent ${parts.join(' and ')}]`;
|
|
34
|
+
}
|
|
16
35
|
|
|
17
36
|
export { stripAnsweredMedia };
|
|
@@ -43,6 +43,9 @@ async function prepareChatContainer(container, messageContext, mindsetCtor) {
|
|
|
43
43
|
chatContainer.register(chatBotMetadata.injectionToken, { useValue: chatBot });
|
|
44
44
|
});
|
|
45
45
|
}
|
|
46
|
+
if (mindsetCtor) {
|
|
47
|
+
chatContainer.register(Mindset, { useClass: mindsetCtor });
|
|
48
|
+
}
|
|
46
49
|
return chatContainer;
|
|
47
50
|
}
|
|
48
51
|
const logger = new Logger('wabot:chat-controller');
|
|
@@ -91,4 +94,4 @@ function runChatControllers(controllers) {
|
|
|
91
94
|
}
|
|
92
95
|
}
|
|
93
96
|
|
|
94
|
-
export { runChatControllers };
|
|
97
|
+
export { prepareChatContainer, runChatControllers };
|
|
@@ -16,11 +16,12 @@ import { Container } from '../../core/injection/Container.js';
|
|
|
16
16
|
function buildRequest(req) {
|
|
17
17
|
return Object.assign({}, req.body, req.query, req.params);
|
|
18
18
|
}
|
|
19
|
-
function
|
|
19
|
+
function registerRestControllers(controllers, options = {}) {
|
|
20
20
|
const logger = new Logger('wabot:rest');
|
|
21
|
-
const
|
|
22
|
-
const
|
|
23
|
-
const
|
|
21
|
+
const baseContainer = options.baseContainer ?? container;
|
|
22
|
+
const metadataStore = baseContainer.resolve(RestControllerMetadataStore);
|
|
23
|
+
const expressProvider = options.expressProvider ?? baseContainer.resolve(ExpressProvider);
|
|
24
|
+
const validationMetadataStore = baseContainer.resolve(ValidationMetadataStore);
|
|
24
25
|
const expressApp = expressProvider.getExpress();
|
|
25
26
|
controllers.forEach((controller) => {
|
|
26
27
|
const endPoints = metadataStore.getControllerEndPointsInfo(controller);
|
|
@@ -38,7 +39,7 @@ function runRestControllers(controllers) {
|
|
|
38
39
|
rawMiddlewares.push(urlencoded({ extended: true }));
|
|
39
40
|
}
|
|
40
41
|
expressApp[method](route, ...rawMiddlewares, async (req, res) => {
|
|
41
|
-
const requestContainer =
|
|
42
|
+
const requestContainer = baseContainer.createChildContainer();
|
|
42
43
|
requestContainer.register(Container, { useValue: requestContainer });
|
|
43
44
|
requestContainer.register(EXPRESS_REQ, { useValue: req });
|
|
44
45
|
requestContainer.register(EXPRESS_RES, { useValue: req });
|
|
@@ -93,6 +94,10 @@ function runRestControllers(controllers) {
|
|
|
93
94
|
});
|
|
94
95
|
});
|
|
95
96
|
});
|
|
97
|
+
return expressProvider;
|
|
98
|
+
}
|
|
99
|
+
function runRestControllers(controllers) {
|
|
100
|
+
const expressProvider = registerRestControllers(controllers);
|
|
96
101
|
expressProvider.listen();
|
|
97
102
|
}
|
|
98
103
|
function removeCircular(obj, seen = new WeakSet()) {
|
|
@@ -110,4 +115,4 @@ function removeCircular(obj, seen = new WeakSet()) {
|
|
|
110
115
|
return obj;
|
|
111
116
|
}
|
|
112
117
|
|
|
113
|
-
export { runRestControllers };
|
|
118
|
+
export { registerRestControllers, runRestControllers };
|
package/dist/src/index.d.ts
CHANGED
|
@@ -1295,6 +1295,7 @@ interface IMessageContext extends IReceivedMessage {
|
|
|
1295
1295
|
authInfo?: object;
|
|
1296
1296
|
}
|
|
1297
1297
|
|
|
1298
|
+
declare function prepareChatContainer(container: DependencyContainer$1, messageContext: IMessageContext, mindsetCtor?: IConstructor<IMindset>): Promise<DependencyContainer$1>;
|
|
1298
1299
|
declare function runChatControllers(controllers: IConstructor<any>[]): void;
|
|
1299
1300
|
|
|
1300
1301
|
declare class HttpServerProvider {
|
|
@@ -1623,6 +1624,13 @@ declare class RestControllerMetadataStore {
|
|
|
1623
1624
|
}[];
|
|
1624
1625
|
}
|
|
1625
1626
|
|
|
1627
|
+
interface IRegisterRestControllersOptions {
|
|
1628
|
+
/** Container the per-request child containers derive from. */
|
|
1629
|
+
baseContainer?: DependencyContainer$1;
|
|
1630
|
+
/** Express provider to mount the routes on. */
|
|
1631
|
+
expressProvider?: ExpressProvider;
|
|
1632
|
+
}
|
|
1633
|
+
declare function registerRestControllers(controllers: IConstructor<any>[], options?: IRegisterRestControllersOptions): ExpressProvider;
|
|
1626
1634
|
declare function runRestControllers(controllers: IConstructor<any>[]): void;
|
|
1627
1635
|
|
|
1628
1636
|
declare const EXPRESS_REQ = "EXPRESS_REQ";
|
|
@@ -2722,4 +2730,4 @@ declare function HtmlModule(options: IHtmlModuleOptions): {
|
|
|
2722
2730
|
new (): {};
|
|
2723
2731
|
};
|
|
2724
2732
|
|
|
2725
|
-
export { AnthropicChatAdapter, ApiKey, ApiKeyGuardMiddleware, ApiKeyHandshakeGuardMiddleware, ApiKeyRepository, Async, AsyncMetadataStore, Auth, Chat, ChatAdapter, ChatAdapterMetadataStore, ChatAdapterRegistry, ChatBot, ChatBotMetadataStore, ChatItem, ChatMemory, ChatOperator, ChatRepository, ChatResolver, type ClientMap, CmdChannel, CmdChannelConfig, CmdChannelServer, type CmdClientMessage, type CmdServerMessage, type ConfigReference, type ConfigReferenceType, ConfigResolver, Container, ControllerMetadataStore, CronJob, CronJobRepository, CrudRepository, CustomError, DeepSeekChatAdapter, DescriptionMetadataStore, EXPRESS_REQ, EXPRESS_RES, Entity, Env, type ErrorSeverity, ExpressProvider, GoogleChatAdapter, type GoogleChatAdapterV2Options, HtmlModule, HttpServerProvider, type IApiKeyData, type IApiKeyRepository, type IArrayValidationError, type IArrayValidationResult, type IBotMessageItem, type IBuiltQuery, type IChannelMessage, type IChannelMetadata, type IChatAdapter, type IChatAdapterDecoratorConfig, type IChatAdapterMetadata, type IChatAdapterNextItemsReq, type IChatAdapterNextItemsRes, type IChatAssociation, type IChatBot, type IChatBotMetadata, type IChatChannel, type IChatConnection, type IChatControllerMetadata, type IChatData, type IChatItem, type IChatItemData, type IChatItemType, type IChatMemory, type IChatMessage, type IChatMessageDocument, type IChatMessageFile, type IChatMessageImage, type IChatMessagesPrivateFile, type IChatMessagesPublicFile, type IChatRepository, type IChatType, type ICmdChannelEntry, type ICmdChannelHandlers, type ICmdChannelMessage, type ICmdImage, type ICmdIncomingMessage, type ICmdReceivedMessage, type ICommandConfig, type ICommandHandler, type ICommandHandlerConfig, type IConstructor, type ICronConfig, type ICronHandler, type ICronJobData, type ICronJobRepository, type ICrudRepository, type ICustomErrorData, type IDedupConfig, type IDescriptionMetadata, type IEndPointConfig, type IEndPointMetadata, type IEntityData, type IEnvType, type IErrorHandlersConfig, type IErrorMonitor, type IErrorMonitorContext, type IExtractChatMessageTextOptions, type IFunctionCall, type IFunctionCallItem, type IGenerateApiKeyReq, type IGenerateApiKeyRes, type IHandshakeMiddleware, type IHandshakeMiddlewareMetadata, type IHtmlModuleOptions, type IHumanMessageItem, type IJobData, type IJobOptions, type IJobRepository, type IJwtRefreshTokenData, type IJwtRefreshTokenRepository, type IKapsoChannelConfig, type IKapsoChannelMessage, type IKapsoChannelMessageListener, type IKapsoChatMessage, type IKapsoConversation, type IKapsoEvent, type IKapsoIncomingMessage, type IKapsoMessageReceivedEvent, type IKapsoReceivedMessage, type IKapsoUnknownEvent, type ILanguageModelUsage, type ILockKey, type ILocker, type ILockerKey, type IMemoryRepositoryAdapterOptions, type IMessageContext, type IMiddleware, type IMiddlewareMetadata, type IMindset, type IMindsetConfig, type IMindsetIdentity, type IMindsetLlm, type IMindsetMetadata, type IMindsetModelKind, type IMindsetModelRef, type IMindsetModels, type IMindsetModuleConfig, type IMindsetModuleMetadata, type IMindsetParameterSchema, type IMindsetTool, type IMindsetToolParameter, type IModelValidationError, type IModelValidationResult, type IModelValidatorsInfo, type IMoneyData, type IPersistentData, type IPgRepositoryConfig, type IProjectRunnerConfig, type IPropertyValidatorInfo, type IQueryAst, type IQueryCondition, type IQueryMethodMetadata, type IQueryOrderBy, type IReceivedMessage, type IRemoteApiKeyFetcher, type IRepositoryAdapter, type IRepositoryConfig, type IRepositoryRuntime, type IRestControllerConfig, type IRestControllerMetadata, type IScanProjectFilesOptions, type IScheduleAt, type IScheduleDelay, type ISendWhatsAppMessageReq, type ISendWhatsAppTemplateReq, type ISocketChannelConfig, type ISocketChannelMessage, type ISocketChannelReceivedMessage, type ISocketControllerConfig, type ISocketControllerMetadata, type ISocketEventConfig, type ISocketEventMetadata, type ISocketReceivedMessage, type IStorableData, type ITelegramChannelConfig, type ITelegramChannelMessage, type ITelegramReceivedMessage, type ITransactionAdapter, type IValidateArrayOptions, type IValidateArrayOptionsWithItemsValidators, type IValidateInputShape, type IValidateIsInOptions, type IValidateIsRecordOptions, type IValidateMaxOptions, type IValidateMinOptions, type IValidationError, type IValidationResult, type IValidator, type IValidatorMetadata, type IWasenderChannelConfig, type IWasenderChannelMessageListener, type IWasenderDeviceListMetadata, type IWasenderEvent, type IWasenderMessageContent, type IWasenderMessageContextInfo, type IWasenderMessageKey, type IWasenderMessageReceivedData, type IWasenderMessageReceivedEvent, type IWasenderQrUpdatedEvent, type IWasenderReceivedMessage, type IWhatsAppCloudContact, type IWhatsAppCloudMessage, type IWhatsAppCloudMessageMetadata, type IWhatsAppCloudTemplate, type IWhatsAppCloudTemplateComponent, type IWhatsAppCloudTemplateResponse, type IWhatsAppCloudWebhookPayload, type IWhatsAppSender, type IWhatsAppTemplateData, type IWhatsAppTemplateParameter, type IchatControllerConfig, InMemoryChatMemory, InMemoryChatRepository, InMemoryCronJobRepository, InMemoryJobRepository, InMemoryLockKey, InMemoryLocker, Job, JobRepository, JobRunner, Jwt, JwtAccessAndRefreshTokenDto, JwtConfig, JwtGuardMiddleware, JwtHandshakeGuardMiddleware, JwtRefreshToken, JwtRefreshTokenRepository, JwtSigner, JwtTokenDto, KapsoChannel, KapsoChannelConfig, KapsoReceiver, KapsoSender, KapsoWebhookController, Lifecycle, Locker, Logger, MEMORY_ADAPTER_ID, Mapper, MemoryRepositoryAdapter, MemoryRepositoryExtension, Mindset, MindsetMetadataStore, MindsetOperator, Money, MoneyDto, OpenRouterChatAdapter, OpenaiChatAdapter, PG_ADAPTER_ID, Password, type PasswordHashOptions, Persistent, PgApiKeyRepository, PgChatMemory, PgChatRepository, PgCronJobRepository, PgCrudRepository, PgJobRepository, PgJsonRepositoryAdapter, PgJwtRefreshTokenRepository, PgLockKey, PgLocker, PgRepositoryBase, PgRepositoryBase as PgRepositoryExtension, PgTransactionAdapter, ProjectRunner, type QueryConnector, type QueryOperator, type QueryPrefix, Random, RemoteApiKeyRepository, RepositoryAdapterRegistry, RepositoryMetadataStore, type ResolvedConfig, RestControllerMetadataStore, RestRequest, SocketChannel, SocketChannelConfig, SocketChannelMessageFile, SocketChannelReceivedMessage, SocketControllerMetadataStore, SocketServerConfig, SocketServerProvider, Storable, TelegramChannel, TelegramChannelConfig, TransactionMetadataStore, UnionChatAdapter, ValidationMetadataStore, WabotChatAdapter, WasenderChannel, WasenderChannelConfig, WasenderReceiver, WasenderSender, WasenderWebhookController, WhatsAppApiSender, WhatsAppReceiverByCloudApi, WhatsAppSender, apiKeyGuard, apiKeyHandshakeGuard, bool, boolArr, buildQuerySql, chatAdapter, chatBot, chatController, chatItemTypeOptions, cmd, cmdChannelName, cmdChannelSocketPath, command, commandHandler, computeDedupKey, container, cronHandler, description, errorToPlainObject, evaluateQueryAst, extractChatMessageText, extractNumberFromWasenderMessageKey, getClientMap, getPgClient, handshakeMiddlewares, inject, injectable, isArray, isBoolean, isChatMessageEmpty, isDate, isIn, isModel, isNotEmpty, isNumber, isOptional, isPresent, isRecord, isRetryableError, isString, jwtGuard, jwtHandshakeGuard, kapso, kapsoChannelName, markdownToTelegramHtml, max, memExtension, middleware, min, mindset, mindsetModule, modelInfo, num, numArr, obj, onDelete, onGet, onPost, onPut, onSocketEvent, parseQueryMethodName, pendingMediaStartIndex, pgExtension, pgStorage, query, queryExtension, readJsonFromFile, repository, resolveConfigReferences, restController, run, runChatAdapters, runChatControllers, runCmdClient, runCommandHandlers, runCronHandlers, runRestControllers, runSocketControllers, safeJsonParse, scanProjectFiles, scoped, setupErrorHandlers, singleton, socket, socketChannelName, socketController, stopCommandHandlers, stopCronHandlers, str, strArr, stripAnsweredMedia, telegram, telegramChannelName, transaction, validateAndTransform, validateArray, validateIsBoolean, validateIsDate, validateIsIn, validateIsNotEmpty, validateIsNumber, validateIsPresent, validateIsRecord, validateIsString, validateMax, validateMin, validateModel, wasender, wasenderChannelName, withPgClient, withPgTransaction, writeJsonToFile };
|
|
2733
|
+
export { AnthropicChatAdapter, ApiKey, ApiKeyGuardMiddleware, ApiKeyHandshakeGuardMiddleware, ApiKeyRepository, Async, AsyncMetadataStore, Auth, Chat, ChatAdapter, ChatAdapterMetadataStore, ChatAdapterRegistry, ChatBot, ChatBotMetadataStore, ChatItem, ChatMemory, ChatOperator, ChatRepository, ChatResolver, type ClientMap, CmdChannel, CmdChannelConfig, CmdChannelServer, type CmdClientMessage, type CmdServerMessage, type ConfigReference, type ConfigReferenceType, ConfigResolver, Container, ControllerMetadataStore, CronJob, CronJobRepository, CrudRepository, CustomError, DeepSeekChatAdapter, DescriptionMetadataStore, EXPRESS_REQ, EXPRESS_RES, Entity, Env, type ErrorSeverity, ExpressProvider, GoogleChatAdapter, type GoogleChatAdapterV2Options, HtmlModule, HttpServerProvider, type IApiKeyData, type IApiKeyRepository, type IArrayValidationError, type IArrayValidationResult, type IBotMessageItem, type IBuiltQuery, type IChannelMessage, type IChannelMetadata, type IChatAdapter, type IChatAdapterDecoratorConfig, type IChatAdapterMetadata, type IChatAdapterNextItemsReq, type IChatAdapterNextItemsRes, type IChatAssociation, type IChatBot, type IChatBotMetadata, type IChatChannel, type IChatConnection, type IChatControllerMetadata, type IChatData, type IChatItem, type IChatItemData, type IChatItemType, type IChatMemory, type IChatMessage, type IChatMessageDocument, type IChatMessageFile, type IChatMessageImage, type IChatMessagesPrivateFile, type IChatMessagesPublicFile, type IChatRepository, type IChatType, type ICmdChannelEntry, type ICmdChannelHandlers, type ICmdChannelMessage, type ICmdImage, type ICmdIncomingMessage, type ICmdReceivedMessage, type ICommandConfig, type ICommandHandler, type ICommandHandlerConfig, type IConstructor, type ICronConfig, type ICronHandler, type ICronJobData, type ICronJobRepository, type ICrudRepository, type ICustomErrorData, type IDedupConfig, type IDescriptionMetadata, type IEndPointConfig, type IEndPointMetadata, type IEntityData, type IEnvType, type IErrorHandlersConfig, type IErrorMonitor, type IErrorMonitorContext, type IExtractChatMessageTextOptions, type IFunctionCall, type IFunctionCallItem, type IGenerateApiKeyReq, type IGenerateApiKeyRes, type IHandshakeMiddleware, type IHandshakeMiddlewareMetadata, type IHtmlModuleOptions, type IHumanMessageItem, type IJobData, type IJobOptions, type IJobRepository, type IJwtRefreshTokenData, type IJwtRefreshTokenRepository, type IKapsoChannelConfig, type IKapsoChannelMessage, type IKapsoChannelMessageListener, type IKapsoChatMessage, type IKapsoConversation, type IKapsoEvent, type IKapsoIncomingMessage, type IKapsoMessageReceivedEvent, type IKapsoReceivedMessage, type IKapsoUnknownEvent, type ILanguageModelUsage, type ILockKey, type ILocker, type ILockerKey, type IMemoryRepositoryAdapterOptions, type IMessageContext, type IMiddleware, type IMiddlewareMetadata, type IMindset, type IMindsetConfig, type IMindsetIdentity, type IMindsetLlm, type IMindsetMetadata, type IMindsetModelKind, type IMindsetModelRef, type IMindsetModels, type IMindsetModuleConfig, type IMindsetModuleMetadata, type IMindsetParameterSchema, type IMindsetTool, type IMindsetToolParameter, type IModelValidationError, type IModelValidationResult, type IModelValidatorsInfo, type IMoneyData, type IPersistentData, type IPgRepositoryConfig, type IProjectRunnerConfig, type IPropertyValidatorInfo, type IQueryAst, type IQueryCondition, type IQueryMethodMetadata, type IQueryOrderBy, type IReceivedMessage, type IRegisterRestControllersOptions, type IRemoteApiKeyFetcher, type IRepositoryAdapter, type IRepositoryConfig, type IRepositoryRuntime, type IRestControllerConfig, type IRestControllerMetadata, type IScanProjectFilesOptions, type IScheduleAt, type IScheduleDelay, type ISendWhatsAppMessageReq, type ISendWhatsAppTemplateReq, type ISocketChannelConfig, type ISocketChannelMessage, type ISocketChannelReceivedMessage, type ISocketControllerConfig, type ISocketControllerMetadata, type ISocketEventConfig, type ISocketEventMetadata, type ISocketReceivedMessage, type IStorableData, type ITelegramChannelConfig, type ITelegramChannelMessage, type ITelegramReceivedMessage, type ITransactionAdapter, type IValidateArrayOptions, type IValidateArrayOptionsWithItemsValidators, type IValidateInputShape, type IValidateIsInOptions, type IValidateIsRecordOptions, type IValidateMaxOptions, type IValidateMinOptions, type IValidationError, type IValidationResult, type IValidator, type IValidatorMetadata, type IWasenderChannelConfig, type IWasenderChannelMessageListener, type IWasenderDeviceListMetadata, type IWasenderEvent, type IWasenderMessageContent, type IWasenderMessageContextInfo, type IWasenderMessageKey, type IWasenderMessageReceivedData, type IWasenderMessageReceivedEvent, type IWasenderQrUpdatedEvent, type IWasenderReceivedMessage, type IWhatsAppCloudContact, type IWhatsAppCloudMessage, type IWhatsAppCloudMessageMetadata, type IWhatsAppCloudTemplate, type IWhatsAppCloudTemplateComponent, type IWhatsAppCloudTemplateResponse, type IWhatsAppCloudWebhookPayload, type IWhatsAppSender, type IWhatsAppTemplateData, type IWhatsAppTemplateParameter, type IchatControllerConfig, InMemoryChatMemory, InMemoryChatRepository, InMemoryCronJobRepository, InMemoryJobRepository, InMemoryLockKey, InMemoryLocker, Job, JobRepository, JobRunner, Jwt, JwtAccessAndRefreshTokenDto, JwtConfig, JwtGuardMiddleware, JwtHandshakeGuardMiddleware, JwtRefreshToken, JwtRefreshTokenRepository, JwtSigner, JwtTokenDto, KapsoChannel, KapsoChannelConfig, KapsoReceiver, KapsoSender, KapsoWebhookController, Lifecycle, Locker, Logger, MEMORY_ADAPTER_ID, Mapper, MemoryRepositoryAdapter, MemoryRepositoryExtension, Mindset, MindsetMetadataStore, MindsetOperator, Money, MoneyDto, OpenRouterChatAdapter, OpenaiChatAdapter, PG_ADAPTER_ID, Password, type PasswordHashOptions, Persistent, PgApiKeyRepository, PgChatMemory, PgChatRepository, PgCronJobRepository, PgCrudRepository, PgJobRepository, PgJsonRepositoryAdapter, PgJwtRefreshTokenRepository, PgLockKey, PgLocker, PgRepositoryBase, PgRepositoryBase as PgRepositoryExtension, PgTransactionAdapter, ProjectRunner, type QueryConnector, type QueryOperator, type QueryPrefix, Random, RemoteApiKeyRepository, RepositoryAdapterRegistry, RepositoryMetadataStore, type ResolvedConfig, RestControllerMetadataStore, RestRequest, SocketChannel, SocketChannelConfig, SocketChannelMessageFile, SocketChannelReceivedMessage, SocketControllerMetadataStore, SocketServerConfig, SocketServerProvider, Storable, TelegramChannel, TelegramChannelConfig, TransactionMetadataStore, UnionChatAdapter, ValidationMetadataStore, WabotChatAdapter, WasenderChannel, WasenderChannelConfig, WasenderReceiver, WasenderSender, WasenderWebhookController, WhatsAppApiSender, WhatsAppReceiverByCloudApi, WhatsAppSender, apiKeyGuard, apiKeyHandshakeGuard, bool, boolArr, buildQuerySql, chatAdapter, chatBot, chatController, chatItemTypeOptions, cmd, cmdChannelName, cmdChannelSocketPath, command, commandHandler, computeDedupKey, container, cronHandler, description, errorToPlainObject, evaluateQueryAst, extractChatMessageText, extractNumberFromWasenderMessageKey, getClientMap, getPgClient, handshakeMiddlewares, inject, injectable, isArray, isBoolean, isChatMessageEmpty, isDate, isIn, isModel, isNotEmpty, isNumber, isOptional, isPresent, isRecord, isRetryableError, isString, jwtGuard, jwtHandshakeGuard, kapso, kapsoChannelName, markdownToTelegramHtml, max, memExtension, middleware, min, mindset, mindsetModule, modelInfo, num, numArr, obj, onDelete, onGet, onPost, onPut, onSocketEvent, parseQueryMethodName, pendingMediaStartIndex, pgExtension, pgStorage, prepareChatContainer, query, queryExtension, readJsonFromFile, registerRestControllers, repository, resolveConfigReferences, restController, run, runChatAdapters, runChatControllers, runCmdClient, runCommandHandlers, runCronHandlers, runRestControllers, runSocketControllers, safeJsonParse, scanProjectFiles, scoped, setupErrorHandlers, singleton, socket, socketChannelName, socketController, stopCommandHandlers, stopCronHandlers, str, strArr, stripAnsweredMedia, telegram, telegramChannelName, transaction, validateAndTransform, validateArray, validateIsBoolean, validateIsDate, validateIsIn, validateIsNotEmpty, validateIsNumber, validateIsPresent, validateIsRecord, validateIsString, validateMax, validateMin, validateModel, wasender, wasenderChannelName, withPgClient, withPgTransaction, writeJsonToFile };
|
package/dist/src/index.js
CHANGED
|
@@ -84,7 +84,7 @@ export { stripAnsweredMedia } from './feature/chat-bot/stripAnsweredMedia.js';
|
|
|
84
84
|
export { chatController } from './feature/chat-controller/metadata/controller/@chatController.js';
|
|
85
85
|
export { ControllerMetadataStore } from './feature/chat-controller/metadata/ControllerMetadataStore.js';
|
|
86
86
|
export { ChatResolver } from './feature/chat-controller/ChatResolver.js';
|
|
87
|
-
export { runChatControllers } from './feature/chat-controller/runChatControllers.js';
|
|
87
|
+
export { prepareChatContainer, runChatControllers } from './feature/chat-controller/runChatControllers.js';
|
|
88
88
|
export { ExpressProvider } from './feature/express/ExpressProvider.js';
|
|
89
89
|
export { HttpServerProvider } from './feature/http/HttpServerProvider.js';
|
|
90
90
|
export { mindset } from './feature/mindset/metadata/mindsets/@mindset.js';
|
|
@@ -120,7 +120,7 @@ export { onPut } from './feature/rest-controller/metadata/@onPut.js';
|
|
|
120
120
|
export { onDelete } from './feature/rest-controller/metadata/@onDelete.js';
|
|
121
121
|
export { restController } from './feature/rest-controller/metadata/@restController.js';
|
|
122
122
|
export { RestControllerMetadataStore } from './feature/rest-controller/metadata/RestControllerMetadataStore.js';
|
|
123
|
-
export { runRestControllers } from './feature/rest-controller/runRestControllers.js';
|
|
123
|
+
export { registerRestControllers, runRestControllers } from './feature/rest-controller/runRestControllers.js';
|
|
124
124
|
export { EXPRESS_REQ, EXPRESS_RES } from './feature/rest-controller/injection-tokens.js';
|
|
125
125
|
export { RestRequest } from './feature/rest-controller/RestRequest.js';
|
|
126
126
|
export { SocketServerConfig } from './feature/socket/SocketServerConfig.js';
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
const VERDICT_TOOL = {
|
|
2
|
+
language: 'english',
|
|
3
|
+
name: 'submitVerdict',
|
|
4
|
+
description: 'Submit your evaluation verdict. You MUST always call this tool exactly once; never answer with plain text.',
|
|
5
|
+
parameters: [
|
|
6
|
+
{
|
|
7
|
+
name: 'pass',
|
|
8
|
+
required: true,
|
|
9
|
+
schema: { type: 'boolean', description: 'true if the transcript satisfies the criteria' },
|
|
10
|
+
},
|
|
11
|
+
{
|
|
12
|
+
name: 'reasoning',
|
|
13
|
+
required: true,
|
|
14
|
+
schema: { type: 'string', description: 'short explanation of the verdict' },
|
|
15
|
+
},
|
|
16
|
+
],
|
|
17
|
+
};
|
|
18
|
+
const JUDGE_SYSTEM_PROMPT = `
|
|
19
|
+
You are a strict QA judge for chatbot conversations.
|
|
20
|
+
You will receive a chat transcript and evaluation criteria.
|
|
21
|
+
Evaluate whether the transcript satisfies ALL the criteria.
|
|
22
|
+
You MUST report your verdict by calling the submitVerdict tool exactly once.
|
|
23
|
+
Never reply with plain text.
|
|
24
|
+
`;
|
|
25
|
+
function renderTranscript(items) {
|
|
26
|
+
return items
|
|
27
|
+
.map((item) => {
|
|
28
|
+
if (item.type === 'humanMessage') {
|
|
29
|
+
const msg = item.humanMessage;
|
|
30
|
+
const parts = [
|
|
31
|
+
msg.text,
|
|
32
|
+
msg.images?.length ? `[${msg.images.length} image(s)]` : undefined,
|
|
33
|
+
msg.documents?.length ? `[${msg.documents.length} document(s)]` : undefined,
|
|
34
|
+
msg.object ? JSON.stringify(msg.object) : undefined,
|
|
35
|
+
].filter(Boolean);
|
|
36
|
+
return `HUMAN: ${parts.join(' ')}`;
|
|
37
|
+
}
|
|
38
|
+
if (item.type === 'botMessage') {
|
|
39
|
+
return `BOT: ${item.botMessage.text ?? JSON.stringify(item.botMessage)}`;
|
|
40
|
+
}
|
|
41
|
+
const call = item.functionCall;
|
|
42
|
+
return `TOOL CALL: ${call.name}(${call.arguments ?? '{}'}) -> ${call.result ?? '(no result)'}`;
|
|
43
|
+
})
|
|
44
|
+
.join('\n');
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* Grades chatbot behavior with a real LLM. Provider-agnostic: the verdict is
|
|
48
|
+
* extracted through a forced tool call instead of parsing free-form text.
|
|
49
|
+
*/
|
|
50
|
+
class LlmJudge {
|
|
51
|
+
options;
|
|
52
|
+
constructor(options) {
|
|
53
|
+
this.options = options;
|
|
54
|
+
}
|
|
55
|
+
async evaluate(req) {
|
|
56
|
+
const transcript = typeof req.transcript === 'string' ? req.transcript : renderTranscript(req.transcript);
|
|
57
|
+
const { nextItems } = await this.options.adapter.nextItems({
|
|
58
|
+
models: this.options.models,
|
|
59
|
+
systemPrompt: JUDGE_SYSTEM_PROMPT,
|
|
60
|
+
tools: [VERDICT_TOOL],
|
|
61
|
+
prevItems: [
|
|
62
|
+
{
|
|
63
|
+
type: 'humanMessage',
|
|
64
|
+
humanMessage: {
|
|
65
|
+
text: `## Criteria\n${req.criteria}\n\n## Transcript\n${transcript}\n\nEvaluate now and call submitVerdict.`,
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
});
|
|
70
|
+
const verdictCall = nextItems.find((item) => item.type === 'functionCall' && item.functionCall.name === VERDICT_TOOL.name);
|
|
71
|
+
if (!verdictCall || verdictCall.type !== 'functionCall') {
|
|
72
|
+
const texts = nextItems
|
|
73
|
+
.map((item) => (item.type === 'botMessage' ? item.botMessage.text : null))
|
|
74
|
+
.filter(Boolean);
|
|
75
|
+
throw new Error(`LlmJudge: the judge model did not call submitVerdict. Got: ${texts.join(' | ') || JSON.stringify(nextItems)}`);
|
|
76
|
+
}
|
|
77
|
+
const args = JSON.parse(verdictCall.functionCall.arguments ?? '{}');
|
|
78
|
+
if (typeof args.pass !== 'boolean') {
|
|
79
|
+
throw new Error(`LlmJudge: invalid verdict arguments: ${verdictCall.functionCall.arguments}`);
|
|
80
|
+
}
|
|
81
|
+
return { pass: args.pass, reasoning: String(args.reasoning ?? '') };
|
|
82
|
+
}
|
|
83
|
+
/** Like evaluate(), but throws (with the judge's reasoning) when it fails. */
|
|
84
|
+
async assert(req) {
|
|
85
|
+
const verdict = await this.evaluate(req);
|
|
86
|
+
if (!verdict.pass) {
|
|
87
|
+
throw new Error(`LlmJudge: criteria not satisfied: ${req.criteria}\n${verdict.reasoning}`);
|
|
88
|
+
}
|
|
89
|
+
return verdict;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export { LlmJudge, renderTranscript };
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Deterministic IChatAdapter for tests: script the LLM turns with
|
|
3
|
+
* reply()/callTool()/enqueue() and assert on the recorded requests.
|
|
4
|
+
* Each queued response is consumed by one nextItems() call, so a tool
|
|
5
|
+
* call turn must be followed by another scripted turn (the ChatBot loop
|
|
6
|
+
* calls the adapter again after executing the tool).
|
|
7
|
+
*/
|
|
8
|
+
class MockChatAdapter {
|
|
9
|
+
options;
|
|
10
|
+
requests = [];
|
|
11
|
+
queue = [];
|
|
12
|
+
callIdCounter = 0;
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
this.options = options;
|
|
15
|
+
}
|
|
16
|
+
/** Queue a turn where the bot answers with a plain text message. */
|
|
17
|
+
reply(text) {
|
|
18
|
+
return this.enqueue([{ type: 'botMessage', botMessage: { text } }]);
|
|
19
|
+
}
|
|
20
|
+
/** Queue a turn where the bot requests a tool call (executed for real by the ChatBot). */
|
|
21
|
+
callTool(name, args) {
|
|
22
|
+
return this.enqueue([
|
|
23
|
+
{
|
|
24
|
+
type: 'functionCall',
|
|
25
|
+
functionCall: {
|
|
26
|
+
id: `mock-call-${++this.callIdCounter}`,
|
|
27
|
+
name,
|
|
28
|
+
arguments: typeof args === 'string' ? args : JSON.stringify(args ?? {}),
|
|
29
|
+
},
|
|
30
|
+
},
|
|
31
|
+
]);
|
|
32
|
+
}
|
|
33
|
+
/** Queue a raw turn: a list of chat items, or a function of the request. */
|
|
34
|
+
enqueue(response) {
|
|
35
|
+
this.queue.push(response);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
get lastRequest() {
|
|
39
|
+
return this.requests[this.requests.length - 1];
|
|
40
|
+
}
|
|
41
|
+
pendingResponses() {
|
|
42
|
+
return this.queue.length;
|
|
43
|
+
}
|
|
44
|
+
async nextItems(req) {
|
|
45
|
+
this.requests.push(req);
|
|
46
|
+
let response = this.queue.shift();
|
|
47
|
+
if (!response) {
|
|
48
|
+
if (this.options.fallbackReply === undefined) {
|
|
49
|
+
throw new Error('MockChatAdapter: no scripted response left for this turn. ' +
|
|
50
|
+
'Queue one with reply()/callTool()/enqueue(), or set the fallbackReply option. ' +
|
|
51
|
+
'Remember that after a callTool() turn the ChatBot calls the adapter again.');
|
|
52
|
+
}
|
|
53
|
+
response = [{ type: 'botMessage', botMessage: { text: this.options.fallbackReply } }];
|
|
54
|
+
}
|
|
55
|
+
const nextItems = typeof response === 'function' ? await response(req) : response;
|
|
56
|
+
return {
|
|
57
|
+
nextItems,
|
|
58
|
+
usage: {
|
|
59
|
+
inputTokens: 1,
|
|
60
|
+
outputTokens: 1,
|
|
61
|
+
provider: 'mock',
|
|
62
|
+
model: req.models[0]?.model ?? 'mock-model',
|
|
63
|
+
},
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export { MockChatAdapter };
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import '../feature/chat-bot/ChatAdapterRegistry.js';
|
|
3
|
+
import '../feature/chat-bot/ChatBot.js';
|
|
4
|
+
import { ChatOperator } from '../feature/chat-bot/ChatOperator.js';
|
|
5
|
+
import '../feature/chat-bot/UnionChatAdapter.js';
|
|
6
|
+
import '../core/injection/index.js';
|
|
7
|
+
import '../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
|
|
8
|
+
import 'uuid';
|
|
9
|
+
import '../feature/chat-bot/metadata/ChatBotMetadataStore.js';
|
|
10
|
+
import '../core/error/setupErrorHandlers.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Pure in-RAM chat memory for tests. Unlike the in-memory addon, it never
|
|
14
|
+
* touches the filesystem and exposes the full item list for assertions.
|
|
15
|
+
*/
|
|
16
|
+
class TestChatMemory {
|
|
17
|
+
items = [];
|
|
18
|
+
async findLastItems(count) {
|
|
19
|
+
return this.items.slice(-count);
|
|
20
|
+
}
|
|
21
|
+
async create(item) {
|
|
22
|
+
if (!item.wasCreated()) {
|
|
23
|
+
item['data'].id = randomUUID();
|
|
24
|
+
item['data'].createdAt = Date.now();
|
|
25
|
+
}
|
|
26
|
+
this.items.push(item);
|
|
27
|
+
}
|
|
28
|
+
all() {
|
|
29
|
+
return [...this.items];
|
|
30
|
+
}
|
|
31
|
+
allData() {
|
|
32
|
+
return this.items.map((item) => item.getData());
|
|
33
|
+
}
|
|
34
|
+
clear() {
|
|
35
|
+
this.items = [];
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
/** Pure in-RAM chat repository for tests (no `.wabot/` persistence). */
|
|
39
|
+
class TestChatRepository {
|
|
40
|
+
chats = [];
|
|
41
|
+
memories = new Map();
|
|
42
|
+
async create(chat) {
|
|
43
|
+
if (chat.wasCreated()) {
|
|
44
|
+
throw new Error('Chat already created');
|
|
45
|
+
}
|
|
46
|
+
chat['data'].id = randomUUID();
|
|
47
|
+
chat['data'].createdAt = Date.now();
|
|
48
|
+
chat.validate();
|
|
49
|
+
this.chats.push(chat);
|
|
50
|
+
this.memories.set(chat.id, new TestChatMemory());
|
|
51
|
+
}
|
|
52
|
+
async update(chat) {
|
|
53
|
+
if (!chat.wasCreated()) {
|
|
54
|
+
throw new Error('Chat was not created');
|
|
55
|
+
}
|
|
56
|
+
chat.validate();
|
|
57
|
+
}
|
|
58
|
+
async findByConnection(query) {
|
|
59
|
+
return this.chats.find((chat) => chat.hasConnection(query)) ?? null;
|
|
60
|
+
}
|
|
61
|
+
async findMemory(chatId) {
|
|
62
|
+
return this.memories.get(chatId) ?? null;
|
|
63
|
+
}
|
|
64
|
+
async findOperator(chatId) {
|
|
65
|
+
const chat = this.chats.find((item) => item.id === chatId) ?? null;
|
|
66
|
+
const memory = this.memories.get(chatId);
|
|
67
|
+
if (!chat || !memory)
|
|
68
|
+
return null;
|
|
69
|
+
return new ChatOperator(chat, memory, this);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export { TestChatMemory, TestChatRepository };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { Auth } from '../core/auth/Auth.js';
|
|
2
|
+
import { container } from '../core/injection/index.js';
|
|
3
|
+
import { AsyncMetadataStore } from '../feature/async/AsyncMetadataStore.js';
|
|
4
|
+
import '../feature/async/TransactionMetadataStore.js';
|
|
5
|
+
import '../feature/async/Async.js';
|
|
6
|
+
import '../_virtual/index.js';
|
|
7
|
+
import '../core/error/setupErrorHandlers.js';
|
|
8
|
+
import 'node:crypto';
|
|
9
|
+
import '../feature/async/JobRepository.js';
|
|
10
|
+
import '../feature/async/JobRunner.js';
|
|
11
|
+
import '../feature/async/JobScheduler.js';
|
|
12
|
+
import '../feature/async/JobWatchdog.js';
|
|
13
|
+
import '../feature/async/CronScheduler.js';
|
|
14
|
+
import { assertValid } from './validation.js';
|
|
15
|
+
import { Container } from '../core/injection/Container.js';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Executes @command handlers and @cronHandler jobs inline — no PostgreSQL,
|
|
19
|
+
* no polling workers — with the same validation production applies.
|
|
20
|
+
*/
|
|
21
|
+
class AsyncHarness {
|
|
22
|
+
container;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
const child = container.createChildContainer();
|
|
25
|
+
child.register(Container, { useValue: child });
|
|
26
|
+
for (const [token, instance] of options.register ?? []) {
|
|
27
|
+
child.registerInstance(token, instance);
|
|
28
|
+
}
|
|
29
|
+
if (options.authInfo) {
|
|
30
|
+
const auth = child.resolve(Auth);
|
|
31
|
+
auth.assign(options.authInfo);
|
|
32
|
+
}
|
|
33
|
+
this.container = child;
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Validate the command data and run its handler immediately, like the
|
|
37
|
+
* JobRunner does for a scheduled job. Returns the transformed command.
|
|
38
|
+
*/
|
|
39
|
+
async execute(commandConstructor, data = {}) {
|
|
40
|
+
const store = container.resolve(AsyncMetadataStore);
|
|
41
|
+
const commandName = store.getCommandName(commandConstructor);
|
|
42
|
+
if (!commandName) {
|
|
43
|
+
throw new Error(`AsyncHarness: ${commandConstructor.name} is not registered with the @command decorator`);
|
|
44
|
+
}
|
|
45
|
+
const handlerConstructor = store.getHandlerForCommandName(commandName);
|
|
46
|
+
if (!handlerConstructor) {
|
|
47
|
+
throw new Error(`AsyncHarness: no @commandHandler registered for command '${commandName}'`);
|
|
48
|
+
}
|
|
49
|
+
const command = assertValid(commandConstructor, data);
|
|
50
|
+
const handler = this.container.resolve(handlerConstructor);
|
|
51
|
+
await handler.handle(command);
|
|
52
|
+
return command;
|
|
53
|
+
}
|
|
54
|
+
/** Run a @cronHandler's handle() once, immediately. */
|
|
55
|
+
async runCron(cronConstructor) {
|
|
56
|
+
const store = container.resolve(AsyncMetadataStore);
|
|
57
|
+
store.requireCronMetadata(cronConstructor);
|
|
58
|
+
const handler = this.container.resolve(cronConstructor);
|
|
59
|
+
await handler.handle();
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function createAsyncHarness(options = {}) {
|
|
63
|
+
return new AsyncHarness(options);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { AsyncHarness, createAsyncHarness };
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
import jwt from 'jsonwebtoken';
|
|
2
|
+
import '../feature/socket-controller/metadata/SocketControllerMetadataStore.js';
|
|
3
|
+
import '../core/injection/index.js';
|
|
4
|
+
import { CustomError } from '../core/error/CustomError.js';
|
|
5
|
+
import '../core/error/setupErrorHandlers.js';
|
|
6
|
+
import 'debug';
|
|
7
|
+
import '../core/validation/metadata/ValidationMetadataStore.js';
|
|
8
|
+
import 'socket.io';
|
|
9
|
+
import '../feature/socket/SocketServerConfig.js';
|
|
10
|
+
import '../feature/socket/SocketServerProvider.js';
|
|
11
|
+
import '../addon/auth/api-key/ApiKeyHandshakeGuardMiddleware.js';
|
|
12
|
+
import '../feature/rest-controller/metadata/RestControllerMetadataStore.js';
|
|
13
|
+
import '../feature/express/ExpressProvider.js';
|
|
14
|
+
import 'express';
|
|
15
|
+
import 'node:path';
|
|
16
|
+
import 'node:http';
|
|
17
|
+
import '../addon/auth/api-key/ApiKeyGuardMiddleware.js';
|
|
18
|
+
import { ApiKey } from '../addon/auth/api-key/ApiKey.js';
|
|
19
|
+
import { ApiKeyRepository } from '../addon/auth/api-key/ApiKeyRepository.js';
|
|
20
|
+
import '../addon/auth/api-key/PgApiKeyRepository.js';
|
|
21
|
+
import '../addon/auth/jwt/JwtHandshakeGuardMiddleware.js';
|
|
22
|
+
import '../addon/auth/jwt/JwtGuardMiddleware.js';
|
|
23
|
+
import '../addon/auth/jwt/Jwt.js';
|
|
24
|
+
import '../addon/auth/jwt/JwtAccessAndRefreshTokenDto.js';
|
|
25
|
+
import { JwtConfig } from '../addon/auth/jwt/JwtConfig.js';
|
|
26
|
+
import 'node:crypto';
|
|
27
|
+
import '../addon/auth/jwt/JwtSigner.js';
|
|
28
|
+
import '../addon/auth/jwt/JwtTokenDto.js';
|
|
29
|
+
import '../addon/auth/jwt/PgJwtRefreshTokenRepository.js';
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* JWT setup for tests: a JwtConfig with a test secret (no JWT_SECRET env var
|
|
33
|
+
* needed) plus a signer, so the real JwtGuardMiddleware validates the tokens.
|
|
34
|
+
*/
|
|
35
|
+
class TestJwt {
|
|
36
|
+
config;
|
|
37
|
+
constructor(options = {}) {
|
|
38
|
+
const config = Object.create(JwtConfig.prototype);
|
|
39
|
+
config.algorithm = 'HS256';
|
|
40
|
+
config.secretOrPublicKey = options.secret ?? 'wabot-test-jwt-secret';
|
|
41
|
+
config.secretOrPrivateKey = options.secret ?? 'wabot-test-jwt-secret';
|
|
42
|
+
config.accessExpirationSeconds = options.accessExpirationSeconds ?? 10 * 60;
|
|
43
|
+
config.refreshExpirationSeconds = 365 * 24 * 3600;
|
|
44
|
+
this.config = config;
|
|
45
|
+
}
|
|
46
|
+
/** Sign a valid access token carrying the given authInfo. */
|
|
47
|
+
sign(authInfo) {
|
|
48
|
+
return jwt.sign(authInfo, this.config.secretOrPrivateKey, {
|
|
49
|
+
algorithm: this.config.algorithm,
|
|
50
|
+
expiresIn: this.config.accessExpirationSeconds,
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
/** A token signed with a different secret; useful for 401 tests. */
|
|
54
|
+
signInvalid(authInfo = {}) {
|
|
55
|
+
return jwt.sign(authInfo, `${this.config.secretOrPrivateKey}-corrupted`, {
|
|
56
|
+
algorithm: this.config.algorithm,
|
|
57
|
+
expiresIn: this.config.accessExpirationSeconds,
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
function setupTestJwt(options = {}) {
|
|
62
|
+
return new TestJwt(options);
|
|
63
|
+
}
|
|
64
|
+
/**
|
|
65
|
+
* In-RAM IApiKeyRepository so the real ApiKeyGuardMiddleware can run in
|
|
66
|
+
* tests. Register it with: register: [[ApiKeyRepository, testApiKeys]].
|
|
67
|
+
*/
|
|
68
|
+
class TestApiKeyRepository extends ApiKeyRepository {
|
|
69
|
+
items = [];
|
|
70
|
+
idCounter = 0;
|
|
71
|
+
/** Create a key for the given authInfo and return its secret. */
|
|
72
|
+
async addKey(authInfo, name = 'test-key') {
|
|
73
|
+
const { secret } = await this.generate({ name, authInfo });
|
|
74
|
+
return secret;
|
|
75
|
+
}
|
|
76
|
+
async find(id) {
|
|
77
|
+
return this.items.find((item) => item.id === id) ?? null;
|
|
78
|
+
}
|
|
79
|
+
async findOrThrow(id) {
|
|
80
|
+
const item = await this.find(id);
|
|
81
|
+
if (!item) {
|
|
82
|
+
throw new CustomError({ message: `Not found ApiKey with id = '${id}'`, httpCode: 404 });
|
|
83
|
+
}
|
|
84
|
+
return item;
|
|
85
|
+
}
|
|
86
|
+
async findByMetadata(metadata) {
|
|
87
|
+
return this.items.filter((item) => Object.entries(metadata).every(([key, value]) => item.metadata[key] === value));
|
|
88
|
+
}
|
|
89
|
+
async create(item) {
|
|
90
|
+
if (!item.wasCreated()) {
|
|
91
|
+
item['data'].id = `test-api-key-${++this.idCounter}`;
|
|
92
|
+
item['data'].createdAt = Date.now();
|
|
93
|
+
}
|
|
94
|
+
this.items.push(item);
|
|
95
|
+
}
|
|
96
|
+
async generate(req) {
|
|
97
|
+
const apiKey = new ApiKey({ name: req.name, metadata: req.metadata, authInfo: req.authInfo });
|
|
98
|
+
const secret = apiKey.generateSecret();
|
|
99
|
+
await this.create(apiKey);
|
|
100
|
+
return { apiKey, secret };
|
|
101
|
+
}
|
|
102
|
+
async findBySecret(secret) {
|
|
103
|
+
return this.items.find((item) => item.isValidSecret(secret)) ?? null;
|
|
104
|
+
}
|
|
105
|
+
async findAndValidate(secret) {
|
|
106
|
+
const apiKey = await this.findBySecret(secret);
|
|
107
|
+
if (!apiKey) {
|
|
108
|
+
throw new CustomError({ message: 'Invalid API key', httpCode: 401 });
|
|
109
|
+
}
|
|
110
|
+
return apiKey.authInfo;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
export { TestApiKeyRepository, TestJwt, setupTestJwt };
|