@romiluz/clawmongo 0.1.0-rc.0 → 0.1.0-rc.2

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 (124) hide show
  1. package/LICENSE +0 -1
  2. package/README.md +126 -1
  3. package/dist/cli/agent-smoke.js +249 -0
  4. package/dist/cli/automation-smoke.js +174 -0
  5. package/dist/cli/cli-chat-smoke.js +46 -0
  6. package/dist/cli/cli-command-smoke.js +103 -0
  7. package/dist/cli/cli-config-smoke.js +58 -0
  8. package/dist/cli/cli-health-smoke.js +78 -0
  9. package/dist/cli/cli-send-smoke.js +50 -0
  10. package/dist/cli/cli-sessions-smoke.js +59 -0
  11. package/dist/cli/commands/backup.js +142 -0
  12. package/dist/cli/commands/benchmark.js +151 -0
  13. package/dist/cli/commands/chat.js +123 -0
  14. package/dist/cli/commands/config.js +143 -0
  15. package/dist/cli/commands/cron.js +177 -0
  16. package/dist/cli/commands/health.js +117 -0
  17. package/dist/cli/commands/index.js +19 -0
  18. package/dist/cli/commands/output.js +105 -0
  19. package/dist/cli/commands/parser.js +116 -0
  20. package/dist/cli/commands/plugin.js +155 -0
  21. package/dist/cli/commands/registry.js +96 -0
  22. package/dist/cli/commands/security.js +138 -0
  23. package/dist/cli/commands/send.js +110 -0
  24. package/dist/cli/commands/sessions.js +203 -0
  25. package/dist/cli/commands/types.js +18 -0
  26. package/dist/cli/discord-connector-smoke.js +260 -0
  27. package/dist/cli/exec-tools-smoke.js +141 -0
  28. package/dist/cli/fs-tools-smoke.js +172 -0
  29. package/dist/cli/google-chat-connector-smoke.js +90 -0
  30. package/dist/cli/idempotency-smoke.js +219 -0
  31. package/dist/cli/imessage-connector-smoke.js +347 -0
  32. package/dist/cli/node-smoke.js +247 -0
  33. package/dist/cli/orchestrator-e2e-smoke.js +126 -0
  34. package/dist/cli/plugin-smoke.js +238 -0
  35. package/dist/cli/session-lifecycle-smoke.js +153 -0
  36. package/dist/cli/session-store-smoke.js +128 -0
  37. package/dist/cli/session-tools-smoke.js +162 -0
  38. package/dist/cli/slack-connector-smoke.js +92 -0
  39. package/dist/cli/sprint-checks.js +397 -1
  40. package/dist/cli/telegram-connector-smoke.js +215 -0
  41. package/dist/cli/whatsapp-connector-smoke.js +241 -0
  42. package/dist/cli/ws-gateway-smoke.js +93 -0
  43. package/dist/connectors/discord/index.js +10 -0
  44. package/dist/connectors/discord/normalize.js +134 -0
  45. package/dist/connectors/discord/outbound.js +121 -0
  46. package/dist/connectors/discord/types.js +14 -0
  47. package/dist/connectors/google-chat/index.js +56 -0
  48. package/dist/connectors/google-chat/normalize.js +126 -0
  49. package/dist/connectors/google-chat/outbound.js +117 -0
  50. package/dist/connectors/google-chat/types.js +7 -0
  51. package/dist/connectors/idempotency/index.js +10 -0
  52. package/dist/connectors/idempotency/retry.js +154 -0
  53. package/dist/connectors/idempotency/service.js +184 -0
  54. package/dist/connectors/idempotency/types.js +26 -0
  55. package/dist/connectors/imessage/index.js +78 -0
  56. package/dist/connectors/imessage/normalize.js +134 -0
  57. package/dist/connectors/imessage/outbound.js +138 -0
  58. package/dist/connectors/imessage/types.js +8 -0
  59. package/dist/connectors/slack/index.js +49 -0
  60. package/dist/connectors/slack/normalize.js +127 -0
  61. package/dist/connectors/slack/outbound.js +134 -0
  62. package/dist/connectors/slack/types.js +7 -0
  63. package/dist/connectors/telegram/index.js +9 -0
  64. package/dist/connectors/telegram/normalize.js +186 -0
  65. package/dist/connectors/telegram/outbound.js +108 -0
  66. package/dist/connectors/telegram/types.js +7 -0
  67. package/dist/connectors/whatsapp/index.js +9 -0
  68. package/dist/connectors/whatsapp/normalize.js +148 -0
  69. package/dist/connectors/whatsapp/outbound.js +126 -0
  70. package/dist/connectors/whatsapp/types.js +7 -0
  71. package/dist/http/health.js +152 -0
  72. package/dist/http/ratelimit.js +131 -0
  73. package/dist/lifecycle/shutdown.js +143 -0
  74. package/dist/main.js +85 -87
  75. package/dist/modules/agent/history.js +132 -0
  76. package/dist/modules/agent/index.js +16 -0
  77. package/dist/modules/agent/loop.js +177 -0
  78. package/dist/modules/agent/service.js +114 -0
  79. package/dist/modules/agent/types.js +17 -0
  80. package/dist/modules/automation/cron/index.js +24 -0
  81. package/dist/modules/automation/cron/scheduler.js +177 -0
  82. package/dist/modules/automation/cron/store.js +118 -0
  83. package/dist/modules/automation/cron/types.js +14 -0
  84. package/dist/modules/automation/hooks/executor.js +178 -0
  85. package/dist/modules/automation/hooks/index.js +25 -0
  86. package/dist/modules/automation/hooks/lifecycle.js +116 -0
  87. package/dist/modules/automation/hooks/types.js +20 -0
  88. package/dist/modules/automation/index.js +23 -0
  89. package/dist/modules/gateway/ws.js +97 -0
  90. package/dist/modules/node/executor.js +191 -0
  91. package/dist/modules/node/index.js +33 -0
  92. package/dist/modules/node/pairing.js +140 -0
  93. package/dist/modules/node/store.js +98 -0
  94. package/dist/modules/node/types.js +16 -0
  95. package/dist/modules/plugin/cli.js +146 -0
  96. package/dist/modules/plugin/hooks.js +139 -0
  97. package/dist/modules/plugin/index.js +49 -0
  98. package/dist/modules/plugin/lifecycle.js +136 -0
  99. package/dist/modules/plugin/loader.js +143 -0
  100. package/dist/modules/plugin/marketplace/client.js +148 -0
  101. package/dist/modules/plugin/marketplace/index.js +13 -0
  102. package/dist/modules/plugin/marketplace/installer.js +157 -0
  103. package/dist/modules/plugin/marketplace/search.js +145 -0
  104. package/dist/modules/plugin/marketplace/types.js +13 -0
  105. package/dist/modules/plugin/store.js +112 -0
  106. package/dist/modules/plugin/tools.js +117 -0
  107. package/dist/modules/plugin/types.js +9 -0
  108. package/dist/modules/provider-adapter/service.js +95 -1
  109. package/dist/modules/tool-runtime/executors/exec.js +155 -0
  110. package/dist/modules/tool-runtime/executors/filesystem.js +385 -0
  111. package/dist/modules/tool-runtime/executors/index.js +10 -0
  112. package/dist/modules/tool-runtime/executors/process.js +243 -0
  113. package/dist/modules/tool-runtime/executors/session.js +257 -0
  114. package/dist/modules/tool-runtime/executors/types.js +6 -0
  115. package/dist/modules/tool-runtime/service.js +101 -1
  116. package/dist/observability/metrics.js +151 -0
  117. package/dist/session/index.js +9 -0
  118. package/dist/session/search.js +155 -0
  119. package/dist/session/service.js +277 -0
  120. package/dist/session/store.js +281 -0
  121. package/dist/session/types.js +20 -0
  122. package/dist/store/mongo/optimizer.js +133 -0
  123. package/dist/store/mongo/pool.js +156 -0
  124. package/package.json +26 -1
