@wabot-dev/framework 0.9.27 → 2.0.0-beta.0

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.
Files changed (94) hide show
  1. package/README.md +27 -0
  2. package/bin/skills.mjs +151 -0
  3. package/bin/wabot-skills.mjs +120 -0
  4. package/dist/build/build.js +1031 -8
  5. package/dist/src/addon/chat-bot/in-memory/InMemoryChatMemory.js +1 -3
  6. package/dist/src/addon/chat-bot/xai/XAIChatAdapter.js +180 -0
  7. package/dist/src/addon/chat-controller/cmd/cmdChannelSocketPath.js +1 -5
  8. package/dist/src/addon/chat-controller/hubspot/@hubspot.js +28 -0
  9. package/dist/src/addon/chat-controller/hubspot/HubSpotChannel.js +81 -0
  10. package/dist/src/addon/chat-controller/hubspot/HubSpotChannelConfig.js +20 -0
  11. package/dist/src/addon/chat-controller/hubspot/HubSpotReceiver.js +42 -0
  12. package/dist/src/addon/chat-controller/hubspot/HubSpotSender.js +118 -0
  13. package/dist/src/addon/chat-controller/hubspot/HubSpotWebhookController.js +122 -0
  14. package/dist/src/addon/chat-controller/hubspot/downloadHubSpotAttachments.js +45 -0
  15. package/dist/src/addon/chat-controller/hubspot/hubspotChannelName.js +3 -0
  16. package/dist/src/addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js +28 -0
  17. package/dist/src/addon/chat-controller/{telegram/markdownToTelegramHtml.js → markdown/markdownToChatHtml.js} +5 -8
  18. package/dist/src/addon/chat-controller/slack/@slack.js +22 -0
  19. package/dist/src/addon/chat-controller/slack/SlackChannel.js +187 -0
  20. package/dist/src/addon/chat-controller/slack/SlackChannelConfig.js +12 -0
  21. package/dist/src/addon/chat-controller/slack/markdownToSlackMrkdwn.js +38 -0
  22. package/dist/src/addon/chat-controller/slack/slackChannelName.js +3 -0
  23. package/dist/src/addon/chat-controller/telegram/TelegramChannel.js +2 -2
  24. package/dist/src/addon/ui/preact/PreactRenderer.js +86 -0
  25. package/dist/src/addon/ui/preact/outlet.js +22 -0
  26. package/dist/src/addon/ui/preact/preactClientRuntime.js +67 -0
  27. package/dist/src/core/repository/CrudRepository.js +7 -7
  28. package/dist/src/feature/async/computeDedupKey.js +1 -1
  29. package/dist/src/feature/chat-controller/runChatControllers.js +4 -1
  30. package/dist/src/feature/pg/@pgExtension.js +2 -4
  31. package/dist/src/feature/project-runner/ProjectRunner.js +62 -10
  32. package/dist/src/feature/project-runner/scanner.js +1 -1
  33. package/dist/src/feature/repository/@memExtension.js +1 -2
  34. package/dist/src/feature/rest-controller/runRestControllers.js +11 -6
  35. package/dist/src/feature/ui-controller/actions.js +35 -0
  36. package/dist/src/feature/ui-controller/bundler/UiBundler.js +191 -0
  37. package/dist/src/feature/ui-controller/bundler/devMiddleware.js +41 -0
  38. package/dist/src/feature/ui-controller/bundler/index.js +4 -0
  39. package/dist/src/feature/ui-controller/bundler/manifest.js +34 -0
  40. package/dist/src/feature/ui-controller/bundler/navRuntime.js +236 -0
  41. package/dist/src/feature/ui-controller/bundler/pageAssets.js +30 -0
  42. package/dist/src/feature/ui-controller/document/escape.js +17 -0
  43. package/dist/src/feature/ui-controller/document/helpers.js +13 -0
  44. package/dist/src/feature/ui-controller/document/renderDocument.js +43 -0
  45. package/dist/src/feature/ui-controller/island/IslandRegistry.js +68 -0
  46. package/dist/src/feature/ui-controller/island/island.js +40 -0
  47. package/dist/src/feature/ui-controller/island/serialize.js +35 -0
  48. package/dist/src/feature/ui-controller/metadata/@action.js +18 -0
  49. package/dist/src/feature/ui-controller/metadata/@uiController.js +19 -0
  50. package/dist/src/feature/ui-controller/metadata/@uiMiddleware.js +20 -0
  51. package/dist/src/feature/ui-controller/metadata/@view.js +18 -0
  52. package/dist/src/feature/ui-controller/metadata/UiControllerMetadataStore.js +107 -0
  53. package/dist/src/feature/ui-controller/renderer/UiRendererRegistry.js +42 -0
  54. package/dist/src/feature/ui-controller/runUiControllers.js +285 -0
  55. package/dist/src/index.d.ts +640 -3
  56. package/dist/src/index.js +32 -3
  57. package/dist/src/testing/LlmJudge.js +93 -0
  58. package/dist/src/testing/MockChatAdapter.js +68 -0
  59. package/dist/src/testing/TestChatMemory.js +73 -0
  60. package/dist/src/testing/asyncHarness.js +66 -0
  61. package/dist/src/testing/auth.js +114 -0
  62. package/dist/src/testing/chatBotHarness.js +88 -0
  63. package/dist/src/testing/chatControllerHarness.js +94 -0
  64. package/dist/src/testing/conformance/chatAdapterConformanceCases.js +656 -0
  65. package/dist/src/testing/fixtures.js +53 -0
  66. package/dist/src/testing/helpers.js +42 -0
  67. package/dist/src/testing/index.d.ts +818 -0
  68. package/dist/src/testing/index.js +14 -0
  69. package/dist/src/testing/repositories.js +34 -0
  70. package/dist/src/testing/restHarness.js +127 -0
  71. package/dist/src/testing/testImageBase64.js +5 -0
  72. package/dist/src/testing/uiHarness.js +102 -0
  73. package/dist/src/testing/validation.js +66 -0
  74. package/dist/src/ui/client.js +6 -0
  75. package/dist/src/ui/index.d.ts +427 -0
  76. package/dist/src/ui/index.js +29 -0
  77. package/dist/src/ui/jsx-dev-runtime.d.ts +1 -0
  78. package/dist/src/ui/jsx-dev-runtime.js +1 -0
  79. package/dist/src/ui/jsx-runtime.d.ts +1 -0
  80. package/dist/src/ui/jsx-runtime.js +1 -0
  81. package/package.json +48 -11
  82. package/skills/wabot-async/SKILL.md +143 -0
  83. package/skills/wabot-auth/SKILL.md +153 -0
  84. package/skills/wabot-chat/SKILL.md +140 -0
  85. package/skills/wabot-di-config/SKILL.md +117 -0
  86. package/skills/wabot-framework/SKILL.md +81 -0
  87. package/skills/wabot-framework/references/quickstart.md +85 -0
  88. package/skills/wabot-mindset/SKILL.md +159 -0
  89. package/skills/wabot-ops/SKILL.md +151 -0
  90. package/skills/wabot-persistence/SKILL.md +159 -0
  91. package/skills/wabot-rest-socket/SKILL.md +167 -0
  92. package/skills/wabot-testing/SKILL.md +214 -0
  93. package/skills/wabot-ui/SKILL.md +201 -0
  94. package/skills/wabot-validation/SKILL.md +108 -0
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';
@@ -130,6 +130,20 @@ export { socketController } from './feature/socket-controller/metadata/@socketCo
130
130
  export { onSocketEvent } from './feature/socket-controller/metadata/@onSocketEvent.js';
