@mingxy/cerebro-claude-code 0.3.3
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/.claude-plugin/marketplace.json +20 -0
- package/.claude-plugin/plugin.json +13 -0
- package/.mcp.json +10 -0
- package/README.md +159 -0
- package/config.json +22 -0
- package/hooks/common.mjs +687 -0
- package/hooks/common.sh +646 -0
- package/hooks/dream.mjs +240 -0
- package/hooks/flush-detached.mjs +45 -0
- package/hooks/hooks.json +83 -0
- package/hooks/post-compact.mjs +49 -0
- package/hooks/pre-compact.mjs +26 -0
- package/hooks/recall-approve.mjs +26 -0
- package/hooks/session-end.mjs +34 -0
- package/hooks/session-start.mjs +156 -0
- package/hooks/stop.mjs +35 -0
- package/hooks/user-prompt-submit.mjs +49 -0
- package/package.json +28 -0
- package/scripts/memory-profile.sh +67 -0
- package/scripts/memory-save.sh +127 -0
- package/scripts/memory-search.sh +94 -0
- package/scripts/web-server.mjs +132 -0
- package/skills/memory-profile/SKILL.md +41 -0
- package/skills/memory-save/SKILL.md +70 -0
- package/skills/memory-search/SKILL.md +47 -0
- package/tests/common.test.mjs +123 -0
- package/tests/hooks.test.mjs +111 -0
- package/tests/test_smoke.sh +233 -0
- package/web/assets/geist-cyrillic-wght-normal-CHSlOQsW.woff2 +0 -0
- package/web/assets/geist-latin-ext-wght-normal-DMtmJ5ZE.woff2 +0 -0
- package/web/assets/geist-latin-wght-normal-Dm3htQBi.woff2 +0 -0
- package/web/assets/index-DVguCuEA.css +1 -0
- package/web/assets/index-Z_oxQQlO.js +165 -0
- package/web/favicon.svg +1 -0
- package/web/icons.svg +24 -0
- package/web/index.html +15 -0
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: memory-search
|
|
3
|
+
description: Semantic search over the user's long-term memory (Cerebro). Use when the user references past work, prior decisions, stored preferences, or session history you may have saved; when an ambiguous name/concept could be disambiguated by stored context; or when you need background that earlier sessions likely captured. Searches are auto-filtered to the current project + user tags. Returns ranked matches with relevance scores.
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Memory Search
|
|
7
|
+
|
|
8
|
+
Search long-term memory by semantic similarity. Memories are scoped to the current project automatically; global-scope memories are always included.
|
|
9
|
+
|
|
10
|
+
## When to use
|
|
11
|
+
|
|
12
|
+
- User mentions "earlier / before / last time / we discussed / remember when …"
|
|
13
|
+
- A name, file, or concept feels ambiguous and stored context could resolve it
|
|
14
|
+
- You need a prior decision, rationale, or preference before acting
|
|
15
|
+
- Recovering context that was compacted away in a previous session
|
|
16
|
+
|
|
17
|
+
## How to run
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
bash "$CLAUDE_PLUGIN_ROOT/scripts/memory-search.sh" "auth flow refresh-token decision" 10
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
- Arg 1: natural-language query (auto-truncated; quoting is recommended).
|
|
24
|
+
- Arg 2 (optional): result limit. Defaults to `$MEM_SEARCH_COUNT` (8).
|
|
25
|
+
|
|
26
|
+
Content can also be piped via stdin:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
echo "why did we pick sqlite over postgres" | bash "$CLAUDE_PLUGIN_ROOT/scripts/memory-search.sh" -
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Output format
|
|
33
|
+
|
|
34
|
+
One line per match:
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
[0.91] mem_abc123: Fixed refresh-token rotation bug in auth.rs — added replay guard
|
|
38
|
+
[0.78] mem_def456: Decision: use sqlite for local cache, postgres only on cloud
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
`no memories` when nothing matches. `error: …` on transport/server failure.
|
|
42
|
+
|
|
43
|
+
## Notes
|
|
44
|
+
|
|
45
|
+
- Query is auto-truncated to `$MEM_MAX_QUERY_LENGTH` (200 chars).
|
|
46
|
+
- Filters by `omem_user_<hash>` and `omem_project_<hash>` tags plus `project_path`, so results respect project isolation.
|
|
47
|
+
- For full content of a truncated match, follow up with `memory-save` is NOT the right tool — fetch by id via the server API or re-run search with a sharper query.
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import {
|
|
4
|
+
cleanText,
|
|
5
|
+
formatRelativeAge,
|
|
6
|
+
truncateQuery,
|
|
7
|
+
sanitizeContent,
|
|
8
|
+
} from "../hooks/common.mjs";
|
|
9
|
+
|
|
10
|
+
describe("cleanText", () => {
|
|
11
|
+
test("strips cerebro inject tags", () => {
|
|
12
|
+
const input = "hello <cerebro-memory>secret</cerebro-memory> world";
|
|
13
|
+
assert.equal(cleanText(input), "hello world");
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
test("strips cerebro self-closing tags", () => {
|
|
17
|
+
const input = "text <cerebro-nudge /> after";
|
|
18
|
+
assert.equal(cleanText(input), "text after");
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test("strips system-reminder tags", () => {
|
|
22
|
+
const input = "before <system-reminder>internal</system-reminder> after";
|
|
23
|
+
assert.equal(cleanText(input), "before after");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
test("strips supermemory tags", () => {
|
|
27
|
+
const input = "a <supermemory-context>data</supermemory-context> b";
|
|
28
|
+
assert.equal(cleanText(input), "a b");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
test("collapses whitespace", () => {
|
|
32
|
+
const input = "line1\n\n\n line2 \n\tline3";
|
|
33
|
+
assert.equal(cleanText(input), "line1 line2 line3");
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
test("handles non-string input", () => {
|
|
37
|
+
assert.equal(cleanText(42), "42");
|
|
38
|
+
assert.equal(cleanText(null), "null");
|
|
39
|
+
});
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
describe("formatRelativeAge", () => {
|
|
43
|
+
test("returns 'unknown' for null/undefined", () => {
|
|
44
|
+
assert.equal(formatRelativeAge(null), "unknown");
|
|
45
|
+
assert.equal(formatRelativeAge(undefined), "unknown");
|
|
46
|
+
assert.equal(formatRelativeAge(""), "unknown");
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test("returns minutes for recent dates", () => {
|
|
50
|
+
const fiveMinAgo = new Date(Date.now() - 5 * 60000).toISOString();
|
|
51
|
+
const result = formatRelativeAge(fiveMinAgo);
|
|
52
|
+
assert.match(result, /^\d+m ago$/);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
test("returns hours for older dates", () => {
|
|
56
|
+
const threeHrAgo = new Date(Date.now() - 3 * 3600000).toISOString();
|
|
57
|
+
const result = formatRelativeAge(threeHrAgo);
|
|
58
|
+
assert.match(result, /^\d+h ago$/);
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
test("returns days for dates > 24h", () => {
|
|
62
|
+
const threeDayAgo = new Date(Date.now() - 3 * 86400000).toISOString();
|
|
63
|
+
const result = formatRelativeAge(threeDayAgo);
|
|
64
|
+
assert.match(result, /^\d+d ago$/);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("returns months for dates > 30d", () => {
|
|
68
|
+
const twoMonthAgo = new Date(Date.now() - 70 * 86400000).toISOString();
|
|
69
|
+
const result = formatRelativeAge(twoMonthAgo);
|
|
70
|
+
assert.match(result, /^\d+mo ago$/);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
describe("truncateQuery", () => {
|
|
75
|
+
test("returns empty for empty input", () => {
|
|
76
|
+
assert.equal(truncateQuery(""), "");
|
|
77
|
+
assert.equal(truncateQuery(null), "");
|
|
78
|
+
assert.equal(truncateQuery(undefined), "");
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("returns short query unchanged", () => {
|
|
82
|
+
assert.equal(truncateQuery("hello"), "hello");
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("truncates long query to default length", () => {
|
|
86
|
+
const long = "x".repeat(300);
|
|
87
|
+
const result = truncateQuery(long);
|
|
88
|
+
assert.equal(result.length, 200);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test("respects custom length", () => {
|
|
92
|
+
const long = "x".repeat(100);
|
|
93
|
+
assert.equal(truncateQuery(long, 50).length, 50);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
describe("sanitizeContent", () => {
|
|
98
|
+
test("removes HTML tags", () => {
|
|
99
|
+
const input = "text <div>inner</div> more";
|
|
100
|
+
assert.equal(sanitizeContent(input, 1000), "text more");
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test("removes self-closing tags", () => {
|
|
104
|
+
const input = "a <br/> b";
|
|
105
|
+
assert.equal(sanitizeContent(input, 1000), "a b");
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test("collapses whitespace", () => {
|
|
109
|
+
const input = "line1\n\n line2";
|
|
110
|
+
assert.equal(sanitizeContent(input, 1000), "line1 line2");
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test("truncates with ellipsis marker when exceeding maxLen", () => {
|
|
114
|
+
const long = "x".repeat(200);
|
|
115
|
+
const result = sanitizeContent(long, 50);
|
|
116
|
+
assert.ok(result.length <= 65); // 50 + "…[truncated]"
|
|
117
|
+
assert.ok(result.includes("…[truncated]"));
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test("does not truncate short content", () => {
|
|
121
|
+
assert.equal(sanitizeContent("short", 1000), "short");
|
|
122
|
+
});
|
|
123
|
+
});
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { test, describe } from "node:test";
|
|
2
|
+
import assert from "node:assert/strict";
|
|
3
|
+
import { spawn } from "node:child_process";
|
|
4
|
+
import { join, dirname } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const HOOKS_DIR = join(dirname(fileURLToPath(import.meta.url)), "..", "hooks");
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Run a hook script with given stdin JSON, return parsed stdout JSON.
|
|
11
|
+
* Points OMEM_API_URL to a dead port so fetch calls fail fast
|
|
12
|
+
* (postRecallEvent / buildMemoryInjection swallow errors via try/catch).
|
|
13
|
+
*/
|
|
14
|
+
function runHook(script, stdinObj, opts = {}) {
|
|
15
|
+
return new Promise((resolve, reject) => {
|
|
16
|
+
const child = spawn(process.execPath, [join(HOOKS_DIR, script)], {
|
|
17
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
18
|
+
env: {
|
|
19
|
+
...process.env,
|
|
20
|
+
OMEM_API_KEY: opts.apiKey ?? "test-key-12345",
|
|
21
|
+
OMEM_API_URL: opts.apiUrl ?? "http://127.0.0.1:1",
|
|
22
|
+
},
|
|
23
|
+
});
|
|
24
|
+
let stdout = "";
|
|
25
|
+
let stderr = "";
|
|
26
|
+
child.stdout.on("data", (d) => (stdout += d));
|
|
27
|
+
child.stderr.on("data", (d) => (stderr += d));
|
|
28
|
+
child.on("close", (code) => resolve({ stdout, stderr, code }));
|
|
29
|
+
child.on("error", reject);
|
|
30
|
+
child.stdin.write(JSON.stringify(stdinObj));
|
|
31
|
+
child.stdin.end();
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
describe("user-prompt-submit.mjs", () => {
|
|
36
|
+
test("injects cerebro-recall instruction for normal message", async () => {
|
|
37
|
+
const { stdout } = await runHook("user-prompt-submit.mjs", {
|
|
38
|
+
session_id: "test-sid",
|
|
39
|
+
prompt: "hello world",
|
|
40
|
+
});
|
|
41
|
+
const out = JSON.parse(stdout);
|
|
42
|
+
const ctx = out.hookSpecificOutput.additionalContext;
|
|
43
|
+
assert.ok(ctx.includes("<cerebro-recall>"), "should contain cerebro-recall instruction");
|
|
44
|
+
assert.ok(!ctx.includes("cerebro-nudge"), "should NOT contain nudge for normal message");
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
test("injects save nudge when prompt contains save keyword", async () => {
|
|
48
|
+
const { stdout } = await runHook("user-prompt-submit.mjs", {
|
|
49
|
+
session_id: "test-sid",
|
|
50
|
+
prompt: "记住这个配置",
|
|
51
|
+
});
|
|
52
|
+
const ctx = JSON.parse(stdout).hookSpecificOutput.additionalContext;
|
|
53
|
+
assert.ok(ctx.includes("<cerebro-nudge>"), "should contain nudge");
|
|
54
|
+
assert.ok(ctx.includes("memory-save"), "should mention memory-save skill");
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
test("injects recall nudge when prompt contains recall keyword", async () => {
|
|
58
|
+
const { stdout } = await runHook("user-prompt-submit.mjs", {
|
|
59
|
+
session_id: "test-sid",
|
|
60
|
+
prompt: "之前那个bug怎么修的",
|
|
61
|
+
});
|
|
62
|
+
const ctx = JSON.parse(stdout).hookSpecificOutput.additionalContext;
|
|
63
|
+
assert.ok(ctx.includes("<cerebro-nudge>"), "should contain nudge");
|
|
64
|
+
assert.ok(ctx.includes("memory-search"), "should mention memory-search skill");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
test("handles English keywords", async () => {
|
|
68
|
+
const { stdout } = await runHook("user-prompt-submit.mjs", {
|
|
69
|
+
session_id: "test-sid",
|
|
70
|
+
prompt: "remember to update the docs",
|
|
71
|
+
});
|
|
72
|
+
const ctx = JSON.parse(stdout).hookSpecificOutput.additionalContext;
|
|
73
|
+
assert.ok(ctx.includes("cerebro-nudge"), "should detect English save keyword");
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
|
|
77
|
+
describe("pre-compact.mjs", () => {
|
|
78
|
+
test("outputs empty JSON (no hookSpecificOutput)", async () => {
|
|
79
|
+
const { stdout } = await runHook("pre-compact.mjs", {
|
|
80
|
+
session_id: "test-sid",
|
|
81
|
+
transcript_path: "/nonexistent/path.jsonl",
|
|
82
|
+
});
|
|
83
|
+
const out = JSON.parse(stdout);
|
|
84
|
+
assert.deepEqual(out, {});
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
test("outputs empty JSON when missing session_id", async () => {
|
|
88
|
+
const { stdout } = await runHook("pre-compact.mjs", {
|
|
89
|
+
transcript_path: "/nonexistent/path.jsonl",
|
|
90
|
+
});
|
|
91
|
+
const out = JSON.parse(stdout);
|
|
92
|
+
assert.deepEqual(out, {});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
describe("session-end.mjs", () => {
|
|
97
|
+
test("outputs empty JSON for nonexistent transcript", async () => {
|
|
98
|
+
const { stdout } = await runHook("session-end.mjs", {
|
|
99
|
+
session_id: "test-sid",
|
|
100
|
+
transcript_path: "/nonexistent/path.jsonl",
|
|
101
|
+
});
|
|
102
|
+
const out = JSON.parse(stdout);
|
|
103
|
+
assert.deepEqual(out, {});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("outputs empty JSON when missing fields", async () => {
|
|
107
|
+
const { stdout } = await runHook("session-end.mjs", {});
|
|
108
|
+
const out = JSON.parse(stdout);
|
|
109
|
+
assert.deepEqual(out, {});
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# cerebro Claude Code plugin — Phase5 smoke tests
|
|
3
|
+
#
|
|
4
|
+
# 零依赖:纯 bash + python3(不装 bats)。一键跑:bash tests/test_smoke.sh
|
|
5
|
+
#
|
|
6
|
+
# 覆盖:
|
|
7
|
+
# - session-start:无 key 分支([cerebro] 提示)+ 带 key 分支(stub server,
|
|
8
|
+
# 断言 [CEREBRO-MEMORY] / [CEREBRO-TIME] / <cerebro-profile> / recent)
|
|
9
|
+
# - user-prompt-submit:保存类 / 召回类 / 普通 / 英文 关键词 nudge
|
|
10
|
+
# - recall-approve:memory-search 放行 / 普通 Bash / 危险 Bash
|
|
11
|
+
# - memory-save / memory-search:参数校验 exit code + --help
|
|
12
|
+
# - stop / pre-compact:无 key 短路 {}
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
PLUGIN_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
16
|
+
HOOKS="$PLUGIN_DIR/hooks"
|
|
17
|
+
SCRIPTS="$PLUGIN_DIR/scripts"
|
|
18
|
+
TMP="$(mktemp -d)"
|
|
19
|
+
STUB_PID=""
|
|
20
|
+
|
|
21
|
+
cleanup() {
|
|
22
|
+
[[ -n "$STUB_PID" ]] && kill "$STUB_PID" 2>/dev/null || true
|
|
23
|
+
rm -rf "$TMP" 2>/dev/null || true
|
|
24
|
+
}
|
|
25
|
+
trap cleanup EXIT
|
|
26
|
+
|
|
27
|
+
# ─── 环境隔离:不读真实 config,不带真实 key,不写日志 ────────────────────────
|
|
28
|
+
export CEREBRO_CONFIG_PATH="/nonexistent-test-cfg.json"
|
|
29
|
+
export OMEM_API_KEY=""
|
|
30
|
+
export OMEM_API_URL="http://127.0.0.1:1" # unreachable → curl fails silently
|
|
31
|
+
export MEM_LOG_ENABLED="0"
|
|
32
|
+
export MEM_LOG_DIR="$TMP/logs"
|
|
33
|
+
|
|
34
|
+
PASS=0
|
|
35
|
+
FAIL=0
|
|
36
|
+
FAILED_CASES=()
|
|
37
|
+
|
|
38
|
+
ok() { echo "PASS: $1"; PASS=$((PASS + 1)); }
|
|
39
|
+
fail() { echo "FAIL: $1"; FAIL=$((FAIL + 1)); FAILED_CASES+=("$1"); }
|
|
40
|
+
|
|
41
|
+
assert_contains() { # <label> <haystack> <needle>
|
|
42
|
+
if [[ "$2" == *"$3"* ]]; then ok "$1"; else fail "$1 (missing: $3)"; fi
|
|
43
|
+
}
|
|
44
|
+
assert_not_contains() { # <label> <haystack> <needle>
|
|
45
|
+
if [[ "$2" != *"$3"* ]]; then ok "$1"; else fail "$1 (unexpected: $3)"; fi
|
|
46
|
+
}
|
|
47
|
+
assert_exit() { # <label> <expected> <actual>
|
|
48
|
+
if [[ "$2" == "$3" ]]; then ok "$1"; else fail "$1 (exit: want=$2 got=$3)"; fi
|
|
49
|
+
}
|
|
50
|
+
assert_eq() { # <label> <expected> <actual>
|
|
51
|
+
if [[ "$2" == "$3" ]]; then ok "$1"; else fail "$1 (want=[$2] got=[$3])"; fi
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
# ─── 本地 stub HTTP server(测 session-start 带 key 分支)─────────────────────
|
|
55
|
+
# 返回固定 profile + recent JSON,让 hook 走 [CEREBRO-MEMORY] 组装分支。
|
|
56
|
+
start_stub() {
|
|
57
|
+
cat > "$TMP/stub.py" <<'PYEOF'
|
|
58
|
+
import http.server, socketserver, json
|
|
59
|
+
class H(http.server.BaseHTTPRequestHandler):
|
|
60
|
+
def do_GET(self):
|
|
61
|
+
p = self.path
|
|
62
|
+
if p.startswith("/v2/profile/inject"):
|
|
63
|
+
body = json.dumps({"content": "TEST PROFILE", "preference_count": 1})
|
|
64
|
+
elif p.startswith("/v1/memories"):
|
|
65
|
+
body = json.dumps({"memories": [{"content": "recent activity line", "id": "m1"}]})
|
|
66
|
+
else:
|
|
67
|
+
body = "{}"
|
|
68
|
+
b = body.encode()
|
|
69
|
+
self.send_response(200)
|
|
70
|
+
self.send_header("Content-Type", "application/json")
|
|
71
|
+
self.send_header("Content-Length", str(len(b)))
|
|
72
|
+
self.end_headers()
|
|
73
|
+
self.wfile.write(b)
|
|
74
|
+
def log_message(self, *a): pass
|
|
75
|
+
class S(socketserver.ThreadingMixIn, http.server.HTTPServer):
|
|
76
|
+
daemon_threads = True
|
|
77
|
+
srv = S(("127.0.0.1", 0), H)
|
|
78
|
+
print(srv.server_address[1], flush=True)
|
|
79
|
+
srv.serve_forever()
|
|
80
|
+
PYEOF
|
|
81
|
+
python3 "$TMP/stub.py" > "$TMP/port" 2>/dev/null &
|
|
82
|
+
STUB_PID=$!
|
|
83
|
+
for _ in 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20; do
|
|
84
|
+
[[ -s "$TMP/port" ]] && break
|
|
85
|
+
sleep 0.1
|
|
86
|
+
done
|
|
87
|
+
STUB_PORT="$(cat "$TMP/port" 2>/dev/null || echo "")"
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
echo "=== SessionStart ==="
|
|
91
|
+
|
|
92
|
+
# (1) 无 key 分支
|
|
93
|
+
out="$(OMEM_API_KEY="" bash "$HOOKS/session-start.sh" </dev/null 2>/dev/null || true)"
|
|
94
|
+
assert_contains "session-start no-key: hookSpecificOutput" "$out" '"hookSpecificOutput"'
|
|
95
|
+
assert_contains "session-start no-key: [cerebro] prefix" "$out" '[cerebro]'
|
|
96
|
+
assert_contains "session-start no-key: mentions API key" "$out" 'OMEM_API_KEY'
|
|
97
|
+
|
|
98
|
+
# (2) 带 key 分支(stub server)——验证 [CEREBRO-MEMORY] / [CEREBRO-TIME] / profile / recent
|
|
99
|
+
start_stub
|
|
100
|
+
if [[ -z "$STUB_PORT" ]]; then
|
|
101
|
+
fail "session-start stub: server did not start"
|
|
102
|
+
else
|
|
103
|
+
out="$(OMEM_API_KEY="test-key" OMEM_API_URL="http://127.0.0.1:$STUB_PORT" \
|
|
104
|
+
bash "$HOOKS/session-start.sh" </dev/null 2>/dev/null || true)"
|
|
105
|
+
assert_contains "session-start stub: hookSpecificOutput" "$out" '"hookSpecificOutput"'
|
|
106
|
+
assert_contains "session-start stub: [CEREBRO-MEMORY] open" "$out" '[CEREBRO-MEMORY]'
|
|
107
|
+
assert_contains "session-start stub: [/CEREBRO-MEMORY] close" "$out" '[/CEREBRO-MEMORY]'
|
|
108
|
+
assert_contains "session-start stub: [CEREBRO-TIME] line" "$out" '[CEREBRO-TIME]'
|
|
109
|
+
assert_contains "session-start stub: time prefix text" "$out" '当前:'
|
|
110
|
+
assert_contains "session-start stub: <cerebro-profile>" "$out" '<cerebro-profile>'
|
|
111
|
+
assert_contains "session-start stub: profile content" "$out" 'TEST PROFILE'
|
|
112
|
+
assert_contains "session-start stub: recent activity" "$out" 'recent activity line'
|
|
113
|
+
fi
|
|
114
|
+
|
|
115
|
+
echo
|
|
116
|
+
echo "=== UserPromptSubmit (keyword nudge) ==="
|
|
117
|
+
|
|
118
|
+
run_ups() { printf '%s' "$1" | bash "$HOOKS/user-prompt-submit.sh" 2>/dev/null || true; }
|
|
119
|
+
|
|
120
|
+
# 保存类(中文)
|
|
121
|
+
out="$(run_ups '{"prompt":"请记住这个决定:以后用 rust 写后端"}')"
|
|
122
|
+
assert_contains "ups save-zh: nudge tag" "$out" '<cerebro-nudge>'
|
|
123
|
+
assert_contains "ups save-zh: memory-save" "$out" 'memory-save skill'
|
|
124
|
+
assert_contains "ups save-zh: recall instr" "$out" '<cerebro-recall>'
|
|
125
|
+
|
|
126
|
+
# 召回类(中文)
|
|
127
|
+
out="$(run_ups '{"prompt":"上次那个 bug 修好了吗"}')"
|
|
128
|
+
assert_contains "ups recall-zh: nudge tag" "$out" '<cerebro-nudge>'
|
|
129
|
+
assert_contains "ups recall-zh: memory-search" "$out" 'memory-search skill'
|
|
130
|
+
|
|
131
|
+
# 保存类(英文)
|
|
132
|
+
out="$(run_ups '{"prompt":"please remember this decision"}')"
|
|
133
|
+
assert_contains "ups save-en: nudge tag" "$out" '<cerebro-nudge>'
|
|
134
|
+
assert_contains "ups save-en: memory-save" "$out" 'memory-save skill'
|
|
135
|
+
|
|
136
|
+
# 召回类(英文)
|
|
137
|
+
out="$(run_ups '{"prompt":"lets continue from earlier"}')"
|
|
138
|
+
assert_contains "ups recall-en: nudge tag" "$out" '<cerebro-nudge>'
|
|
139
|
+
assert_contains "ups recall-en: memory-search" "$out" 'memory-search skill'
|
|
140
|
+
|
|
141
|
+
# 普通消息:无 nudge,只有 recall 指令
|
|
142
|
+
out="$(run_ups '{"prompt":"你好,今天天气如何"}')"
|
|
143
|
+
assert_not_contains "ups plain: no nudge" "$out" '<cerebro-nudge>'
|
|
144
|
+
assert_contains "ups plain: recall instr" "$out" '<cerebro-recall>'
|
|
145
|
+
|
|
146
|
+
# 空/缺字段 stdin:不崩,有 recall 指令
|
|
147
|
+
out="$(run_ups '{}')"
|
|
148
|
+
assert_contains "ups empty: recall instr" "$out" '<cerebro-recall>'
|
|
149
|
+
assert_not_contains "ups empty: no nudge" "$out" '<cerebro-nudge>'
|
|
150
|
+
|
|
151
|
+
echo
|
|
152
|
+
echo "=== recall-approve (PreToolUse) ==="
|
|
153
|
+
|
|
154
|
+
run_ra() { printf '%s' "$1" | bash "$HOOKS/recall-approve.sh" 2>/dev/null || true; }
|
|
155
|
+
|
|
156
|
+
# memory-search Skill → allow
|
|
157
|
+
out="$(run_ra '{"tool_name":"Skill","tool_input":{"name":"memory-search"}}')"
|
|
158
|
+
assert_contains "ra skill-search: allow" "$out" '"permissionDecision"'
|
|
159
|
+
assert_contains "ra skill-search: allow val" "$out" 'allow'
|
|
160
|
+
|
|
161
|
+
# tool_name 直接含 memory-search → allow
|
|
162
|
+
out="$(run_ra '{"tool_name":"memory-search","tool_input":{}}')"
|
|
163
|
+
assert_contains "ra direct-search: allow" "$out" 'allow'
|
|
164
|
+
|
|
165
|
+
# 普通 Bash(非 memory-search)→ {}
|
|
166
|
+
out="$(run_ra '{"tool_name":"Bash","tool_input":{"command":"ls -la"}}')"
|
|
167
|
+
assert_eq "ra plain-bash: deny {}" '{}' "$out"
|
|
168
|
+
|
|
169
|
+
# 危险 Bash(memory-search.sh; rm)→ {}
|
|
170
|
+
out="$(run_ra '{"tool_name":"Bash","tool_input":{"command":"bash memory-search.sh; rm -rf /"}}')"
|
|
171
|
+
assert_eq "ra danger-bash: deny {}" '{}' "$out"
|
|
172
|
+
|
|
173
|
+
# 危险 Bash($( 命令替换)→ {}
|
|
174
|
+
out="$(run_ra '{"tool_name":"Bash","tool_input":{"command":"memory-search.sh $(whoami)"}}')"
|
|
175
|
+
assert_eq "ra subst-bash: deny {}" '{}' "$out"
|
|
176
|
+
|
|
177
|
+
echo
|
|
178
|
+
echo "=== memory-save arg validation ==="
|
|
179
|
+
|
|
180
|
+
# 空 content → exit 1
|
|
181
|
+
bash "$SCRIPTS/memory-save.sh" </dev/null >/dev/null 2>&1; rc=$?
|
|
182
|
+
assert_exit "save empty-content: exit 1" "1" "$rc"
|
|
183
|
+
|
|
184
|
+
# bad visibility → exit 2
|
|
185
|
+
bash "$SCRIPTS/memory-save.sh" --content "x" --visibility bad >/dev/null 2>&1; rc=$?
|
|
186
|
+
assert_exit "save bad-visibility: exit 2" "2" "$rc"
|
|
187
|
+
|
|
188
|
+
# bad category → exit 2
|
|
189
|
+
bash "$SCRIPTS/memory-save.sh" --content "x" --category bad >/dev/null 2>&1; rc=$?
|
|
190
|
+
assert_exit "save bad-category: exit 2" "2" "$rc"
|
|
191
|
+
|
|
192
|
+
# bad scope → exit 2
|
|
193
|
+
bash "$SCRIPTS/memory-save.sh" --content "x" --scope bad >/dev/null 2>&1; rc=$?
|
|
194
|
+
assert_exit "save bad-scope: exit 2" "2" "$rc"
|
|
195
|
+
|
|
196
|
+
# unknown option → exit 2
|
|
197
|
+
bash "$SCRIPTS/memory-save.sh" --bogus >/dev/null 2>&1; rc=$?
|
|
198
|
+
assert_exit "save unknown-opt: exit 2" "2" "$rc"
|
|
199
|
+
|
|
200
|
+
# --help → exit 0 + usage text
|
|
201
|
+
help_out="$(bash "$SCRIPTS/memory-save.sh" --help 2>&1 || true)"; rc=$?
|
|
202
|
+
assert_exit "save --help: exit 0" "0" "$rc"
|
|
203
|
+
assert_contains "save --help: usage text" "$help_out" 'content'
|
|
204
|
+
|
|
205
|
+
echo
|
|
206
|
+
echo "=== memory-search arg validation ==="
|
|
207
|
+
|
|
208
|
+
# 空 query → exit 1
|
|
209
|
+
bash "$SCRIPTS/memory-search.sh" "" </dev/null >/dev/null 2>&1; rc=$?
|
|
210
|
+
assert_exit "search empty-query: exit 1" "1" "$rc"
|
|
211
|
+
|
|
212
|
+
echo
|
|
213
|
+
echo "=== stop / pre-compact (no-key short-circuit) ==="
|
|
214
|
+
|
|
215
|
+
# stop 无 key → {}
|
|
216
|
+
out="$(OMEM_API_KEY="" bash "$HOOKS/stop.sh" </dev/null 2>/dev/null || true)"
|
|
217
|
+
assert_eq "stop no-key: {}" '{}' "$out"
|
|
218
|
+
|
|
219
|
+
# pre-compact 无 key → {}
|
|
220
|
+
out="$(OMEM_API_KEY="" bash "$HOOKS/pre-compact.sh" </dev/null 2>/dev/null || true)"
|
|
221
|
+
assert_eq "pre-compact no-key: {}" '{}' "$out"
|
|
222
|
+
|
|
223
|
+
echo
|
|
224
|
+
echo "========================"
|
|
225
|
+
TOTAL=$((PASS + FAIL))
|
|
226
|
+
echo "Total: $TOTAL | PASS: $PASS | FAIL: $FAIL"
|
|
227
|
+
if [[ "$FAIL" -ne 0 ]]; then
|
|
228
|
+
echo "Failed cases:"
|
|
229
|
+
for c in "${FAILED_CASES[@]}"; do echo " - $c"; done
|
|
230
|
+
exit 1
|
|
231
|
+
fi
|
|
232
|
+
echo "ALL PASS"
|
|
233
|
+
exit 0
|
|
Binary file
|
|
Binary file
|
|
Binary file
|