package/LICENSE CHANGED
@@ -19,4 +19,3 @@ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
19
  LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
20
  OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
21
  SOFTWARE.
22
-
package/README.md CHANGED
@@ -1,3 +1,128 @@
1
1
  # ClawMongo
2
2
 
3
- A new project created with Intent by Augment.
3
+ > **MongoDB is the brain, OpenClaw is the heart.**
4
+
5
+ ClawMongo is a MongoDB-native fork of [OpenClaw](https://github.com/TavernAI/OpenClaw) that leverages MongoDB's capabilities (Atlas Search, Vector Search, `$rankFusion`) while maintaining full parity with OpenClaw's features.
6
+
7
+ [![npm version](https://badge.fury.io/js/@romiluz%2Fclawmongo.svg)](https://www.npmjs.com/package/@romiluz/clawmongo)
8
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
9
+
10
+ ## Features
11
+
12
+ - **MongoDB-Native Storage** - All data stored in MongoDB with proper indexing and validation
13
+ - **Atlas Search** - Full-text search with autocomplete for sessions and plugins
14
+ - **Vector Search** - `$vectorSearch` with embedding providers (Voyage AI, OpenAI)
15
+ - **$rankFusion** - Native MongoDB fusion of lexical and vector results
16
+ - **7 Messaging Connectors** - WhatsApp, Telegram, Discord, Slack, Google Chat, iMessage, WebSocket
17
+ - **Plugin Framework** - Install plugins from npm with dependency resolution
18
+ - **Automation Engine** - Cron scheduling and event-driven hooks
19
+ - **Production Ready** - Health checks, metrics, rate limiting, graceful shutdown, Docker
20
+
21
+ ## Installation
22
+
23
+ ```bash
24
+ npm install @romiluz/clawmongo
25
+ ```
26
+
27
+ Or use the RC version:
28
+
29
+ ```bash
30
+ npm install @romiluz/clawmongo@next
31
+ ```
32
+
33
+ ## Quick Start
34
+
35
+ ```bash
36
+ # Check health
37
+ npx clawmongo health
38
+
39
+ # Start interactive chat
40
+ npx clawmongo chat
41
+
42
+ # Send a message
43
+ npx clawmongo send "Hello, ClawMongo!"
44
+
45
+ # List sessions
46
+ npx clawmongo sessions list
47
+ ```
48
+
49
+ ## Configuration
50
+
51
+ Set environment variables or use a `.env` file:
52
+
53
+ ```bash
54
+ # Required for MongoDB features
55
+ CLAWMONGO_MONGODB_URI=mongodb+srv://...
56
+
57
+ # LLM Provider (optional - defaults to stub)
58
+ ANTHROPIC_API_KEY=sk-...
59
+ OPENAI_API_KEY=sk-...
60
+
61
+ # Embedding Provider
62
+ VOYAGE_API_KEY=pa-...
63
+ ```
64
+
65
+ See [docs/ENV.md](docs/ENV.md) for full configuration reference.
66
+
67
+ ## Docker
68
+
69
+ ```bash
70
+ # Build and run
71
+ docker-compose up -d
72
+
73
+ # Or build manually
74
+ docker build -t clawmongo .
75
+ docker run -p 3000:3000 --env-file .env clawmongo
76
+ ```
77
+
78
+ See [docs/DEPLOYMENT.md](docs/DEPLOYMENT.md) for production deployment guide.
79
+
80
+ ## OpenClaw Parity
81
+
82
+ ClawMongo maintains 60 capability parity with OpenClaw:
83
+
84
+ | Category | Capabilities |
85
+ |----------|-------------|
86
+ | Sessions | 10 (lifecycle, scopes, reset) |
87
+ | Retrieval | 12 (lexical, vector, fusion) |
88
+ | Tools | 10 (exec, fs, session) |
89
+ | Connectors | 8 (WhatsApp, Telegram, Discord, Slack, etc.) |
90
+ | Plugins | 10 (lifecycle, hooks, tools) |
91
+ | Automation | 10 (cron, hooks, CLI) |
92
+
93
+ ## Development
94
+
95
+ ```bash
96
+ # Install dependencies
97
+ npm install
98
+
99
+ # Run type check
100
+ npm run typecheck
101
+
102
+ # Run health check
103
+ npm run health
104
+
105
+ # Run sprint checks
106
+ npm run sprint:checks
107
+ ```
108
+
109
+ ## Architecture
110
+
111
+ ClawMongo is organized into modules:
112
+
113
+ - `src/session/` - Session management
114
+ - `src/retrieval/` - Lexical, vector, and fusion search
115
+ - `src/modules/` - Agent, plugin, automation, gateway
116
+ - `src/connectors/` - Messaging platform connectors
117
+ - `src/cli/` - CLI commands and smoke tests
118
+ - `src/store/mongo/` - MongoDB connection and optimization
119
+
120
+ ## License
121
+
122
+ MIT - see [LICENSE](LICENSE)
123
+
124
+ ## Credits
125
+
126
+ - [OpenClaw](https://github.com/TavernAI/OpenClaw) - The original project
127
+ - [MongoDB](https://www.mongodb.com/) - The database
128
+ - [Voyage AI](https://www.voyageai.com/) - Default embedding provider
@@ -0,0 +1,249 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Agent Module Smoke Tests
4
+ *
5
+ * Tests the core agent module without requiring Anthropic API calls.
6
+ * Tests types, history, tool execution, and service instantiation.
7
+ *
8
+ * @module cli/agent-smoke
9
+ */
10
+ import { DEFAULT_MODEL_CONFIG, MessageHistory, executeTools, Agent, createAgent } from "../modules/agent/index.js";
11
+ const results = [];
12
+ function test(name, fn) {
13
+ try {
14
+ const result = fn();
15
+ if (result instanceof Promise) {
16
+ result
17
+ .then(() => results.push({ name, passed: true }))
18
+ .catch((e) => results.push({ name, passed: false, error: String(e) }));
19
+ }
20
+ else {
21
+ results.push({ name, passed: true });
22
+ }
23
+ }
24
+ catch (e) {
25
+ results.push({ name, passed: false, error: String(e) });
26
+ }
27
+ }
28
+ function assert(condition, message) {
29
+ if (!condition) {
30
+ throw new Error(`Assertion failed: ${message}`);
31
+ }
32
+ }
33
+ // ============================================================
34
+ // Test 1: Default model config
35
+ // ============================================================
36
+ test("DEFAULT_MODEL_CONFIG has expected values", () => {
37
+ assert(DEFAULT_MODEL_CONFIG.model === "claude-sonnet-4-20250514", "model should be claude-sonnet-4-20250514");
38
+ assert(DEFAULT_MODEL_CONFIG.maxTokens === 4096, "maxTokens should be 4096");
39
+ assert(DEFAULT_MODEL_CONFIG.temperature === 1.0, "temperature should be 1.0");
40
+ assert(DEFAULT_MODEL_CONFIG.contextWindowTokens === 180000, "contextWindowTokens should be 180000");
41
+ });
42
+ // ============================================================
43
+ // Test 2: Tool interface
44
+ // ============================================================
45
+ test("Tool interface is valid", () => {
46
+ const tool = {
47
+ name: "test_tool",
48
+ description: "A test tool",
49
+ inputSchema: {
50
+ type: "object",
51
+ properties: {
52
+ input: { type: "string", description: "Test input" }
53
+ },
54
+ required: ["input"]
55
+ },
56
+ execute: async (params) => `Executed with: ${JSON.stringify(params)}`
57
+ };
58
+ assert(tool.name === "test_tool", "tool name should match");
59
+ assert(tool.description === "A test tool", "tool description should match");
60
+ assert(tool.inputSchema.type === "object", "inputSchema type should be object");
61
+ });
62
+ // ============================================================
63
+ // Test 3: Tool execution
64
+ // ============================================================
65
+ test("executeTools executes tools correctly", async () => {
66
+ const mockTool = {
67
+ name: "echo",
68
+ description: "Echo input",
69
+ inputSchema: { type: "object", properties: {} },
70
+ execute: async (params) => `Echo: ${JSON.stringify(params)}`
71
+ };
72
+ const toolDict = new Map([["echo", mockTool]]);
73
+ const toolCalls = [
74
+ { id: "call_1", name: "echo", input: { message: "hello" } }
75
+ ];
76
+ const results = await executeTools(toolCalls, toolDict);
77
+ assert(results.length === 1, "should return 1 result");
78
+ const first = results[0];
79
+ assert(first.toolUseId === "call_1", "toolUseId should match");
80
+ assert(first.content.includes("hello"), "content should include input");
81
+ assert(first.isError !== true, "should not be error");
82
+ });
83
+ // ============================================================
84
+ // Test 4: Tool not found handling
85
+ // ============================================================
86
+ test("executeTools handles missing tool", async () => {
87
+ const toolDict = new Map();
88
+ const toolCalls = [
89
+ { id: "call_1", name: "nonexistent", input: {} }
90
+ ];
91
+ const results = await executeTools(toolCalls, toolDict);
92
+ assert(results.length === 1, "should return 1 result");
93
+ const first = results[0];
94
+ assert(first.isError === true, "should be error");
95
+ assert(first.content.includes("not found"), "should mention not found");
96
+ });
97
+ // ============================================================
98
+ // Test 5: Tool execution error handling
99
+ // ============================================================
100
+ test("executeTools handles tool errors", async () => {
101
+ const errorTool = {
102
+ name: "error_tool",
103
+ description: "Always errors",
104
+ inputSchema: { type: "object", properties: {} },
105
+ execute: async () => { throw new Error("Intentional error"); }
106
+ };
107
+ const toolDict = new Map([["error_tool", errorTool]]);
108
+ const toolCalls = [
109
+ { id: "call_1", name: "error_tool", input: {} }
110
+ ];
111
+ const results = await executeTools(toolCalls, toolDict);
112
+ assert(results.length === 1, "should return 1 result");
113
+ const first = results[0];
114
+ assert(first.isError === true, "should be error");
115
+ assert(first.content.includes("Intentional error"), "should include error message");
116
+ });
117
+ // ============================================================
118
+ // Test 6: Parallel tool execution
119
+ // ============================================================
120
+ test("executeTools runs tools in parallel", async () => {
121
+ const executionOrder = [];
122
+ const slowTool = {
123
+ name: "slow",
124
+ description: "Slow tool",
125
+ inputSchema: { type: "object", properties: {} },
126
+ execute: async () => {
127
+ await new Promise(r => setTimeout(r, 50));
128
+ executionOrder.push("slow");
129
+ return "slow done";
130
+ }
131
+ };
132
+ const fastTool = {
133
+ name: "fast",
134
+ description: "Fast tool",
135
+ inputSchema: { type: "object", properties: {} },
136
+ execute: async () => {
137
+ executionOrder.push("fast");
138
+ return "fast done";
139
+ }
140
+ };
141
+ const toolDict = new Map([["slow", slowTool], ["fast", fastTool]]);
142
+ const toolCalls = [
143
+ { id: "call_1", name: "slow", input: {} },
144
+ { id: "call_2", name: "fast", input: {} }
145
+ ];
146
+ await executeTools(toolCalls, toolDict, true);
147
+ // In parallel, fast should complete before slow
148
+ assert(executionOrder[0] === "fast", "fast should execute first in parallel");
149
+ assert(executionOrder[1] === "slow", "slow should execute second in parallel");
150
+ });
151
+ // ============================================================
152
+ // Test 7: AgentConfig validation
153
+ // ============================================================
154
+ test("AgentConfig is valid", () => {
155
+ const config = {
156
+ name: "TestAgent",
157
+ systemPrompt: "You are a helpful assistant.",
158
+ tools: [],
159
+ modelConfig: DEFAULT_MODEL_CONFIG,
160
+ verbose: false
161
+ };
162
+ assert(config.name === "TestAgent", "name should match");
163
+ assert(config.systemPrompt.includes("helpful"), "systemPrompt should include helpful");
164
+ });
165
+ // ============================================================
166
+ // Test 8: createAgent factory function
167
+ // ============================================================
168
+ test("createAgent creates agent instance", () => {
169
+ // Note: This test will fail without ANTHROPIC_API_KEY
170
+ // We're testing the factory function structure, not actual API calls
171
+ const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
172
+ if (!hasApiKey) {
173
+ // Skip actual instantiation without API key
174
+ console.log(" [skipped - no ANTHROPIC_API_KEY]");
175
+ return;
176
+ }
177
+ const agent = createAgent({
178
+ name: "TestAgent",
179
+ systemPrompt: "You are a test agent."
180
+ });
181
+ assert(agent.getName() === "TestAgent", "agent name should match");
182
+ assert(agent.getTools().length === 0, "agent should have no tools initially");
183
+ });
184
+ // ============================================================
185
+ // Test 9: Agent tool registration
186
+ // ============================================================
187
+ test("Agent.registerTool adds tools", () => {
188
+ const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
189
+ if (!hasApiKey) {
190
+ console.log(" [skipped - no ANTHROPIC_API_KEY]");
191
+ return;
192
+ }
193
+ const agent = createAgent({
194
+ name: "TestAgent",
195
+ systemPrompt: "Test"
196
+ });
197
+ const tool = {
198
+ name: "test_tool",
199
+ description: "Test",
200
+ inputSchema: { type: "object", properties: {} },
201
+ execute: async () => "done"
202
+ };
203
+ agent.registerTool(tool);
204
+ assert(agent.getTools().length === 1, "agent should have 1 tool");
205
+ assert(agent.getTools()[0]?.name === "test_tool", "tool name should match");
206
+ });
207
+ // ============================================================
208
+ // Test 10: Agent token count tracking
209
+ // ============================================================
210
+ test("Agent.getTokenCount returns initial count", () => {
211
+ const hasApiKey = !!process.env.ANTHROPIC_API_KEY;
212
+ if (!hasApiKey) {
213
+ console.log(" [skipped - no ANTHROPIC_API_KEY]");
214
+ return;
215
+ }
216
+ const agent = createAgent({
217
+ name: "TestAgent",
218
+ systemPrompt: "A short system prompt."
219
+ });
220
+ const tokenCount = agent.getTokenCount();
221
+ assert(tokenCount > 0, "token count should be > 0");
222
+ assert(tokenCount < 100, "token count for short prompt should be < 100");
223
+ });
224
+ // ============================================================
225
+ // Run all tests and report
226
+ // ============================================================
227
+ async function runTests() {
228
+ // Wait for async tests to complete
229
+ await new Promise(r => setTimeout(r, 200));
230
+ console.log("\n=== Agent Smoke Tests ===\n");
231
+ let passed = 0;
232
+ let failed = 0;
233
+ for (const result of results) {
234
+ if (result.passed) {
235
+ console.log(`✅ ${result.name}`);
236
+ passed++;
237
+ }
238
+ else {
239
+ console.log(`❌ ${result.name}`);
240
+ console.log(` Error: ${result.error}`);
241
+ failed++;
242
+ }
243
+ }
244
+ console.log(`\n📊 Results: ${passed} passed, ${failed} failed\n`);
245
+ if (failed > 0) {
246
+ process.exit(1);
247
+ }
248
+ }
249
+ runTests().catch(console.error);
@@ -0,0 +1,174 @@
1
+ /**
2
+ * Automation Module Smoke Tests
3
+ *
4
+ * Validates cron and hooks functionality for Sprint 07.
5
+ */
6
+ import { parseCronExpression, getNextRunTime, getCronRegistry, resetCronRegistry } from "../modules/automation/cron/index.js";
7
+ import { getHookRegistry, getHookHandlerRegistry, getHookExecutor, resetHookServices } from "../modules/automation/hooks/index.js";
8
+ const results = [];
9
+ function test(name, fn) {
10
+ try {
11
+ const result = fn();
12
+ if (result instanceof Promise) {
13
+ result
14
+ .then(() => results.push({ name, passed: true }))
15
+ .catch((err) => results.push({ name, passed: false, error: String(err) }));
16
+ }
17
+ else {
18
+ results.push({ name, passed: true });
19
+ }
20
+ }
21
+ catch (err) {
22
+ results.push({ name, passed: false, error: String(err) });
23
+ }
24
+ }
25
+ function assert(condition, msg) {
26
+ if (!condition)
27
+ throw new Error(msg);
28
+ }
29
+ // Cron tests
30
+ test("parseCronExpression parses valid expression", () => {
31
+ const parts = parseCronExpression("0 9 * * 1");
32
+ assert(parts !== null, "Should parse valid expression");
33
+ assert(parts.minute.includes(0), "Minute should be 0");
34
+ assert(parts.hour.includes(9), "Hour should be 9");
35
+ assert(parts.dayOfWeek.includes(1), "Day of week should be Monday");
36
+ });
37
+ test("parseCronExpression rejects invalid expression", () => {
38
+ const parts = parseCronExpression("invalid");
39
+ assert(parts === null, "Should reject invalid expression");
40
+ });
41
+ test("parseCronExpression handles ranges", () => {
42
+ const parts = parseCronExpression("0-30 * * * *");
43
+ assert(parts !== null, "Should parse range");
44
+ assert(parts.minute.length === 31, "Should have 31 minutes (0-30)");
45
+ });
46
+ test("parseCronExpression handles steps", () => {
47
+ const parts = parseCronExpression("*/15 * * * *");
48
+ assert(parts !== null, "Should parse step");
49
+ if (!parts)
50
+ throw new Error("Parts should not be null");
51
+ assert(parts.minute.includes(0), "Should include 0");
52
+ assert(parts.minute.includes(15), "Should include 15");
53
+ assert(parts.minute.includes(30), "Should include 30");
54
+ assert(parts.minute.includes(45), "Should include 45");
55
+ });
56
+ test("getNextRunTime calculates next run for 'at' schedule", () => {
57
+ const future = new Date(Date.now() + 60000);
58
+ const schedule = { type: "at", datetime: future };
59
+ const next = getNextRunTime(schedule);
60
+ assert(next !== null, "Should return next run time");
61
+ assert(next.getTime() === future.getTime(), "Should match datetime");
62
+ });
63
+ test("getNextRunTime calculates next run for 'every' schedule", () => {
64
+ const schedule = { type: "every", interval: 5, unit: "minutes" };
65
+ const now = new Date();
66
+ const next = getNextRunTime(schedule, now);
67
+ assert(next !== null, "Should return next run time");
68
+ assert(next.getTime() > now.getTime(), "Should be in future");
69
+ });
70
+ test("InMemoryCronRegistry can create and list jobs", async () => {
71
+ resetCronRegistry();
72
+ const registry = getCronRegistry();
73
+ const job = await registry.create({
74
+ name: "Test Job",
75
+ schedule: { type: "every", interval: 1, unit: "hours" },
76
+ handler: "test:handler"
77
+ });
78
+ assert(job.id.startsWith("cron-"), "Job ID should have prefix");
79
+ assert(registry.list().length === 1, "Should have 1 job");
80
+ });
81
+ test("InMemoryCronRegistry can pause and resume jobs", async () => {
82
+ resetCronRegistry();
83
+ const registry = getCronRegistry();
84
+ const job = await registry.create({
85
+ name: "Pausable Job",
86
+ schedule: { type: "every", interval: 1, unit: "days" },
87
+ handler: "test:handler"
88
+ });
89
+ await registry.pause(job.id);
90
+ assert(registry.get(job.id)?.state === "paused", "Should be paused");
91
+ await registry.resume(job.id);
92
+ assert(registry.get(job.id)?.state === "active", "Should be active");
93
+ });
94
+ // Hook tests
95
+ test("InMemoryHookRegistry can register and list hooks", async () => {
96
+ resetHookServices();
97
+ const registry = getHookRegistry();
98
+ const hook = await registry.register({
99
+ name: "Test Hook",
100
+ event: "message:received",
101
+ handler: "test:on-message"
102
+ });
103
+ assert(hook.id.startsWith("hook-"), "Hook ID should have prefix");
104
+ assert(registry.list().length === 1, "Should have 1 hook");
105
+ });
106
+ test("InMemoryHookRegistry listForEvent returns sorted hooks", async () => {
107
+ resetHookServices();
108
+ const registry = getHookRegistry();
109
+ await registry.register({ name: "Low", event: "session:start", handler: "h1", priority: "low" });
110
+ await registry.register({ name: "High", event: "session:start", handler: "h2", priority: "high" });
111
+ await registry.register({ name: "Normal", event: "session:start", handler: "h3", priority: "normal" });
112
+ const hooks = registry.listForEvent("session:start");
113
+ assert(hooks.length === 3, "Should have 3 hooks");
114
+ const first = hooks[0];
115
+ assert(Boolean(first && first.priority === "high"), "First should be high priority");
116
+ const second = hooks[1];
117
+ assert(Boolean(second && second.priority === "normal"), "Second should be normal");
118
+ const third = hooks[2];
119
+ assert(Boolean(third && third.priority === "low"), "Third should be low");
120
+ });
121
+ test("InMemoryHookExecutor emits events and executes handlers", async () => {
122
+ resetHookServices();
123
+ const registry = getHookRegistry();
124
+ const handlerRegistry = getHookHandlerRegistry();
125
+ const executor = getHookExecutor();
126
+ let executed = false;
127
+ const handler = {
128
+ id: "test:handler",
129
+ name: "Test Handler",
130
+ execute: async () => { executed = true; return "ok"; }
131
+ };
132
+ handlerRegistry.register(handler);
133
+ await registry.register({ name: "Emit Test", event: "gateway:startup", handler: "test:handler" });
134
+ const emitResults = await executor.emit("gateway:startup", { data: {} });
135
+ assert(emitResults.length === 1, "Should have 1 result");
136
+ const first = emitResults[0];
137
+ assert(Boolean(first && first.success), "Should succeed");
138
+ assert(executed, "Handler should have executed");
139
+ });
140
+ test("InMemoryHookExecutor skips hooks with filter mismatch", async () => {
141
+ resetHookServices();
142
+ const registry = getHookRegistry();
143
+ const executor = getHookExecutor();
144
+ await registry.register({
145
+ name: "Filtered Hook",
146
+ event: "message:received",
147
+ handler: "test:handler",
148
+ filter: { channels: ["whatsapp"] }
149
+ });
150
+ const emitResults = await executor.emit("message:received", { channelId: "telegram", data: {} });
151
+ assert(emitResults.length === 1, "Should have 1 result");
152
+ const result = emitResults[0];
153
+ assert(Boolean(result && result.skipped), "Should be skipped");
154
+ assert(Boolean(result && result.skipReason === "Channel not in filter"), "Should have skip reason");
155
+ });
156
+ // Print results
157
+ setTimeout(() => {
158
+ console.log("\n=== Automation Module Smoke Tests ===\n");
159
+ let passed = 0;
160
+ let failed = 0;
161
+ for (const r of results) {
162
+ if (r.passed) {
163
+ console.log(`✓ ${r.name}`);
164
+ passed++;
165
+ }
166
+ else {
167
+ console.log(`✗ ${r.name}`);
168
+ console.log(` Error: ${r.error}`);
169
+ failed++;
170
+ }
171
+ }
172
+ console.log(`\n${passed}/${passed + failed} tests passed`);
173
+ process.exit(failed > 0 ? 1 : 0);
174
+ }, 100);
@@ -0,0 +1,46 @@
1
+ /**
2
+ * CLI Chat Command Smoke Test
3
+ *
4
+ * Validates S5-004: CLI chat command
5
+ */
6
+ import { chatCommand } from "./commands/chat.js";
7
+ const results = [];
8
+ function test(name, fn) {
9
+ try {
10
+ fn();
11
+ results.push({ name, passed: true });
12
+ }
13
+ catch (err) {
14
+ results.push({ name, passed: false, error: err instanceof Error ? err.message : String(err) });
15
+ }
16
+ }
17
+ function assert(condition, message) {
18
+ if (!condition)
19
+ throw new Error(message);
20
+ }
21
+ // Test chat command structure
22
+ test("chatCommand has correct name", () => {
23
+ assert(chatCommand.name === "chat", "Expected name 'chat'");
24
+ });
25
+ test("chatCommand has description", () => {
26
+ assert(chatCommand.description.length > 0, "Expected description");
27
+ });
28
+ test("chatCommand has execute function", () => {
29
+ assert(typeof chatCommand.execute === "function", "Expected execute function");
30
+ });
31
+ test("chatCommand has usage string", () => {
32
+ assert(typeof chatCommand.usage === "string", "Expected usage string");
33
+ assert(chatCommand.usage.length > 0, "Expected non-empty usage");
34
+ });
35
+ test("chatCommand has aliases (optional)", () => {
36
+ // Aliases are optional but should be an array if present
37
+ if (chatCommand.aliases) {
38
+ assert(Array.isArray(chatCommand.aliases), "Expected aliases to be array if present");
39
+ }
40
+ assert(true, "Aliases check passed");
41
+ });
42
+ // Output results
43
+ const passed = results.filter((r) => r.passed).length;
44
+ const failed = results.filter((r) => !r.passed).length;
45
+ console.log(JSON.stringify({ tests: results.length, passed, failed, results }, null, 2));
46
+ process.exitCode = failed > 0 ? 1 : 0;