131
131
  export { SocketControllerMetadataStore } from './feature/socket-controller/metadata/SocketControllerMetadataStore.js';
132
132
  export { runSocketControllers } from './feature/socket-controller/runSocketControllers.js';
133
+ export { uiController } from './feature/ui-controller/metadata/@uiController.js';
134
+ export { view } from './feature/ui-controller/metadata/@view.js';
135
+ export { action } from './feature/ui-controller/metadata/@action.js';
136
+ export { uiMiddleware } from './feature/ui-controller/metadata/@uiMiddleware.js';
137
+ export { UiControllerMetadataStore } from './feature/ui-controller/metadata/UiControllerMetadataStore.js';
138
+ export { UiRendererRegistry } from './feature/ui-controller/renderer/UiRendererRegistry.js';
139
+ export { ISLAND_MARKER, getIslandMeta, isIsland, island, setIslandId } from './feature/ui-controller/island/island.js';
140
+ export { deserializeProps, serializeProps } from './feature/ui-controller/island/serialize.js';
141
+ export { ISLAND_FILE_PATTERN, IslandRegistry, isIslandFile, toIslandId } from './feature/ui-controller/island/IslandRegistry.js';
142
+ export { renderDocument } from './feature/ui-controller/document/renderDocument.js';
143
+ export { REDIRECT_MARKER, isRedirect, redirect } from './feature/ui-controller/document/helpers.js';
144
+ export { escapeAttr, escapeHtml } from './feature/ui-controller/document/escape.js';
145
+ export { actionUrl, callAction } from './feature/ui-controller/actions.js';
146
+ export { registerUiControllers, runUiControllers } from './feature/ui-controller/runUiControllers.js';
133
147
  export { PgCronJobRepository } from './addon/async/pg/PgCronJobRepository.js';
