@onlooker-community/ecosystem 0.22.0 → 0.23.1
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/plugin.json +1 -1
- package/.release-please-manifest.json +3 -3
- package/CHANGELOG.md +15 -0
- package/hooks/hooks.json +4 -0
- package/package.json +1 -1
- package/plugins/historian/.claude-plugin/plugin.json +2 -2
- package/plugins/historian/CHANGELOG.md +7 -0
- package/plugins/historian/README.md +21 -7
- package/plugins/historian/config.json +19 -3
- package/plugins/historian/scripts/hooks/historian-prompt-submit.sh +262 -8
- package/plugins/historian/scripts/hooks/historian-session-end.sh +31 -0
- package/plugins/historian/scripts/lib/historian-embedder.sh +126 -0
- package/plugins/historian/scripts/lib/historian-retriever.sh +191 -0
- package/plugins/historian/scripts/lib/historian-storage.sh +47 -0
- package/plugins/scribe/.claude-plugin/plugin.json +4 -2
- package/plugins/scribe/CHANGELOG.md +7 -0
- package/plugins/scribe/scripts/hooks/scribe-capture.sh +0 -0
- package/plugins/scribe/scripts/hooks/scribe-session-start.sh +0 -0
- package/plugins/scribe/scripts/hooks/scribe-stop.sh +0 -0
- package/scripts/hooks/memory-recall-tracker.sh +206 -0
- package/test/bats/historian-prompt-submit.bats +236 -0
- package/test/bats/memory-recall-tracker.bats +189 -0
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
#
|
|
3
|
+
# Exercises the historian UserPromptSubmit retrieval pipeline end-to-end
|
|
4
|
+
# against a synthetic ollama daemon (a fake `curl` binary on PATH that
|
|
5
|
+
# returns predictable embeddings keyed on sentinel substrings in the
|
|
6
|
+
# prompt). Indexing happens via the real SessionEnd hook against the
|
|
7
|
+
# same stub, so the test exercises both halves of the embedder
|
|
8
|
+
# integration.
|
|
9
|
+
|
|
10
|
+
setup() {
|
|
11
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
12
|
+
setup_test_env
|
|
13
|
+
|
|
14
|
+
PLUGIN_ROOT="${REPO_ROOT}/plugins/historian"
|
|
15
|
+
export CLAUDE_PLUGIN_ROOT="$PLUGIN_ROOT"
|
|
16
|
+
export ONLOOKER_ECOSYSTEM_ROOT="$REPO_ROOT"
|
|
17
|
+
|
|
18
|
+
PROJECT_REPO="${BATS_TEST_TMPDIR}/repo"
|
|
19
|
+
mkdir -p "$PROJECT_REPO"
|
|
20
|
+
git -C "$PROJECT_REPO" init -q
|
|
21
|
+
git -C "$PROJECT_REPO" config user.email t@example.com
|
|
22
|
+
git -C "$PROJECT_REPO" config user.name "Test"
|
|
23
|
+
git -C "$PROJECT_REPO" remote add origin git@github.com:org/historian-retrieval-test.git
|
|
24
|
+
|
|
25
|
+
# shellcheck disable=SC1091
|
|
26
|
+
source "${PLUGIN_ROOT}/scripts/lib/historian-project-key.sh"
|
|
27
|
+
PROJECT_KEY=$(historian_project_key "$PROJECT_REPO")
|
|
28
|
+
[ -n "$PROJECT_KEY" ]
|
|
29
|
+
|
|
30
|
+
HIST_DIR="${ONLOOKER_DIR}/historian/${PROJECT_KEY}"
|
|
31
|
+
ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
32
|
+
|
|
33
|
+
STUB_BIN="${BATS_TEST_TMPDIR}/bin"
|
|
34
|
+
mkdir -p "$STUB_BIN"
|
|
35
|
+
cat > "${STUB_BIN}/curl" <<'STUB'
|
|
36
|
+
#!/usr/bin/env bash
|
|
37
|
+
# Mini curl stub for historian bats tests.
|
|
38
|
+
# Parses just enough of the curl arg shape to find the URL and the -d
|
|
39
|
+
# payload. Returns deterministic embeddings keyed on sentinel substrings
|
|
40
|
+
# in the prompt.
|
|
41
|
+
url=""
|
|
42
|
+
payload=""
|
|
43
|
+
prev=""
|
|
44
|
+
for arg in "$@"; do
|
|
45
|
+
case "$prev" in
|
|
46
|
+
-d|--data|--data-raw)
|
|
47
|
+
payload="$arg"; prev=""; continue ;;
|
|
48
|
+
--max-time|-o|-H|--header)
|
|
49
|
+
prev=""; continue ;;
|
|
50
|
+
esac
|
|
51
|
+
case "$arg" in
|
|
52
|
+
-d|--data|--data-raw|--max-time|-o|-H|--header)
|
|
53
|
+
prev="$arg" ;;
|
|
54
|
+
-*)
|
|
55
|
+
;;
|
|
56
|
+
*)
|
|
57
|
+
[[ -z "$url" ]] && url="$arg" ;;
|
|
58
|
+
esac
|
|
59
|
+
done
|
|
60
|
+
|
|
61
|
+
# An env var toggles the probe success so the same stub serves the
|
|
62
|
+
# "embedder unavailable" test case.
|
|
63
|
+
if [[ "${HISTORIAN_STUB_OLLAMA_AVAILABLE:-1}" == "0" ]]; then
|
|
64
|
+
exit 7
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [[ "$url" == */api/tags ]]; then
|
|
68
|
+
exit 0
|
|
69
|
+
fi
|
|
70
|
+
|
|
71
|
+
if [[ "$url" == */api/embeddings ]]; then
|
|
72
|
+
prompt=$(printf '%s' "$payload" | jq -r '.prompt // ""' 2>/dev/null)
|
|
73
|
+
case "$prompt" in
|
|
74
|
+
*redash*) printf '{"embedding":[1,0,0]}' ;;
|
|
75
|
+
*kafka*) printf '{"embedding":[0,1,0]}' ;;
|
|
76
|
+
*postgres*) printf '{"embedding":[0,0,1]}' ;;
|
|
77
|
+
*) printf '{"embedding":[0.5,0.5,0.5]}' ;;
|
|
78
|
+
esac
|
|
79
|
+
exit 0
|
|
80
|
+
fi
|
|
81
|
+
|
|
82
|
+
exit 1
|
|
83
|
+
STUB
|
|
84
|
+
chmod +x "${STUB_BIN}/curl"
|
|
85
|
+
export PATH="${STUB_BIN}:${PATH}"
|
|
86
|
+
|
|
87
|
+
TRANSCRIPT="${BATS_TEST_TMPDIR}/transcript.jsonl"
|
|
88
|
+
SESSION_ID="sess-retrieval"
|
|
89
|
+
|
|
90
|
+
mkdir -p "${PROJECT_REPO}/.claude"
|
|
91
|
+
printf '%s\n' \
|
|
92
|
+
'{"historian":{"enabled":true,"indexing":{"min_transcript_chars_to_index":50,"chunk_target_chars":400,"chunk_overlap_chars":50},"retrieval":{"cooldown_seconds":60,"max_retrievals_per_session":5,"min_prompt_chars":40,"min_similarity":0.55,"max_age_days":365}}}' \
|
|
93
|
+
> "${PROJECT_REPO}/.claude/settings.json"
|
|
94
|
+
|
|
95
|
+
INDEX_HOOK="${PLUGIN_ROOT}/scripts/hooks/historian-session-end.sh"
|
|
96
|
+
RETRIEVE_HOOK="${PLUGIN_ROOT}/scripts/hooks/historian-prompt-submit.sh"
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
_index_input() {
|
|
100
|
+
local sid="${1:-$SESSION_ID}"
|
|
101
|
+
jq -cn --arg cwd "$PROJECT_REPO" --arg sid "$sid" --arg transcript "$TRANSCRIPT" \
|
|
102
|
+
'{cwd:$cwd, session_id:$sid, transcript_path:$transcript, hook_event_name:"SessionEnd"}'
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
_retrieve_input() {
|
|
106
|
+
local prompt="$1" sid="${2:-current}"
|
|
107
|
+
jq -cn --arg cwd "$PROJECT_REPO" --arg sid "$sid" --arg prompt "$prompt" \
|
|
108
|
+
'{cwd:$cwd, session_id:$sid, prompt:$prompt, hook_event_name:"UserPromptSubmit"}'
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
_append_text_turn() {
|
|
112
|
+
local role="$1" text="$2"
|
|
113
|
+
jq -cn --arg role "$role" --arg text "$text" \
|
|
114
|
+
'{role:$role, content:$text}' >> "$TRANSCRIPT"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
_index_session() {
|
|
118
|
+
local sid="$1"
|
|
119
|
+
shift
|
|
120
|
+
: > "$TRANSCRIPT"
|
|
121
|
+
while [ $# -gt 0 ]; do
|
|
122
|
+
_append_text_turn "user" "$1"; shift
|
|
123
|
+
[ $# -gt 0 ] && { _append_text_turn "assistant" "$1"; shift; }
|
|
124
|
+
done
|
|
125
|
+
bash -c "printf '%s' '$(_index_input "$sid")' | '$INDEX_HOOK'" >/dev/null
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
@test "retrieval no-op when historian is disabled" {
|
|
129
|
+
rm -f "${PROJECT_REPO}/.claude/settings.json"
|
|
130
|
+
run bash -c "printf '%s' '$(_retrieve_input "a prompt long enough to clear the floor and trigger retrieval but historian is off")' | '$RETRIEVE_HOOK'"
|
|
131
|
+
[ "$status" -eq 0 ]
|
|
132
|
+
echo "$output" | jq -e '.hookSpecificOutput.additionalContext == ""' >/dev/null
|
|
133
|
+
[ ! -f "$ONLOOKER_EVENTS_LOG" ] || ! grep -q '"historian.retrieval' "$ONLOOKER_EVENTS_LOG"
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
@test "retrieval skipped when prompt is shorter than min_prompt_chars" {
|
|
137
|
+
run bash -c "printf '%s' '$(_retrieve_input "tiny")' | '$RETRIEVE_HOOK'"
|
|
138
|
+
[ "$status" -eq 0 ]
|
|
139
|
+
echo "$output" | jq -e '.hookSpecificOutput.additionalContext == ""' >/dev/null
|
|
140
|
+
grep '"event_type":"historian.retrieval.complete"' "$ONLOOKER_EVENTS_LOG" \
|
|
141
|
+
| jq -e '.payload.outcome == "skipped" and .payload.skip_reason == "short_prompt"' >/dev/null
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
@test "indexing embeds chunks when ollama is up" {
|
|
145
|
+
_index_session "$SESSION_ID" \
|
|
146
|
+
"We are debugging a redash dashboard problem with timezone offsets and saved query parameters this morning." \
|
|
147
|
+
"Sure — the latest version always passes UTC because of a chart migration we did last week."
|
|
148
|
+
|
|
149
|
+
local jsonl="${HIST_DIR}/sessions/${SESSION_ID}.jsonl"
|
|
150
|
+
[ -f "$jsonl" ]
|
|
151
|
+
jq -e '.embedding | type == "array" and length == 3' "$jsonl" >/dev/null
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
@test "retrieval surfaces a matching past chunk" {
|
|
155
|
+
# Index a past session containing a "redash" topic.
|
|
156
|
+
_index_session "past-1" \
|
|
157
|
+
"We are debugging a redash dashboard problem with timezone offsets and saved query parameters this morning." \
|
|
158
|
+
"Sure — the latest version always passes UTC because of a chart migration we did last week."
|
|
159
|
+
|
|
160
|
+
# New session, same project, query about redash → should match.
|
|
161
|
+
run bash -c "printf '%s' '$(_retrieve_input "Hitting another redash dashboard timezone issue on the same saved query parameters again today")' | '$RETRIEVE_HOOK'"
|
|
162
|
+
[ "$status" -eq 0 ]
|
|
163
|
+
|
|
164
|
+
local ctx
|
|
165
|
+
ctx=$(echo "$output" | jq -r '.hookSpecificOutput.additionalContext')
|
|
166
|
+
[[ "$ctx" == *"Historian: a past chunk looks similar"* ]]
|
|
167
|
+
[[ "$ctx" == *"redash"* ]]
|
|
168
|
+
|
|
169
|
+
grep '"event_type":"historian.retrieval.surfaced"' "$ONLOOKER_EVENTS_LOG" \
|
|
170
|
+
| jq -e '.payload.similarity >= 0.55 and .payload.source_session_id == "past-1"' >/dev/null
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
@test "retrieval returns empty when no chunk clears the similarity floor" {
|
|
174
|
+
# Past session about kafka — query about postgres falls below the
|
|
175
|
+
# 0.55 floor (the embedding vectors are orthogonal in the stub).
|
|
176
|
+
_index_session "past-2" \
|
|
177
|
+
"Investigating kafka consumer lag on the ingest pipeline today after the rebalance event yesterday." \
|
|
178
|
+
"Looks like the rebalance left a stale offset; manual reset cleared it."
|
|
179
|
+
|
|
180
|
+
run bash -c "printf '%s' '$(_retrieve_input "Working on a postgres migration plan today for our settings tables to add new columns safely")' | '$RETRIEVE_HOOK'"
|
|
181
|
+
[ "$status" -eq 0 ]
|
|
182
|
+
echo "$output" | jq -e '.hookSpecificOutput.additionalContext == ""' >/dev/null
|
|
183
|
+
|
|
184
|
+
grep '"event_type":"historian.retrieval.complete"' "$ONLOOKER_EVENTS_LOG" \
|
|
185
|
+
| jq -e '.payload.outcome == "empty"' >/dev/null
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
@test "retrieval skipped on cooldown" {
|
|
189
|
+
_index_session "past-3" \
|
|
190
|
+
"Yet another redash dashboard query that we had to fix the timezone on this morning to make the report run again." \
|
|
191
|
+
"ok"
|
|
192
|
+
|
|
193
|
+
# First retrieval surfaces something.
|
|
194
|
+
run bash -c "printf '%s' '$(_retrieve_input "redash dashboard timezone problem again on the saved query parameters this morning afternoon")' | '$RETRIEVE_HOOK'"
|
|
195
|
+
[ "$status" -eq 0 ]
|
|
196
|
+
grep -q '"event_type":"historian.retrieval.surfaced"' "$ONLOOKER_EVENTS_LOG"
|
|
197
|
+
|
|
198
|
+
rm -f "$ONLOOKER_EVENTS_LOG"
|
|
199
|
+
|
|
200
|
+
# Immediate second retrieval (same session) hits the cooldown gate
|
|
201
|
+
# (60s) and gets skipped without calling the embedder.
|
|
202
|
+
run bash -c "printf '%s' '$(_retrieve_input "redash dashboard timezone problem follow-up just a moment after the previous prompt cleared")' | '$RETRIEVE_HOOK'"
|
|
203
|
+
[ "$status" -eq 0 ]
|
|
204
|
+
grep '"event_type":"historian.retrieval.complete"' "$ONLOOKER_EVENTS_LOG" \
|
|
205
|
+
| jq -e '.payload.outcome == "skipped" and .payload.skip_reason == "cooldown"' >/dev/null
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@test "retrieval skipped when the embedder is unreachable" {
|
|
209
|
+
_index_session "past-4" \
|
|
210
|
+
"Yet another redash dashboard query that we had to fix the timezone on this morning to make the report run again." \
|
|
211
|
+
"ok"
|
|
212
|
+
|
|
213
|
+
# Turn off the stub so the probe fails.
|
|
214
|
+
HISTORIAN_STUB_OLLAMA_AVAILABLE=0 \
|
|
215
|
+
run bash -c "printf '%s' '$(_retrieve_input "redash dashboard timezone problem long enough to clear the prompt floor for retrieval")' | '$RETRIEVE_HOOK'"
|
|
216
|
+
[ "$status" -eq 0 ]
|
|
217
|
+
echo "$output" | jq -e '.hookSpecificOutput.additionalContext == ""' >/dev/null
|
|
218
|
+
|
|
219
|
+
grep -q '"event_type":"historian.embedder.unavailable"' "$ONLOOKER_EVENTS_LOG"
|
|
220
|
+
grep '"event_type":"historian.retrieval.complete"' "$ONLOOKER_EVENTS_LOG" \
|
|
221
|
+
| jq -e '.payload.outcome == "skipped" and .payload.skip_reason == "embedder_unavailable"' >/dev/null
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
@test "retrieval excludes chunks from the current session id" {
|
|
225
|
+
# Index the same session id we'll then query from — should be excluded.
|
|
226
|
+
_index_session "current" \
|
|
227
|
+
"Working on a redash dashboard right now in this very session of the test framework that we are running." \
|
|
228
|
+
"ok"
|
|
229
|
+
|
|
230
|
+
run bash -c "printf '%s' '$(_retrieve_input "redash dashboard timezone trouble inside this very session of the test framework")' | '$RETRIEVE_HOOK'"
|
|
231
|
+
[ "$status" -eq 0 ]
|
|
232
|
+
|
|
233
|
+
echo "$output" | jq -e '.hookSpecificOutput.additionalContext == ""' >/dev/null
|
|
234
|
+
grep '"event_type":"historian.retrieval.complete"' "$ONLOOKER_EVENTS_LOG" \
|
|
235
|
+
| jq -e '.payload.outcome == "empty"' >/dev/null
|
|
236
|
+
}
|
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
#!/usr/bin/env bats
|
|
2
|
+
#
|
|
3
|
+
# Exercises the substrate-level memory.recalled emitter. The hook fires
|
|
4
|
+
# on SessionStart and emits one canonical memory.recalled event per
|
|
5
|
+
# typed memory file in the project's typed memory store at
|
|
6
|
+
# ~/.claude/projects/<encoded>/memory/.
|
|
7
|
+
#
|
|
8
|
+
# Curator's usage tracker depends on this signal; without it,
|
|
9
|
+
# zero-recall findings can't be generated. The tests below pin both the
|
|
10
|
+
# happy path (correct count and provenance) and the skip cases (no git
|
|
11
|
+
# context, no memory store, compact source).
|
|
12
|
+
|
|
13
|
+
setup() {
|
|
14
|
+
source "${BATS_TEST_DIRNAME}/../helpers/setup.bash"
|
|
15
|
+
setup_test_env
|
|
16
|
+
|
|
17
|
+
export CLAUDE_PLUGIN_ROOT="$REPO_ROOT"
|
|
18
|
+
|
|
19
|
+
PROJECT_REPO="${BATS_TEST_TMPDIR}/repo"
|
|
20
|
+
mkdir -p "$PROJECT_REPO"
|
|
21
|
+
git -C "$PROJECT_REPO" init -q
|
|
22
|
+
git -C "$PROJECT_REPO" config user.email t@example.com
|
|
23
|
+
git -C "$PROJECT_REPO" config user.name "Test"
|
|
24
|
+
git -C "$PROJECT_REPO" remote add origin git@github.com:org/memory-recall-test.git
|
|
25
|
+
|
|
26
|
+
# Derive the encoded project dir under CLAUDE_HOME so the hook resolves
|
|
27
|
+
# via the path-encoding fallback (CLAUDE_PROJECT_ENCODED unset).
|
|
28
|
+
ABS_CWD=$(cd "$PROJECT_REPO" && pwd -P)
|
|
29
|
+
ENCODED=$(printf '%s' "$ABS_CWD" | sed -E 's#/#-#g')
|
|
30
|
+
MEM_DIR="${TEST_HOME}/.claude/projects/${ENCODED}/memory"
|
|
31
|
+
mkdir -p "$MEM_DIR"
|
|
32
|
+
|
|
33
|
+
ONLOOKER_EVENTS_LOG="${ONLOOKER_DIR}/logs/onlooker-events.jsonl"
|
|
34
|
+
HOOK="${REPO_ROOT}/scripts/hooks/memory-recall-tracker.sh"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
_input() {
|
|
38
|
+
local source="${1:-startup}"
|
|
39
|
+
jq -cn --arg cwd "$PROJECT_REPO" --arg sid "sess-mem-test" --arg source "$source" \
|
|
40
|
+
'{cwd:$cwd, session_id:$sid, source:$source, hook_event_name:"SessionStart"}'
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
_seed_memory() {
|
|
44
|
+
local fname="$1" type="$2" name="${3:-$fname}"
|
|
45
|
+
printf -- '---\nname: %s\ndescription: test\ntype: %s\n---\n\nBody.\n' \
|
|
46
|
+
"$name" "$type" > "${MEM_DIR}/${fname}"
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
@test "memory-recall emits one event per typed memory file" {
|
|
50
|
+
_seed_memory "user_role.md" "user"
|
|
51
|
+
_seed_memory "feedback_no_summaries.md" "feedback"
|
|
52
|
+
_seed_memory "project_auth_rewrite.md" "project"
|
|
53
|
+
_seed_memory "reference_dashboards.md" "reference"
|
|
54
|
+
|
|
55
|
+
run bash -c "printf '%s' '$(_input)' | '$HOOK'"
|
|
56
|
+
[ "$status" -eq 0 ]
|
|
57
|
+
|
|
58
|
+
local count
|
|
59
|
+
count=$(grep -c '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG")
|
|
60
|
+
[ "$count" -eq 4 ]
|
|
61
|
+
|
|
62
|
+
# One event per memory_type, with the right filename.
|
|
63
|
+
grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
64
|
+
| jq -e 'select(.payload.memory_type == "user" and .payload.memory_file == "user_role.md")' >/dev/null
|
|
65
|
+
grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
66
|
+
| jq -e 'select(.payload.memory_type == "feedback")' >/dev/null
|
|
67
|
+
grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
68
|
+
| jq -e 'select(.payload.memory_type == "project")' >/dev/null
|
|
69
|
+
grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
70
|
+
| jq -e 'select(.payload.memory_type == "reference")' >/dev/null
|
|
71
|
+
|
|
72
|
+
# recall_position values are 0..N-1, distinct.
|
|
73
|
+
local positions
|
|
74
|
+
positions=$(grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
75
|
+
| jq -r '.payload.recall_position' | sort -n | paste -sd, -)
|
|
76
|
+
[ "$positions" = "0,1,2,3" ]
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
@test "memory-recall skips MEMORY.md itself" {
|
|
80
|
+
_seed_memory "feedback_one.md" "feedback"
|
|
81
|
+
printf '%s\n' '- [One](feedback_one.md) — one' > "${MEM_DIR}/MEMORY.md"
|
|
82
|
+
|
|
83
|
+
run bash -c "printf '%s' '$(_input)' | '$HOOK'"
|
|
84
|
+
[ "$status" -eq 0 ]
|
|
85
|
+
|
|
86
|
+
# MEMORY.md is not its own memory; should NOT appear as a memory_file.
|
|
87
|
+
! grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
88
|
+
| jq -e 'select(.payload.memory_file == "MEMORY.md")' >/dev/null
|
|
89
|
+
local count
|
|
90
|
+
count=$(grep -c '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG")
|
|
91
|
+
[ "$count" -eq 1 ]
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
@test "memory-recall skips memories without a recognized type" {
|
|
95
|
+
_seed_memory "feedback_valid.md" "feedback"
|
|
96
|
+
# Memory with an unrecognized type field — should be silently dropped.
|
|
97
|
+
printf -- '---\nname: weird\ntype: unknown\n---\n\nBody.\n' \
|
|
98
|
+
> "${MEM_DIR}/weird.md"
|
|
99
|
+
# Memory with no frontmatter at all — also dropped.
|
|
100
|
+
printf '%s\n' 'just a body, no metadata' > "${MEM_DIR}/raw.md"
|
|
101
|
+
|
|
102
|
+
run bash -c "printf '%s' '$(_input)' | '$HOOK'"
|
|
103
|
+
[ "$status" -eq 0 ]
|
|
104
|
+
|
|
105
|
+
local count
|
|
106
|
+
count=$(grep -c '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG")
|
|
107
|
+
[ "$count" -eq 1 ]
|
|
108
|
+
grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
109
|
+
| jq -e '.payload.memory_file == "feedback_valid.md"' >/dev/null
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@test "memory-recall emits nothing when the memory store is empty" {
|
|
113
|
+
# MEM_DIR exists (created in setup) but contains no *.md files. The
|
|
114
|
+
# hook walks the glob, finds zero matches, and emits no events.
|
|
115
|
+
run bash -c "printf '%s' '$(_input)' | '$HOOK'"
|
|
116
|
+
[ "$status" -eq 0 ]
|
|
117
|
+
[ ! -f "$ONLOOKER_EVENTS_LOG" ] || ! grep -q '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG"
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
@test "memory-recall emits nothing when the memory store directory does not exist" {
|
|
121
|
+
# This is the genuinely-missing-directory branch — the dir check at
|
|
122
|
+
# the top of the hook short-circuits before any file walk.
|
|
123
|
+
rm -rf "$MEM_DIR"
|
|
124
|
+
[ ! -d "$MEM_DIR" ]
|
|
125
|
+
|
|
126
|
+
run bash -c "printf '%s' '$(_input)' | '$HOOK'"
|
|
127
|
+
[ "$status" -eq 0 ]
|
|
128
|
+
[ ! -f "$ONLOOKER_EVENTS_LOG" ] || ! grep -q '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG"
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
@test "memory-recall is a no-op when cwd is not a git repo" {
|
|
132
|
+
local non_git="${BATS_TEST_TMPDIR}/no-git"
|
|
133
|
+
mkdir -p "$non_git"
|
|
134
|
+
_seed_memory "user_x.md" "user"
|
|
135
|
+
|
|
136
|
+
local input
|
|
137
|
+
input=$(jq -cn --arg cwd "$non_git" --arg sid "s" --arg source "startup" \
|
|
138
|
+
'{cwd:$cwd, session_id:$sid, source:$source}')
|
|
139
|
+
|
|
140
|
+
run bash -c "printf '%s' '$input' | '$HOOK'"
|
|
141
|
+
[ "$status" -eq 0 ]
|
|
142
|
+
[ ! -f "$ONLOOKER_EVENTS_LOG" ] || ! grep -q '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG"
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
@test "memory-recall skips compact source to avoid double-counting" {
|
|
146
|
+
_seed_memory "user_x.md" "user"
|
|
147
|
+
|
|
148
|
+
run bash -c "printf '%s' '$(_input compact)' | '$HOOK'"
|
|
149
|
+
[ "$status" -eq 0 ]
|
|
150
|
+
[ ! -f "$ONLOOKER_EVENTS_LOG" ] || ! grep -q '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG"
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@test "memory-recall payload carries the same project_key for two clones" {
|
|
154
|
+
_seed_memory "user_x.md" "user"
|
|
155
|
+
|
|
156
|
+
run bash -c "printf '%s' '$(_input)' | '$HOOK'"
|
|
157
|
+
[ "$status" -eq 0 ]
|
|
158
|
+
|
|
159
|
+
local key
|
|
160
|
+
key=$(grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
161
|
+
| jq -r '.payload.project_key' | head -1)
|
|
162
|
+
|
|
163
|
+
# Second clone of the same remote at a different path. The key should
|
|
164
|
+
# match (SHA256 of remote URL, path-independent).
|
|
165
|
+
local clone2="${BATS_TEST_TMPDIR}/clone2"
|
|
166
|
+
mkdir -p "$clone2"
|
|
167
|
+
git -C "$clone2" init -q
|
|
168
|
+
git -C "$clone2" remote add origin git@github.com:org/memory-recall-test.git
|
|
169
|
+
|
|
170
|
+
local ABS_CWD2 ENCODED2 MEM_DIR2
|
|
171
|
+
ABS_CWD2=$(cd "$clone2" && pwd -P)
|
|
172
|
+
ENCODED2=$(printf '%s' "$ABS_CWD2" | sed -E 's#/#-#g')
|
|
173
|
+
MEM_DIR2="${TEST_HOME}/.claude/projects/${ENCODED2}/memory"
|
|
174
|
+
mkdir -p "$MEM_DIR2"
|
|
175
|
+
printf -- '---\nname: x\ntype: user\n---\n\nBody.\n' > "${MEM_DIR2}/user_x.md"
|
|
176
|
+
|
|
177
|
+
rm -f "$ONLOOKER_EVENTS_LOG"
|
|
178
|
+
local input2
|
|
179
|
+
input2=$(jq -cn --arg cwd "$clone2" --arg sid "s" --arg source "startup" \
|
|
180
|
+
'{cwd:$cwd, session_id:$sid, source:$source}')
|
|
181
|
+
run bash -c "printf '%s' '$input2' | '$HOOK'"
|
|
182
|
+
[ "$status" -eq 0 ]
|
|
183
|
+
|
|
184
|
+
local key2
|
|
185
|
+
key2=$(grep '"event_type":"memory.recalled"' "$ONLOOKER_EVENTS_LOG" \
|
|
186
|
+
| jq -r '.payload.project_key' | head -1)
|
|
187
|
+
|
|
188
|
+
[ "$key" = "$key2" ]
|
|
189
|
+
}
|