@bjlee2024/claude-mem 13.4.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.
- package/.agents/plugins/marketplace.json +20 -0
- package/.codex-plugin/plugin.json +46 -0
- package/LICENSE +202 -0
- package/README.md +419 -0
- package/dist/npx-cli/index.js +10001 -0
- package/dist/opencode-plugin/index.js +67 -0
- package/openclaw/Dockerfile.e2e +46 -0
- package/openclaw/SKILL.md +462 -0
- package/openclaw/TESTING.md +279 -0
- package/openclaw/dist/index.js +15 -0
- package/openclaw/e2e-verify.sh +222 -0
- package/openclaw/install.sh +1653 -0
- package/openclaw/openclaw.plugin.json +98 -0
- package/openclaw/package.json +21 -0
- package/openclaw/src/index.test.ts +1124 -0
- package/openclaw/src/index.ts +1092 -0
- package/openclaw/test-e2e.sh +40 -0
- package/openclaw/test-install.sh +2086 -0
- package/openclaw/test-sse-consumer.js +98 -0
- package/openclaw/tsconfig.json +26 -0
- package/package.json +211 -0
- package/plugin/.claude-plugin/plugin.json +24 -0
- package/plugin/.codex-plugin/plugin.json +46 -0
- package/plugin/.mcp.json +12 -0
- package/plugin/hooks/bugfixes-2026-01-10.md +92 -0
- package/plugin/hooks/codex-hooks.json +74 -0
- package/plugin/hooks/hooks.json +87 -0
- package/plugin/modes/code--ar.json +24 -0
- package/plugin/modes/code--bn.json +24 -0
- package/plugin/modes/code--chill.json +8 -0
- package/plugin/modes/code--cs.json +24 -0
- package/plugin/modes/code--da.json +24 -0
- package/plugin/modes/code--de.json +24 -0
- package/plugin/modes/code--el.json +24 -0
- package/plugin/modes/code--es.json +24 -0
- package/plugin/modes/code--fi.json +24 -0
- package/plugin/modes/code--fr.json +24 -0
- package/plugin/modes/code--he.json +24 -0
- package/plugin/modes/code--hi.json +24 -0
- package/plugin/modes/code--hu.json +24 -0
- package/plugin/modes/code--id.json +24 -0
- package/plugin/modes/code--it.json +24 -0
- package/plugin/modes/code--ja.json +24 -0
- package/plugin/modes/code--ko.json +24 -0
- package/plugin/modes/code--nl.json +24 -0
- package/plugin/modes/code--no.json +24 -0
- package/plugin/modes/code--pl.json +24 -0
- package/plugin/modes/code--pt-br.json +24 -0
- package/plugin/modes/code--ro.json +24 -0
- package/plugin/modes/code--ru.json +24 -0
- package/plugin/modes/code--sv.json +24 -0
- package/plugin/modes/code--th.json +24 -0
- package/plugin/modes/code--tr.json +24 -0
- package/plugin/modes/code--uk.json +24 -0
- package/plugin/modes/code--ur.json +25 -0
- package/plugin/modes/code--vi.json +24 -0
- package/plugin/modes/code--zh.json +24 -0
- package/plugin/modes/code.json +139 -0
- package/plugin/modes/email-investigation.json +120 -0
- package/plugin/modes/law-study--chill.json +7 -0
- package/plugin/modes/law-study-CLAUDE.md +85 -0
- package/plugin/modes/law-study.json +120 -0
- package/plugin/modes/meme-tokens.json +125 -0
- package/plugin/package.json +46 -0
- package/plugin/scripts/bun-runner.js +216 -0
- package/plugin/scripts/context-generator.cjs +795 -0
- package/plugin/scripts/mcp-server.cjs +239 -0
- package/plugin/scripts/server-beta-service.cjs +9856 -0
- package/plugin/scripts/statusline-counts.js +40 -0
- package/plugin/scripts/version-check.js +69 -0
- package/plugin/scripts/worker-cli.js +19 -0
- package/plugin/scripts/worker-service.cjs +2368 -0
- package/plugin/scripts/worker-wrapper.cjs +2 -0
- package/plugin/skills/babysit/SKILL.md +87 -0
- package/plugin/skills/design-is/SKILL.md +312 -0
- package/plugin/skills/do/SKILL.md +45 -0
- package/plugin/skills/how-it-works/SKILL.md +22 -0
- package/plugin/skills/how-it-works/onboarding-explainer.md +17 -0
- package/plugin/skills/knowledge-agent/SKILL.md +80 -0
- package/plugin/skills/learn-codebase/SKILL.md +21 -0
- package/plugin/skills/make-plan/SKILL.md +67 -0
- package/plugin/skills/mem-search/SKILL.md +131 -0
- package/plugin/skills/oh-my-issues/SKILL.md +226 -0
- package/plugin/skills/pathfinder/SKILL.md +111 -0
- package/plugin/skills/smart-explore/SKILL.md +190 -0
- package/plugin/skills/timeline-report/SKILL.md +211 -0
- package/plugin/skills/version-bump/SKILL.md +68 -0
- package/plugin/skills/version-bump/scripts/generate_changelog.js +34 -0
- package/plugin/skills/weekly-digests/SKILL.md +262 -0
- package/plugin/skills/wowerpoint/SKILL.md +205 -0
- package/plugin/ui/assets/fonts/monaspace-radon-var.woff +0 -0
- package/plugin/ui/assets/fonts/monaspace-radon-var.woff2 +0 -0
- package/plugin/ui/claude-mem-logo-for-dark-mode.webp +0 -0
- package/plugin/ui/claude-mem-logo-stylized.png +0 -0
- package/plugin/ui/claude-mem-logomark.webp +0 -0
- package/plugin/ui/icon-thick-completed.svg +8 -0
- package/plugin/ui/icon-thick-investigated.svg +8 -0
- package/plugin/ui/icon-thick-learned.svg +12 -0
- package/plugin/ui/icon-thick-next-steps.svg +8 -0
- package/plugin/ui/viewer-bundle.js +65 -0
- package/plugin/ui/viewer.html +3145 -0
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
# OpenClaw Claude-Mem Plugin — Testing Guide
|
|
2
|
+
|
|
3
|
+
## Quick Start (Docker)
|
|
4
|
+
|
|
5
|
+
The fastest way to test the plugin is using the pre-built Docker E2E environment:
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
cd openclaw
|
|
9
|
+
|
|
10
|
+
# Automated test (builds, installs plugin on real OpenClaw, verifies everything)
|
|
11
|
+
./test-e2e.sh
|
|
12
|
+
|
|
13
|
+
# Interactive shell (for manual exploration)
|
|
14
|
+
./test-e2e.sh --interactive
|
|
15
|
+
|
|
16
|
+
# Just build the image
|
|
17
|
+
./test-e2e.sh --build-only
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
---
|
|
21
|
+
|
|
22
|
+
## Test Layers
|
|
23
|
+
|
|
24
|
+
### 1. Unit Tests (fastest)
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
cd openclaw
|
|
28
|
+
npm test # compiles TypeScript, runs 17 tests
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Tests plugin registration, service lifecycle, command handling, SSE integration, and all 6 channel types.
|
|
32
|
+
|
|
33
|
+
### 2. Smoke Test
|
|
34
|
+
|
|
35
|
+
```bash
|
|
36
|
+
node test-sse-consumer.js
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Quick check that the plugin loads and registers its service + command correctly.
|
|
40
|
+
|
|
41
|
+
### 3. Container Unit Tests (fresh install)
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
./test-container.sh # Unit tests in clean Docker
|
|
45
|
+
./test-container.sh --full # Integration tests with mock worker
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### 4. E2E on Real OpenClaw (Docker)
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
./test-e2e.sh
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
This is the most comprehensive test. It:
|
|
55
|
+
1. Uses the official `ghcr.io/openclaw/openclaw:main` Docker image
|
|
56
|
+
2. Installs the plugin via `openclaw plugins install` (same as a real user)
|
|
57
|
+
3. Enables the plugin via `openclaw plugins enable`
|
|
58
|
+
4. Starts a mock claude-mem worker on port 37777
|
|
59
|
+
5. Starts the OpenClaw gateway with plugin config
|
|
60
|
+
6. Verifies the plugin loads, connects to SSE, and processes events
|
|
61
|
+
|
|
62
|
+
**All 16 checks must pass.**
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Human E2E Testing (Interactive Docker)
|
|
67
|
+
|
|
68
|
+
For manual walkthrough testing, use the interactive Docker mode:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
./test-e2e.sh --interactive
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This drops you into a fully-configured OpenClaw container with the plugin pre-installed.
|
|
75
|
+
|
|
76
|
+
### Step-by-step inside the container
|
|
77
|
+
|
|
78
|
+
#### 1. Verify plugin is installed
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
node openclaw.mjs plugins list
|
|
82
|
+
node openclaw.mjs plugins info claude-mem
|
|
83
|
+
node openclaw.mjs plugins doctor
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Expected:**
|
|
87
|
+
- `claude-mem` appears in the plugins list as "enabled" or "loaded"
|
|
88
|
+
- Info shows version 1.0.0, source at `/home/node/.openclaw/extensions/claude-mem/`
|
|
89
|
+
- Doctor reports no issues
|
|
90
|
+
|
|
91
|
+
#### 2. Inspect plugin files
|
|
92
|
+
|
|
93
|
+
```bash
|
|
94
|
+
ls -la /home/node/.openclaw/extensions/claude-mem/
|
|
95
|
+
cat /home/node/.openclaw/extensions/claude-mem/openclaw.plugin.json
|
|
96
|
+
cat /home/node/.openclaw/extensions/claude-mem/package.json
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
**Expected:**
|
|
100
|
+
- `dist/index.js` exists (compiled plugin)
|
|
101
|
+
- `openclaw.plugin.json` has `"id": "claude-mem"` and `"kind": "memory"`
|
|
102
|
+
- `package.json` has `openclaw.extensions` field pointing to `./dist/index.js`
|
|
103
|
+
|
|
104
|
+
#### 3. Start mock worker
|
|
105
|
+
|
|
106
|
+
```bash
|
|
107
|
+
node /app/mock-worker.js &
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
Verify it's running:
|
|
111
|
+
|
|
112
|
+
```bash
|
|
113
|
+
curl -s http://localhost:37777/health
|
|
114
|
+
# → {"status":"ok"}
|
|
115
|
+
|
|
116
|
+
curl -s --max-time 3 http://localhost:37777/stream
|
|
117
|
+
# → data: {"type":"connected","message":"Mock worker SSE stream"}
|
|
118
|
+
# → data: {"type":"new_observation","observation":{...}}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
#### 4. Configure and start gateway
|
|
122
|
+
|
|
123
|
+
```bash
|
|
124
|
+
cat > /home/node/.openclaw/openclaw.json << 'EOF'
|
|
125
|
+
{
|
|
126
|
+
"gateway": {
|
|
127
|
+
"mode": "local",
|
|
128
|
+
"auth": {
|
|
129
|
+
"mode": "token",
|
|
130
|
+
"token": "e2e-test-token"
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
"plugins": {
|
|
134
|
+
"slots": {
|
|
135
|
+
"memory": "claude-mem"
|
|
136
|
+
},
|
|
137
|
+
"entries": {
|
|
138
|
+
"claude-mem": {
|
|
139
|
+
"enabled": true,
|
|
140
|
+
"config": {
|
|
141
|
+
"workerPort": 37777,
|
|
142
|
+
"observationFeed": {
|
|
143
|
+
"enabled": true,
|
|
144
|
+
"channel": "telegram",
|
|
145
|
+
"to": "test-chat-id-12345"
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
EOF
|
|
153
|
+
|
|
154
|
+
node openclaw.mjs gateway --allow-unconfigured --verbose --token e2e-test-token
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
**Expected in gateway logs:**
|
|
158
|
+
- `[claude-mem] OpenClaw plugin loaded — v1.0.0`
|
|
159
|
+
- `[claude-mem] Observation feed starting — channel: telegram, target: test-chat-id-12345`
|
|
160
|
+
- `[claude-mem] Connecting to SSE stream at http://localhost:37777/stream`
|
|
161
|
+
- `[claude-mem] Connected to SSE stream`
|
|
162
|
+
|
|
163
|
+
#### 5. Run automated verification (optional)
|
|
164
|
+
|
|
165
|
+
From a second shell in the container (or after stopping the gateway):
|
|
166
|
+
|
|
167
|
+
```bash
|
|
168
|
+
/bin/bash /app/e2e-verify.sh
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
---
|
|
172
|
+
|
|
173
|
+
## Manual E2E (Real OpenClaw + Real Worker)
|
|
174
|
+
|
|
175
|
+
For testing with a real claude-mem worker and real messaging channel:
|
|
176
|
+
|
|
177
|
+
### Prerequisites
|
|
178
|
+
|
|
179
|
+
- OpenClaw gateway installed and configured
|
|
180
|
+
- Claude-Mem worker running on port 37777
|
|
181
|
+
- Plugin built: `cd openclaw && npm run build`
|
|
182
|
+
|
|
183
|
+
### 1. Install the plugin
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
# Build the plugin
|
|
187
|
+
cd openclaw && npm run build
|
|
188
|
+
|
|
189
|
+
# Install on OpenClaw (from the openclaw/ directory)
|
|
190
|
+
openclaw plugins install .
|
|
191
|
+
|
|
192
|
+
# Enable it
|
|
193
|
+
openclaw plugins enable claude-mem
|
|
194
|
+
```
|
|
195
|
+
|
|
196
|
+
### 2. Configure
|
|
197
|
+
|
|
198
|
+
Edit `~/.openclaw/openclaw.json` to add plugin config:
|
|
199
|
+
|
|
200
|
+
```json
|
|
201
|
+
{
|
|
202
|
+
"plugins": {
|
|
203
|
+
"entries": {
|
|
204
|
+
"claude-mem": {
|
|
205
|
+
"enabled": true,
|
|
206
|
+
"config": {
|
|
207
|
+
"workerPort": 37777,
|
|
208
|
+
"observationFeed": {
|
|
209
|
+
"enabled": true,
|
|
210
|
+
"channel": "telegram",
|
|
211
|
+
"to": "YOUR_CHAT_ID"
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
**Supported channels:** `telegram`, `discord`, `signal`, `slack`, `whatsapp`, `line`
|
|
221
|
+
|
|
222
|
+
### 3. Restart gateway
|
|
223
|
+
|
|
224
|
+
```bash
|
|
225
|
+
openclaw restart
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Look for in logs:**
|
|
229
|
+
- `[claude-mem] OpenClaw plugin loaded — v1.0.0`
|
|
230
|
+
- `[claude-mem] Connected to SSE stream`
|
|
231
|
+
|
|
232
|
+
### 4. Trigger an observation
|
|
233
|
+
|
|
234
|
+
Start a Claude Code session with claude-mem enabled and perform any action. The worker will emit a `new_observation` SSE event.
|
|
235
|
+
|
|
236
|
+
### 5. Verify delivery
|
|
237
|
+
|
|
238
|
+
Check the target messaging channel for:
|
|
239
|
+
|
|
240
|
+
```
|
|
241
|
+
🧠 Claude-Mem Observation
|
|
242
|
+
**Observation Title**
|
|
243
|
+
Optional subtitle
|
|
244
|
+
```
|
|
245
|
+
|
|
246
|
+
---
|
|
247
|
+
|
|
248
|
+
## Troubleshooting
|
|
249
|
+
|
|
250
|
+
### `api.log is not a function`
|
|
251
|
+
The plugin was built against the wrong API. Ensure `src/index.ts` uses `api.logger.info()` not `api.log()`. Rebuild with `npm run build`.
|
|
252
|
+
|
|
253
|
+
### Worker not running
|
|
254
|
+
- **Symptom:** `SSE stream error: fetch failed. Reconnecting in 1s`
|
|
255
|
+
- **Fix:** Start the worker: `cd /path/to/claude-mem && npm run build-and-sync`
|
|
256
|
+
|
|
257
|
+
### Port mismatch
|
|
258
|
+
- **Fix:** Ensure `workerPort` in config matches the worker's actual port (default: 37777)
|
|
259
|
+
|
|
260
|
+
### Channel not configured
|
|
261
|
+
- **Symptom:** `Observation feed misconfigured — channel or target missing`
|
|
262
|
+
- **Fix:** Add both `channel` and `to` to `observationFeed` in config
|
|
263
|
+
|
|
264
|
+
### Unknown channel type
|
|
265
|
+
- **Fix:** Use: `telegram`, `discord`, `signal`, `slack`, `whatsapp`, or `line`
|
|
266
|
+
|
|
267
|
+
### Feed disabled
|
|
268
|
+
- **Symptom:** `Observation feed disabled`
|
|
269
|
+
- **Fix:** Set `observationFeed.enabled: true`
|
|
270
|
+
|
|
271
|
+
### Messages not arriving
|
|
272
|
+
1. Verify the bot/integration is configured in the target channel
|
|
273
|
+
2. Check the target ID (`to`) is correct
|
|
274
|
+
3. Look for `Failed to send to <channel>` in logs
|
|
275
|
+
4. Test the channel via OpenClaw's built-in tools
|
|
276
|
+
|
|
277
|
+
### Memory slot conflict
|
|
278
|
+
- **Symptom:** `plugin disabled (memory slot set to "memory-core")`
|
|
279
|
+
- **Fix:** Add `"slots": { "memory": "claude-mem" }` to plugins config
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
var Y="127.0.0.1",X=["\u{1F527}","\u{1F4D0}","\u{1F50D}","\u{1F4BB}","\u{1F9EA}","\u{1F41B}","\u{1F6E1}\uFE0F","\u2601\uFE0F","\u{1F4E6}","\u{1F3AF}","\u{1F52E}","\u26A1","\u{1F30A}","\u{1F3A8}","\u{1F4CA}","\u{1F680}","\u{1F52C}","\u{1F3D7}\uFE0F","\u{1F4DD}","\u{1F3AD}"];function V(e){let s=0;for(let o=0;o<e.length;o++)s=(s<<5)-s+e.charCodeAt(o)|0;return X[Math.abs(s)%X.length]}var ee="\u{1F99E}",ne="\u2328\uFE0F",te="Claude Code Session",re="\u{1F980}";function se(e){let s=e?.primary??ee,o=e?.claudeCode??ne,a=e?.claudeCodeLabel??te,l=e?.default??re,g=e?.agents??{};return function(m){if(!m)return l;if(m.startsWith("openclaw-")){let p=m.slice(9);return p?`${g[p]||V(p)} ${p}`:`${s} openclaw`}if(m==="openclaw")return`${s} openclaw`;let b=a.trim();return b?`${o} ${b} (${m})`:`${o} ${m}`}}var q=Y;function T(e){return`http://${q}:${e}`}var oe=3,Q=3e4,y="CLOSED",j=0,J=0,$=!1;function G(e){return y==="CLOSED"?!0:y==="OPEN"?Date.now()-J>=Q?(y="HALF_OPEN",e.info("[claude-mem] Circuit breaker: probing worker connection"),$?!1:($=!0,!0)):!1:$?!1:($=!0,!0)}function z(e){y!=="CLOSED"&&e.info("[claude-mem] Worker connection restored \u2014 circuit closed"),y="CLOSED",j=0,$=!1}function M(e){$=!1,j++,(y==="HALF_OPEN"||y==="CLOSED"&&j>=oe)&&(y="OPEN",J=Date.now(),e.warn(`[claude-mem] Worker unreachable \u2014 disabling requests for ${Q/1e3}s`))}function ie(){y="CLOSED",j=0,J=0,$=!1}async function Z(e,s,o,a){if(!G(a))return null;try{let l=await fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)});return l.ok?(z(a),await l.json()):(M(a),a.warn(`[claude-mem] Worker POST ${s} returned ${l.status}`),null)}catch(l){let g=l instanceof Error?l.message:String(l);return M(a),y!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`),null}}function ae(e,s,o,a){G(a)&&fetch(`${T(e)}${s}`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify(o)}).then(l=>{if(!l.ok){M(a),a.warn(`[claude-mem] Worker POST ${s} returned ${l.status}`);return}z(a)}).catch(l=>{let g=l instanceof Error?l.message:String(l);M(a),y!=="OPEN"&&a.warn(`[claude-mem] Worker POST ${s} failed: ${g}`)})}async function H(e,s,o){if(!G(o))return null;try{let a=await fetch(`${T(e)}${s}`);return a.ok?(z(o),await a.text()):(M(o),o.warn(`[claude-mem] Worker GET ${s} returned ${a.status}`),null)}catch(a){let l=a instanceof Error?a.message:String(a);return M(o),y!=="OPEN"&&o.warn(`[claude-mem] Worker GET ${s} failed: ${l}`),null}}async function W(e,s,o){let a=await H(e,s,o);if(!a)return null;try{return JSON.parse(a)}catch{return o.warn(`[claude-mem] Worker GET ${s} returned non-JSON response`),null}}function ce(e,s){let o=e.title||"Untitled",l=`${s(e.project)}
|
|
2
|
+
**${o}**`;return e.subtitle&&(l+=`
|
|
3
|
+
${e.subtitle}`),l}var le={telegram:{namespace:"telegram",functionName:"sendMessageTelegram"},whatsapp:{namespace:"whatsapp",functionName:"sendMessageWhatsApp"},discord:{namespace:"discord",functionName:"sendMessageDiscord"},slack:{namespace:"slack",functionName:"sendMessageSlack"},signal:{namespace:"signal",functionName:"sendMessageSignal"},imessage:{namespace:"imessage",functionName:"sendMessageIMessage"},line:{namespace:"line",functionName:"sendMessageLine"}};async function ue(e,s,o,a){try{let l=await fetch(`https://api.telegram.org/bot${e}/sendMessage`,{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({chat_id:s,text:o,parse_mode:"Markdown"})});if(!l.ok){let g=await l.text();a.warn(`[claude-mem] Direct Telegram send failed (${l.status}): ${g}`)}}catch(l){let g=l instanceof Error?l.message:String(l);a.warn(`[claude-mem] Direct Telegram send error: ${g}`)}}function ge(e,s,o,a,l){if(l&&s==="telegram")return ue(l,o,a,e.logger);let g=le[s];if(!g)return e.logger.warn(`[claude-mem] Unsupported channel type: ${s}`),Promise.resolve();let v=e.runtime.channel[g.namespace];if(!v)return e.logger.warn(`[claude-mem] Channel "${s}" not available in runtime`),Promise.resolve();let m=v[g.functionName];return m?m(...s==="whatsapp"?[o,a,{verbose:!1}]:[o,a]).catch(p=>{let w=p instanceof Error?p.message:String(p);e.logger.error(`[claude-mem] Failed to send to ${s}: ${w}`)}):(e.logger.warn(`[claude-mem] Channel "${s}" has no ${g.functionName} function`),Promise.resolve())}async function de(e,s,o,a,l,g,v,m){let b=1e3,p=3e4;for(;!l.signal.aborted;){try{g("reconnecting"),e.logger.info(`[claude-mem] Connecting to SSE stream at ${T(s)}/stream`);let w=await fetch(`${T(s)}/stream`,{signal:l.signal,headers:{Accept:"text/event-stream"}});if(!w.ok)throw new Error(`SSE stream returned HTTP ${w.status}`);if(!w.body)throw new Error("SSE stream response has no body");g("connected"),b=1e3,e.logger.info("[claude-mem] Connected to SSE stream");let x=w.body.getReader(),D=new TextDecoder,_="";for(;;){let{done:R,value:P}=await x.read();if(R)break;_+=D.decode(P,{stream:!0}),_.length>1048576&&(e.logger.warn("[claude-mem] SSE buffer overflow, clearing buffer"),_="");let L=_.split(`
|
|
4
|
+
|
|
5
|
+
`);_=L.pop()||"";for(let B of L){let F=B.split(`
|
|
6
|
+
`).filter(S=>S.startsWith("data:")).map(S=>S.slice(5).trim());if(F.length===0)continue;let A=F.join(`
|
|
7
|
+
`);if(A)try{let S=JSON.parse(A);if(S.type==="new_observation"&&S.observation){let C=ce(S.observation,v);await ge(e,o,a,C,m)}}catch(S){let O=S instanceof Error?S.message:String(S);e.logger.warn(`[claude-mem] Failed to parse SSE frame: ${O}`)}}}}catch(w){if(l.signal.aborted)break;g("reconnecting");let x=w instanceof Error?w.message:String(w);e.logger.warn(`[claude-mem] SSE stream error: ${x}. Reconnecting in ${b/1e3}s`)}if(l.signal.aborted)break;await new Promise(w=>setTimeout(w,b)),b=Math.min(b*2,p)}g("disconnected")}function me(e){let s=e.pluginConfig||{},o=s.workerPort||37777;q=s.workerHost||Y;let a=s.project||"openclaw",l=se(s.observationFeed?.emojis);function g(r){return r.agentId?`openclaw-${r.agentId}`:a}let v=new Map,m=new Map,b=new Map,p=new Map,w=s.syncMemoryFile!==!1,x=new Set(s.syncMemoryFileExclude||[]);function D(r){let n=r||"default";return v.has(n)||v.set(n,`openclaw-${n}-${Date.now()}`),v.get(n)}function _(r){if(!w)return!1;let n=r?.agentId;return!(n&&x.has(n))}function R(r){let n=new Set;for(let t of[r.sessionKey,r.conversationId,r.channelId]){let i=typeof t=="string"?t.trim():"";i&&n.add(i)}return n.size===0&&n.add("default"),Array.from(n)}function P(r){let n=R(r),t=n.find(u=>m.has(u));t=t?m.get(t):n[0];let i=b.get(t);i||(i=new Set([t]),b.set(t,i));for(let u of n)i.add(u),m.set(u,t);let c=D(t);for(let u of i)v.set(u,c);return{canonicalKey:t,contentSessionId:c}}function L(r,n,t){let i=Date.now();for(let[d,f]of p)i-f>2e3&&p.delete(d);let c=`${r}::${n}::${t}`,u=p.get(c);return p.set(c,i),typeof u=="number"&&i-u<=2e3}function B(r){let n=R(r),t=n.map(c=>m.get(c)).find(Boolean)||n[0],i=b.get(t)||new Set([t,...n]);for(let c of i)m.delete(c),v.delete(c);b.delete(t),v.delete(t)}let F=6e4,A=new Map;async function S(r){let n=[a],t=r?g(r):null;t&&t!==a&&n.push(t);let i=n.join(","),c=A.get(i);if(c&&Date.now()-c.fetchedAt<F)return c.text;let u=await H(o,`/api/context/inject?projects=${encodeURIComponent(i)}`,e.logger);if(u&&u.trim().length>0){let d=u.trim();return A.set(i,{text:d,fetchedAt:Date.now()}),d}return null}async function O(r,n,t){let{contentSessionId:i}=P(r),c=g(r);if(L(i,c,n)){e.logger.info(`[claude-mem] Skipping duplicate prompt init: contentSessionId=${i} project=${c} via=${t}`);return}await Z(o,"/api/sessions/init",{contentSessionId:i,project:c,prompt:n},e.logger),e.logger.info(`[claude-mem] Session initialized via ${t}: contentSessionId=${i} project=${c}`)}e.on("session_start",async(r,n)=>{await O(n,"session start","session_start")}),e.on("message_received",async(r,n)=>{let{canonicalKey:t,contentSessionId:i}=P(n);e.logger.info(`[claude-mem] Message received \u2014 prompt capture deferred to before_agent_start: session=${t} contentSessionId=${i} hasContent=${!!r.content}`)}),e.on("after_compaction",async(r,n)=>{await O(n,"after compaction","after_compaction")}),e.on("before_agent_start",async(r,n)=>{await O(n,r.prompt||"agent run","before_agent_start")}),e.on("before_prompt_build",async(r,n)=>{if(!_(n))return;let t=await S(n);if(t)return e.logger.info(`[claude-mem] Context injected via system prompt for agent=${n.agentId??"unknown"}`),{appendSystemContext:t}}),e.on("tool_result_persist",(r,n)=>{e.logger.info(`[claude-mem] tool_result_persist fired: tool=${r.toolName??"unknown"} agent=${n.agentId??"none"} session=${n.sessionKey??"none"}`);let t=r.toolName;if(!t||t.startsWith("memory_"))return;let{canonicalKey:i,contentSessionId:c}=P(n),u="",d=r.message?.content;Array.isArray(d)&&(u=d.filter(E=>(E.type==="tool_result"||E.type==="text")&&"text"in E).map(E=>String(E.text)).join(`
|
|
8
|
+
`));let f=1e3;u.length>f&&(u=u.slice(0,f));let h=n.workspaceDir||process.cwd();n.workspaceDir||e.logger.info(`[claude-mem] tool_result_persist missing workspaceDir; using process.cwd(): session=${i} tool=${t}`),ae(o,"/api/sessions/observations",{contentSessionId:c,tool_name:t,tool_input:r.params||{},tool_response:u,cwd:h},e.logger)}),e.on("agent_end",async(r,n)=>{let{contentSessionId:t}=P(n),i="";if(Array.isArray(r.messages))for(let c=r.messages.length-1;c>=0;c--){let u=r.messages[c];if(u?.role==="assistant"){typeof u.content=="string"?i=u.content:Array.isArray(u.content)&&(i=u.content.filter(d=>d.type==="text").map(d=>d.text||"").join(`
|
|
9
|
+
`));break}}await Z(o,"/api/sessions/summarize",{contentSessionId:t,last_assistant_message:i},e.logger)}),e.on("session_end",async(r,n)=>{B(n),e.logger.info("[claude-mem] Session tracking cleaned up")}),e.on("gateway_start",async()=>{ie(),v.clear(),A.clear(),p.clear(),m.clear(),b.clear(),e.logger.info("[claude-mem] Gateway started \u2014 session tracking reset")});let C=null,I="disconnected",k=null;e.registerService({id:"claude-mem-observation-feed",start:async r=>{C&&(C.abort(),k&&(await k,k=null));let n=s.observationFeed;if(!n?.enabled){e.logger.info("[claude-mem] Observation feed disabled");return}if(!n.channel||!n.to){e.logger.warn("[claude-mem] Observation feed misconfigured \u2014 channel or target missing");return}e.logger.info(`[claude-mem] Observation feed starting \u2014 channel: ${n.channel}, target: ${n.to}`),C=new AbortController,k=de(e,o,n.channel,n.to,C,t=>{I=t},l,n.botToken)},stop:async r=>{C&&(C.abort(),C=null),k&&(await k,k=null),I="disconnected",e.logger.info("[claude-mem] Observation feed stopped \u2014 SSE connection closed")}});function U(r,n=5){return!Array.isArray(r)||r.length===0?"No results found.":r.slice(0,n).map((t,i)=>{let c=t,u=String(c.title||c.subtitle||c.text||"Untitled"),d=c.project?` [${String(c.project)}]`:"";return`${i+1}. ${u}${d}`}).join(`
|
|
10
|
+
`)}function N(r,n=10){let t=Number(r);return Number.isFinite(t)?Math.max(1,Math.min(50,Math.trunc(t))):n}e.registerCommand({name:"claude_mem_feed",description:"Show or toggle Claude-Mem observation feed status",acceptsArgs:!0,handler:async r=>{let n=s.observationFeed;if(!n)return{text:"Observation feed not configured. Add observationFeed to your plugin config."};let t=r.args?.trim();return t==="on"?(e.logger.info("[claude-mem] Feed enable requested via command"),{text:"Feed enable requested. Update observationFeed.enabled in your plugin config to persist."}):t==="off"?(e.logger.info("[claude-mem] Feed disable requested via command"),{text:"Feed disable requested. Update observationFeed.enabled in your plugin config to persist."}):{text:["Claude-Mem Observation Feed",`Enabled: ${n.enabled?"yes":"no"}`,`Channel: ${n.channel||"not set"}`,`Target: ${n.to||"not set"}`,`Connection: ${I}`].join(`
|
|
11
|
+
`)}}}),e.registerCommand({name:"claude-mem-search",description:"Search Claude-Mem observations by query",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-search <query> [limit]";let t=n.split(/\s+/),i=t[t.length-1],c=/^\d+$/.test(i),u=c?N(i,10):10,d=c?t.slice(0,-1).join(" "):n,f=await W(o,`/api/search/observations?query=${encodeURIComponent(d)}&limit=${u}`,e.logger);if(!f)return"Claude-Mem search failed (worker unavailable or invalid response).";let h=Array.isArray(f.items)?f.items:[];return[`Claude-Mem Search: "${d}"`,U(h,u)].join(`
|
|
12
|
+
`)}}),e.registerCommand({name:"claude-mem-recent",description:"Show recent Claude-Mem context for a project",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"",t=n?n.split(/\s+/):[],i=t.length>0?t[t.length-1]:"",c=/^\d+$/.test(i),u=c?N(i,3):3,d=c?t.slice(0,-1).join(" "):n,f=new URLSearchParams;f.set("limit",String(u)),d&&f.set("project",d);let h=await W(o,`/api/context/recent?${f.toString()}`,e.logger);if(!h)return"Claude-Mem recent context failed (worker unavailable or invalid response).";let E=Array.isArray(h.session_summaries)?h.session_summaries:[],K=Array.isArray(h.recent_observations)?h.recent_observations:[];return["Claude-Mem Recent Context",`Project: ${d||"(auto)"}`,`Session summaries: ${E.length}`,`Recent observations: ${K.length}`,U(K,Math.min(5,K.length||5))].join(`
|
|
13
|
+
`)}}),e.registerCommand({name:"claude-mem-timeline",description:"Find best memory match and show nearby timeline events",acceptsArgs:!0,handler:async r=>{let n=r.args?.trim()||"";if(!n)return"Usage: /claude-mem-timeline <query> [depthBefore] [depthAfter]";let t=n.split(/\s+/),i=5,c=5;t.length>=2&&/^\d+$/.test(t[t.length-1])&&(i=N(t.pop(),5)),t.length>=2&&/^\d+$/.test(t[t.length-1])&&(c=N(t.pop(),5));let u=t.join(" "),d=new URLSearchParams({query:u,mode:"auto",depth_before:String(c),depth_after:String(i)}),f=await W(o,`/api/timeline/by-query?${d.toString()}`,e.logger);if(!f)return"Claude-Mem timeline lookup failed (worker unavailable or invalid response).";let h=Array.isArray(f.timeline)?f.timeline:[],E=f.anchor?String(f.anchor):"(none)";return[`Claude-Mem Timeline: "${u}"`,`Anchor: ${E}`,U(h,8)].join(`
|
|
14
|
+
`)}}),e.registerCommand({name:"claude_mem_status",description:"Check Claude-Mem worker health and session status",handler:async()=>{let r=await H(o,"/api/health",e.logger);if(!r)return{text:`Claude-Mem worker unreachable at port ${o}`};try{return{text:["Claude-Mem Worker Status",`Status: ${JSON.parse(r).status||"unknown"}`,`Port: ${o}`,`Active sessions: ${v.size}`,`Observation feed: ${I}`].join(`
|
|
15
|
+
`)}}catch{return{text:"Claude-Mem worker responded but returned unexpected data"}}}}),e.logger.info(`[claude-mem] OpenClaw plugin loaded \u2014 v1.0.0 (worker: ${q}:${o})`)}export{me as default};
|
|
@@ -0,0 +1,222 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
PASS=0
|
|
6
|
+
FAIL=0
|
|
7
|
+
TOTAL=0
|
|
8
|
+
|
|
9
|
+
pass() {
|
|
10
|
+
PASS=$((PASS + 1))
|
|
11
|
+
TOTAL=$((TOTAL + 1))
|
|
12
|
+
echo " PASS: $1"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
fail() {
|
|
16
|
+
FAIL=$((FAIL + 1))
|
|
17
|
+
TOTAL=$((TOTAL + 1))
|
|
18
|
+
echo " FAIL: $1"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
section() {
|
|
22
|
+
echo ""
|
|
23
|
+
echo "=== $1 ==="
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
section "Phase 1: Plugin Discovery"
|
|
27
|
+
|
|
28
|
+
PLUGIN_LIST=$(node /app/openclaw.mjs plugins list 2>&1)
|
|
29
|
+
if echo "$PLUGIN_LIST" | grep -q "claude-mem"; then
|
|
30
|
+
pass "Plugin appears in 'plugins list'"
|
|
31
|
+
else
|
|
32
|
+
fail "Plugin NOT found in 'plugins list'"
|
|
33
|
+
echo "$PLUGIN_LIST"
|
|
34
|
+
fi
|
|
35
|
+
|
|
36
|
+
PLUGIN_INFO=$(node /app/openclaw.mjs plugins info claude-mem 2>&1 || true)
|
|
37
|
+
if echo "$PLUGIN_INFO" | grep -qi "claude-mem"; then
|
|
38
|
+
pass "Plugin info shows claude-mem details"
|
|
39
|
+
else
|
|
40
|
+
fail "Plugin info failed"
|
|
41
|
+
echo "$PLUGIN_INFO"
|
|
42
|
+
fi
|
|
43
|
+
|
|
44
|
+
if echo "$PLUGIN_LIST" | grep -A1 "claude-mem" | grep -qi "enabled\|loaded"; then
|
|
45
|
+
pass "Plugin is enabled"
|
|
46
|
+
else
|
|
47
|
+
if echo "$PLUGIN_INFO" | grep -qi "enabled\|loaded"; then
|
|
48
|
+
pass "Plugin is enabled (via info)"
|
|
49
|
+
else
|
|
50
|
+
fail "Plugin does not appear enabled"
|
|
51
|
+
echo "$PLUGIN_INFO"
|
|
52
|
+
fi
|
|
53
|
+
fi
|
|
54
|
+
|
|
55
|
+
DOCTOR_OUT=$(node /app/openclaw.mjs plugins doctor 2>&1 || true)
|
|
56
|
+
if echo "$DOCTOR_OUT" | grep -qi "no.*issue\|0 issue"; then
|
|
57
|
+
pass "Plugin doctor reports no issues"
|
|
58
|
+
else
|
|
59
|
+
fail "Plugin doctor reports issues"
|
|
60
|
+
echo "$DOCTOR_OUT"
|
|
61
|
+
fi
|
|
62
|
+
|
|
63
|
+
section "Phase 2: Plugin Files"
|
|
64
|
+
|
|
65
|
+
EXTENSIONS_DIR="/home/node/.openclaw/extensions/openclaw-plugin"
|
|
66
|
+
if [ ! -d "$EXTENSIONS_DIR" ]; then
|
|
67
|
+
EXTENSIONS_DIR="/home/node/.openclaw/extensions/claude-mem"
|
|
68
|
+
if [ ! -d "$EXTENSIONS_DIR" ]; then
|
|
69
|
+
FOUND_DIR=$(find /home/node/.openclaw/extensions/ -name "openclaw.plugin.json" -exec dirname {} \; 2>/dev/null | head -1 || true)
|
|
70
|
+
if [ -n "$FOUND_DIR" ]; then
|
|
71
|
+
EXTENSIONS_DIR="$FOUND_DIR"
|
|
72
|
+
fi
|
|
73
|
+
fi
|
|
74
|
+
fi
|
|
75
|
+
|
|
76
|
+
if [ -d "$EXTENSIONS_DIR" ]; then
|
|
77
|
+
pass "Plugin directory exists: $EXTENSIONS_DIR"
|
|
78
|
+
else
|
|
79
|
+
fail "Plugin directory not found under /home/node/.openclaw/extensions/"
|
|
80
|
+
ls -la /home/node/.openclaw/extensions/ 2>/dev/null || echo " (extensions dir not found)"
|
|
81
|
+
fi
|
|
82
|
+
|
|
83
|
+
for FILE in "openclaw.plugin.json" "dist/index.js" "package.json"; do
|
|
84
|
+
if [ -f "$EXTENSIONS_DIR/$FILE" ]; then
|
|
85
|
+
pass "File exists: $FILE"
|
|
86
|
+
else
|
|
87
|
+
fail "File missing: $FILE"
|
|
88
|
+
fi
|
|
89
|
+
done
|
|
90
|
+
|
|
91
|
+
section "Phase 3: Mock Worker + Plugin Integration"
|
|
92
|
+
|
|
93
|
+
echo " Starting mock claude-mem worker..."
|
|
94
|
+
node /app/mock-worker.js &
|
|
95
|
+
MOCK_PID=$!
|
|
96
|
+
|
|
97
|
+
for i in $(seq 1 10); do
|
|
98
|
+
if curl -sf http://localhost:37777/health > /dev/null 2>&1; then
|
|
99
|
+
break
|
|
100
|
+
fi
|
|
101
|
+
sleep 0.5
|
|
102
|
+
done
|
|
103
|
+
|
|
104
|
+
if curl -sf http://localhost:37777/health > /dev/null 2>&1; then
|
|
105
|
+
pass "Mock worker health check passed"
|
|
106
|
+
else
|
|
107
|
+
fail "Mock worker health check failed"
|
|
108
|
+
kill $MOCK_PID 2>/dev/null || true
|
|
109
|
+
fi
|
|
110
|
+
|
|
111
|
+
SSE_TEST=$(curl -s --max-time 2 http://localhost:37777/stream 2>/dev/null || true)
|
|
112
|
+
if echo "$SSE_TEST" | grep -q "connected"; then
|
|
113
|
+
pass "SSE stream returns connected event"
|
|
114
|
+
else
|
|
115
|
+
fail "SSE stream did not return connected event"
|
|
116
|
+
echo " Got: $(echo "$SSE_TEST" | head -5)"
|
|
117
|
+
fi
|
|
118
|
+
|
|
119
|
+
section "Phase 4: Gateway Startup with Plugin"
|
|
120
|
+
|
|
121
|
+
mkdir -p /home/node/.openclaw
|
|
122
|
+
cat > /home/node/.openclaw/openclaw.json << 'EOFCONFIG'
|
|
123
|
+
{
|
|
124
|
+
"gateway": {
|
|
125
|
+
"mode": "local",
|
|
126
|
+
"auth": {
|
|
127
|
+
"mode": "token",
|
|
128
|
+
"token": "e2e-test-token"
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
"plugins": {
|
|
132
|
+
"slots": {
|
|
133
|
+
"memory": "claude-mem"
|
|
134
|
+
},
|
|
135
|
+
"entries": {
|
|
136
|
+
"claude-mem": {
|
|
137
|
+
"enabled": true,
|
|
138
|
+
"config": {
|
|
139
|
+
"workerPort": 37777,
|
|
140
|
+
"observationFeed": {
|
|
141
|
+
"enabled": true,
|
|
142
|
+
"channel": "telegram",
|
|
143
|
+
"to": "test-chat-id-12345"
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
EOFCONFIG
|
|
151
|
+
|
|
152
|
+
pass "OpenClaw config written with plugin enabled"
|
|
153
|
+
|
|
154
|
+
GATEWAY_LOG="/tmp/gateway.log"
|
|
155
|
+
echo " Starting OpenClaw gateway (timeout 15s)..."
|
|
156
|
+
OPENCLAW_GATEWAY_TOKEN=e2e-test-token timeout 15 node /app/openclaw.mjs gateway --allow-unconfigured --verbose --token e2e-test-token > "$GATEWAY_LOG" 2>&1 &
|
|
157
|
+
GATEWAY_PID=$!
|
|
158
|
+
|
|
159
|
+
sleep 5
|
|
160
|
+
|
|
161
|
+
if kill -0 $GATEWAY_PID 2>/dev/null; then
|
|
162
|
+
pass "Gateway process is running"
|
|
163
|
+
else
|
|
164
|
+
fail "Gateway process exited early"
|
|
165
|
+
echo " Gateway log:"
|
|
166
|
+
cat "$GATEWAY_LOG" 2>/dev/null | tail -30
|
|
167
|
+
fi
|
|
168
|
+
|
|
169
|
+
if grep -qi "claude-mem" "$GATEWAY_LOG" 2>/dev/null; then
|
|
170
|
+
pass "Gateway log mentions claude-mem plugin"
|
|
171
|
+
else
|
|
172
|
+
fail "Gateway log does not mention claude-mem"
|
|
173
|
+
echo " Gateway log (last 20 lines):"
|
|
174
|
+
tail -20 "$GATEWAY_LOG" 2>/dev/null
|
|
175
|
+
fi
|
|
176
|
+
|
|
177
|
+
if grep -q "plugin loaded" "$GATEWAY_LOG" 2>/dev/null || grep -q "v1.0.0" "$GATEWAY_LOG" 2>/dev/null; then
|
|
178
|
+
pass "Plugin load message found in gateway log"
|
|
179
|
+
else
|
|
180
|
+
fail "Plugin load message not found"
|
|
181
|
+
fi
|
|
182
|
+
|
|
183
|
+
if grep -qi "observation feed" "$GATEWAY_LOG" 2>/dev/null; then
|
|
184
|
+
pass "Observation feed activity in gateway log"
|
|
185
|
+
else
|
|
186
|
+
fail "No observation feed activity detected"
|
|
187
|
+
fi
|
|
188
|
+
|
|
189
|
+
if grep -qi "connected.*SSE\|SSE.*stream\|connecting.*SSE" "$GATEWAY_LOG" 2>/dev/null; then
|
|
190
|
+
pass "SSE connection activity detected"
|
|
191
|
+
else
|
|
192
|
+
fail "No SSE connection activity in log"
|
|
193
|
+
fi
|
|
194
|
+
|
|
195
|
+
section "Cleanup"
|
|
196
|
+
kill $GATEWAY_PID 2>/dev/null || true
|
|
197
|
+
kill $MOCK_PID 2>/dev/null || true
|
|
198
|
+
wait $GATEWAY_PID 2>/dev/null || true
|
|
199
|
+
wait $MOCK_PID 2>/dev/null || true
|
|
200
|
+
echo " Processes stopped."
|
|
201
|
+
|
|
202
|
+
echo ""
|
|
203
|
+
echo "==============================="
|
|
204
|
+
echo " E2E Test Results"
|
|
205
|
+
echo "==============================="
|
|
206
|
+
echo " Total: $TOTAL"
|
|
207
|
+
echo " Passed: $PASS"
|
|
208
|
+
echo " Failed: $FAIL"
|
|
209
|
+
echo "==============================="
|
|
210
|
+
|
|
211
|
+
if [ "$FAIL" -gt 0 ]; then
|
|
212
|
+
echo ""
|
|
213
|
+
echo " SOME TESTS FAILED"
|
|
214
|
+
echo ""
|
|
215
|
+
echo " Full gateway log:"
|
|
216
|
+
cat "$GATEWAY_LOG" 2>/dev/null
|
|
217
|
+
exit 1
|
|
218
|
+
fi
|
|
219
|
+
|
|
220
|
+
echo ""
|
|
221
|
+
echo " ALL TESTS PASSED"
|
|
222
|
+
exit 0
|