134
148
  export { PgJobRepository } from './addon/async/pg/PgJobRepository.js';
135
149
  export { PgTransactionAdapter } from './addon/async/pg/PgTransactionAdapter.js';
@@ -165,6 +179,7 @@ export { PgChatMemory } from './addon/chat-bot/pg/PgChatMemory.js';
165
179
  export { InMemoryChatMemory } from './addon/chat-bot/in-memory/InMemoryChatMemory.js';
166
180
  export { InMemoryChatRepository } from './addon/chat-bot/in-memory/InMemoryChatRepository.js';
167
181
  export { WabotChatAdapter } from './addon/chat-bot/wabot/WabotChatAdapter.js';
182
+ export { XAIChatAdapter } from './addon/chat-bot/xai/XAIChatAdapter.js';
168
183
  export { cmd } from './addon/chat-controller/cmd/@cmd.js';
169
184
  export { CmdChannel, readJsonFromFile, writeJsonToFile } from './addon/chat-controller/cmd/CmdChannel.js';
170
185
  export { CmdChannelConfig } from './addon/chat-controller/cmd/CmdChannelConfig.js';
@@ -172,6 +187,21 @@ export { CmdChannelServer } from './addon/chat-controller/cmd/CmdChannelServer.j
172
187
  export { cmdChannelName } from './addon/chat-controller/cmd/cmdChannelName.js';
173
188
  export { cmdChannelSocketPath } from './addon/chat-controller/cmd/cmdChannelSocketPath.js';
174
189
  export { runCmdClient } from './addon/chat-controller/cmd/runCmdClient.js';
190
+ export { hubspotChannelName } from './addon/chat-controller/hubspot/hubspotChannelName.js';
191
+ export { HubSpotChannelConfig } from './addon/chat-controller/hubspot/HubSpotChannelConfig.js';
192
+ export { verifyHubSpotSignatureV3 } from './addon/chat-controller/hubspot/verifyHubSpotSignatureV3.js';
193
+ export { downloadHubSpotAttachments } from './addon/chat-controller/hubspot/downloadHubSpotAttachments.js';
194
+ export { HubSpotSender } from './addon/chat-controller/hubspot/HubSpotSender.js';
195
+ export { HubSpotWebhookController } from './addon/chat-controller/hubspot/HubSpotWebhookController.js';
196
+ export { HubSpotReceiver } from './addon/chat-controller/hubspot/HubSpotReceiver.js';
197
+ export { HubSpotChannel } from './addon/chat-controller/hubspot/HubSpotChannel.js';
198
+ export { markdownToChatHtml as markdownToHubSpotHtml, markdownToChatHtml as markdownToTelegramHtml } from './addon/chat-controller/markdown/markdownToChatHtml.js';
199
+ export { hubspot } from './addon/chat-controller/hubspot/@hubspot.js';
200
+ export { slack } from './addon/chat-controller/slack/@slack.js';
201
+ export { SlackChannelConfig } from './addon/chat-controller/slack/SlackChannelConfig.js';
202
+ export { SlackChannel } from './addon/chat-controller/slack/SlackChannel.js';
203
+ export { slackChannelName } from './addon/chat-controller/slack/slackChannelName.js';
204
+ export { markdownToSlackMrkdwn } from './addon/chat-controller/slack/markdownToSlackMrkdwn.js';
175
205
  export { socket } from './addon/chat-controller/socket/@socket.js';
176
206
  export { SocketChannel, SocketChannelMessageFile, SocketChannelReceivedMessage } from './addon/chat-controller/socket/SocketChannel.js';
177
207
  export { SocketChannelConfig } from './addon/chat-controller/socket/SocketChannelConfig.js';
@@ -180,7 +210,6 @@ export { telegram } from './addon/chat-controller/telegram/@telegram.js';
180
210
  export { TelegramChannelConfig } from './addon/chat-controller/telegram/TelegramChannelConfig.js';
181
211
  export { TelegramChannel } from './addon/chat-controller/telegram/TelegramChannel.js';
182
212
  export { telegramChannelName } from './addon/chat-controller/telegram/telegramChannelName.js';
183
- export { markdownToTelegramHtml } from './addon/chat-controller/telegram/markdownToTelegramHtml.js';
184
213
  export { WhatsAppReceiverByCloudApi } from './addon/chat-controller/whatsapp/cloud-api/WhatsAppReceiverByCloudApi.js';
