@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.
- package/LICENSE +0 -1
- package/README.md +126 -1
- package/dist/cli/agent-smoke.js +249 -0
- package/dist/cli/automation-smoke.js +174 -0
- package/dist/cli/cli-chat-smoke.js +46 -0
- package/dist/cli/cli-command-smoke.js +103 -0
- package/dist/cli/cli-config-smoke.js +58 -0
- package/dist/cli/cli-health-smoke.js +78 -0
- package/dist/cli/cli-send-smoke.js +50 -0
- package/dist/cli/cli-sessions-smoke.js +59 -0
- package/dist/cli/commands/backup.js +142 -0
- package/dist/cli/commands/benchmark.js +151 -0
- package/dist/cli/commands/chat.js +123 -0
- package/dist/cli/commands/config.js +143 -0
- package/dist/cli/commands/cron.js +177 -0
- package/dist/cli/commands/health.js +117 -0
- package/dist/cli/commands/index.js +19 -0
- package/dist/cli/commands/output.js +105 -0
- package/dist/cli/commands/parser.js +116 -0
- package/dist/cli/commands/plugin.js +155 -0
- package/dist/cli/commands/registry.js +96 -0
- package/dist/cli/commands/security.js +138 -0
- package/dist/cli/commands/send.js +110 -0
- package/dist/cli/commands/sessions.js +203 -0
- package/dist/cli/commands/types.js +18 -0
- package/dist/cli/discord-connector-smoke.js +260 -0
- package/dist/cli/exec-tools-smoke.js +141 -0
- package/dist/cli/fs-tools-smoke.js +172 -0
- package/dist/cli/google-chat-connector-smoke.js +90 -0
- package/dist/cli/idempotency-smoke.js +219 -0
- package/dist/cli/imessage-connector-smoke.js +347 -0
- package/dist/cli/node-smoke.js +247 -0
- package/dist/cli/orchestrator-e2e-smoke.js +126 -0
- package/dist/cli/plugin-smoke.js +238 -0
- package/dist/cli/session-lifecycle-smoke.js +153 -0
- package/dist/cli/session-store-smoke.js +128 -0
- package/dist/cli/session-tools-smoke.js +162 -0
- package/dist/cli/slack-connector-smoke.js +92 -0
- package/dist/cli/sprint-checks.js +397 -1
- package/dist/cli/telegram-connector-smoke.js +215 -0
- package/dist/cli/whatsapp-connector-smoke.js +241 -0
- package/dist/cli/ws-gateway-smoke.js +93 -0
- package/dist/connectors/discord/index.js +10 -0
- package/dist/connectors/discord/normalize.js +134 -0
- package/dist/connectors/discord/outbound.js +121 -0
- package/dist/connectors/discord/types.js +14 -0
- package/dist/connectors/google-chat/index.js +56 -0
- package/dist/connectors/google-chat/normalize.js +126 -0
- package/dist/connectors/google-chat/outbound.js +117 -0
- package/dist/connectors/google-chat/types.js +7 -0
- package/dist/connectors/idempotency/index.js +10 -0
- package/dist/connectors/idempotency/retry.js +154 -0
- package/dist/connectors/idempotency/service.js +184 -0
- package/dist/connectors/idempotency/types.js +26 -0
- package/dist/connectors/imessage/index.js +78 -0
- package/dist/connectors/imessage/normalize.js +134 -0
- package/dist/connectors/imessage/outbound.js +138 -0
- package/dist/connectors/imessage/types.js +8 -0
- package/dist/connectors/slack/index.js +49 -0
- package/dist/connectors/slack/normalize.js +127 -0
- package/dist/connectors/slack/outbound.js +134 -0
- package/dist/connectors/slack/types.js +7 -0
- package/dist/connectors/telegram/index.js +9 -0
- package/dist/connectors/telegram/normalize.js +186 -0
- package/dist/connectors/telegram/outbound.js +108 -0
- package/dist/connectors/telegram/types.js +7 -0
- package/dist/connectors/whatsapp/index.js +9 -0
- package/dist/connectors/whatsapp/normalize.js +148 -0
- package/dist/connectors/whatsapp/outbound.js +126 -0
- package/dist/connectors/whatsapp/types.js +7 -0
- package/dist/http/health.js +152 -0
- package/dist/http/ratelimit.js +131 -0
- package/dist/lifecycle/shutdown.js +143 -0
- package/dist/main.js +85 -87
- package/dist/modules/agent/history.js +132 -0
- package/dist/modules/agent/index.js +16 -0
- package/dist/modules/agent/loop.js +177 -0
- package/dist/modules/agent/service.js +114 -0
- package/dist/modules/agent/types.js +17 -0
- package/dist/modules/automation/cron/index.js +24 -0
- package/dist/modules/automation/cron/scheduler.js +177 -0
- package/dist/modules/automation/cron/store.js +118 -0
- package/dist/modules/automation/cron/types.js +14 -0
- package/dist/modules/automation/hooks/executor.js +178 -0
- package/dist/modules/automation/hooks/index.js +25 -0
- package/dist/modules/automation/hooks/lifecycle.js +116 -0
- package/dist/modules/automation/hooks/types.js +20 -0
- package/dist/modules/automation/index.js +23 -0
- package/dist/modules/gateway/ws.js +97 -0
- package/dist/modules/node/executor.js +191 -0
- package/dist/modules/node/index.js +33 -0
- package/dist/modules/node/pairing.js +140 -0
- package/dist/modules/node/store.js +98 -0
- package/dist/modules/node/types.js +16 -0
- package/dist/modules/plugin/cli.js +146 -0
- package/dist/modules/plugin/hooks.js +139 -0
- package/dist/modules/plugin/index.js +49 -0
- package/dist/modules/plugin/lifecycle.js +136 -0
- package/dist/modules/plugin/loader.js +143 -0
- package/dist/modules/plugin/marketplace/client.js +148 -0
- package/dist/modules/plugin/marketplace/index.js +13 -0
- package/dist/modules/plugin/marketplace/installer.js +157 -0
- package/dist/modules/plugin/marketplace/search.js +145 -0
- package/dist/modules/plugin/marketplace/types.js +13 -0
- package/dist/modules/plugin/store.js +112 -0
- package/dist/modules/plugin/tools.js +117 -0
- package/dist/modules/plugin/types.js +9 -0
- package/dist/modules/provider-adapter/service.js +95 -1
- package/dist/modules/tool-runtime/executors/exec.js +155 -0
- package/dist/modules/tool-runtime/executors/filesystem.js +385 -0
- package/dist/modules/tool-runtime/executors/index.js +10 -0
- package/dist/modules/tool-runtime/executors/process.js +243 -0
- package/dist/modules/tool-runtime/executors/session.js +257 -0
- package/dist/modules/tool-runtime/executors/types.js +6 -0
- package/dist/modules/tool-runtime/service.js +101 -1
- package/dist/observability/metrics.js +151 -0
- package/dist/session/index.js +9 -0
- package/dist/session/search.js +155 -0
- package/dist/session/service.js +277 -0
- package/dist/session/store.js +281 -0
- package/dist/session/types.js +20 -0
- package/dist/store/mongo/optimizer.js +133 -0
- package/dist/store/mongo/pool.js +156 -0
- package/package.json +26 -1
package/LICENSE
CHANGED
package/README.md
CHANGED
|
@@ -1,3 +1,128 @@
|
|
|
1
1
|
# ClawMongo
|
|
2
2
|
|
|
3
|
-
|
|
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
|
+
[](https://www.npmjs.com/package/@romiluz/clawmongo)
|
|
8
|
+
[](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;
|