185
214
  export { WhatsAppApiSender } from './addon/chat-controller/whatsapp/cloud-api/WhatsAppApiSender.js';
186
215
  export { kapso } from './addon/chat-controller/whatsapp/kapso/@kapso.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 };
@@ -0,0 +1,88 @@
1
+ import { Auth } from '../core/auth/Auth.js';
2
+ import { container } from '../core/injection/index.js';
3
+ import { ChatAdapter } from '../feature/chat-bot/ChatAdapter.js';
4
+ import '../feature/chat-bot/ChatAdapterRegistry.js';
5
+ import { ChatBot } from '../feature/chat-bot/ChatBot.js';
6
+ import { ChatMemory } from '../feature/chat-bot/ChatMemory.js';
7
+ import '../feature/chat-bot/ChatOperator.js';
8
+ import '../feature/chat-bot/UnionChatAdapter.js';
9
+ import '../feature/chat-bot/metadata/ChatAdapterMetadataStore.js';
10
+ import 'uuid';
11
+ import '../feature/chat-bot/metadata/ChatBotMetadataStore.js';
12
+ import '../core/error/setupErrorHandlers.js';
13
+ import '../feature/mindset/metadata/MindsetMetadataStore.js';
14
+ import { Mindset } from '../feature/mindset/IMindset.js';
15
+ import { MindsetOperator } from '../feature/mindset/MindsetOperator.js';
16
+ import { humanMessage } from './fixtures.js';
17
+ import { MockChatAdapter } from './MockChatAdapter.js';
18
+ import { TestChatMemory } from './TestChatMemory.js';
19
+ import { Container } from '../core/injection/Container.js';
20
+
21
+ /**
22
+ * Runs a real ChatBot (real MindsetOperator, system prompt, tool loop and
23
+ * argument validation) against an in-RAM memory and a pluggable adapter.
24
+ */
25
+ class ChatBotHarness {
26
+ adapter;
27
+ memory = new TestChatMemory();
28
+ container;
29
+ chatBot;
30
+ operator;
31
+ constructor(options) {
32
+ this.adapter = options.adapter ?? new MockChatAdapter();
33
+ const child = container.createChildContainer();
34
+ child.register(Container, { useValue: child });
35
+ child.register(Mindset, { useClass: options.mindset });
36
+ child.registerInstance(ChatMemory, this.memory);
37
+ child.registerInstance(ChatAdapter, this.adapter);
38
+ for (const [token, instance] of options.register ?? []) {
39
+ child.registerInstance(token, instance);
40
+ }
41
+ if (options.authInfo) {
42
+ const auth = child.resolve(Auth);
43
+ auth.assign(options.authInfo);
44
+ }
45
+ this.container = child;
46
+ this.chatBot = child.resolve(ChatBot);
47
+ this.operator = child.resolve(MindsetOperator);
48
+ }
49
+ /** Send a human message and collect everything the bot did in response. */
50
+ async send(message) {
51
+ const itemsBefore = this.memory.all().length;
52
+ const replies = [];
53
+ await this.chatBot.sendMessage(humanMessage(message), async (reply) => {
54
+ replies.push(reply);
55
+ });
56
+ const items = this.memory.allData().slice(itemsBefore);
57
+ const toolCalls = items
58
+ .filter((item) => item.type === 'functionCall')
59
+ .map((item) => item.functionCall);
60
+ return { replies, toolCalls, items };
61
+ }
62
+ /**
63
+ * Execute a single mindset tool directly (real argument validation and
64
+ * module resolution), without scripting a whole conversation.
65
+ * Returns the string result exactly as the LLM would receive it.
66
+ */
67
+ async callTool(name, args = {}) {
68
+ const argsJson = typeof args === 'string' ? args : JSON.stringify(args);
69
+ return await this.operator.callFunction(name, argsJson);
70
+ }
71
+ /** Full chat history (data form) accumulated across turns. */
72
+ history() {
73
+ return this.memory.allData();
74
+ }
75
+ /** The real system prompt the mindset produces. */
76
+ async systemPrompt() {
77
+ return await this.operator.systemPrompt();
78
+ }
79
+ /** The real tool definitions derived from the mindset modules. */
80
+ tools() {
81
+ return this.operator.tools();
82
+ }
83
+ }
84
+ function createChatBotHarness(options) {
85
+ return new ChatBotHarness(options);
86
+ }
87
+
88
+ export { ChatBotHarness, createChatBotHarness };