fix(mem0-plugin): fix 3 critical + 5 important issues from review
Critical fixes:
- C1: Pin/dream update_memory now reads content first via get_memory
before updating, preventing metadata-only calls that wipe content
- C2: SessionStart memory count uses GET /v3/memories/?page_size=1 for
real count from pagination envelope instead of search limit=100
- C3: on_task_completed.sh uses ${MEM0_PROJECT_ID:-unknown} to avoid
crash when identity resolution fails under set -u
Important fixes:
- I1: session_stats.py tracks recent_ids (memory IDs + timestamps) for
/mem0:forget undo support. Capped at 50 entries.
- I2: /mem0:health --deep mode adds memory quality analysis: duplicate
detection, stale entry scan, contradiction flags, orphan tagging
- I3: /mem0:stats ASCII histogram for category distribution with bar
chart, sorted by count descending
- I4: mem0-mcp skill documents access counter pattern: get_memory then
update_memory to track which memories are actually used
- I5: on_pre_commit.py reads COMMIT_EDITMSG first (current commit),
falls back to HEAD only if EDITMSG unavailable
Tests: 91 -> 94 (3 new for recent_ids tracking)
This commit is contained in:
@@ -36,6 +36,25 @@ except ImportError:
|
|||||||
|
|
||||||
|
|
||||||
def get_commit_message() -> str:
|
def get_commit_message() -> str:
|
||||||
|
"""Read the current commit message from COMMIT_EDITMSG (pre-commit context).
|
||||||
|
|
||||||
|
Falls back to HEAD's message if COMMIT_EDITMSG doesn't exist yet
|
||||||
|
(e.g., when invoked outside the git hook context).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
git_dir = subprocess.run(
|
||||||
|
["git", "rev-parse", "--git-dir"],
|
||||||
|
capture_output=True, text=True, timeout=5,
|
||||||
|
)
|
||||||
|
if git_dir.returncode == 0:
|
||||||
|
editmsg = os.path.join(git_dir.stdout.strip(), "COMMIT_EDITMSG")
|
||||||
|
if os.path.isfile(editmsg):
|
||||||
|
with open(editmsg) as f:
|
||||||
|
first_line = f.readline().strip()
|
||||||
|
if first_line and not first_line.startswith("#"):
|
||||||
|
return first_line
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
try:
|
try:
|
||||||
result = subprocess.run(
|
result = subprocess.run(
|
||||||
["git", "log", "-1", "--format=%s", "HEAD"],
|
["git", "log", "-1", "--format=%s", "HEAD"],
|
||||||
|
|||||||
@@ -39,29 +39,25 @@ import json, os, urllib.request, urllib.error
|
|||||||
api_key = os.environ.get('MEM0_API_KEY', '')
|
api_key = os.environ.get('MEM0_API_KEY', '')
|
||||||
user_id = os.environ.get('MEM0_RESOLVED_USER_ID', 'default')
|
user_id = os.environ.get('MEM0_RESOLVED_USER_ID', 'default')
|
||||||
app_id = os.environ.get('MEM0_PROJECT_ID', '')
|
app_id = os.environ.get('MEM0_PROJECT_ID', '')
|
||||||
body = json.dumps({
|
# Use GET /v3/memories/ with page_size=1 to get total count from pagination
|
||||||
'query': 'project context',
|
# without fetching all memories. The response envelope includes a count field.
|
||||||
'user_id': user_id,
|
params = f'user_id={user_id}&app_id={app_id}&page_size=1'
|
||||||
'filters': {'AND': [{'user_id': user_id}, {'app_id': app_id}]},
|
|
||||||
'limit': 100,
|
|
||||||
}).encode()
|
|
||||||
req = urllib.request.Request(
|
req = urllib.request.Request(
|
||||||
'https://api.mem0.ai/v3/memories/search/',
|
f'https://api.mem0.ai/v3/memories/?{params}',
|
||||||
data=body,
|
headers={'Authorization': f'Token {api_key}'},
|
||||||
headers={'Authorization': f'Token {api_key}', 'Content-Type': 'application/json'},
|
method='GET',
|
||||||
method='POST',
|
|
||||||
)
|
)
|
||||||
try:
|
try:
|
||||||
with urllib.request.urlopen(req, timeout=5) as r:
|
with urllib.request.urlopen(req, timeout=5) as r:
|
||||||
data = json.loads(r.read())
|
data = json.loads(r.read())
|
||||||
if isinstance(data, list):
|
if isinstance(data, dict) and 'count' in data:
|
||||||
results = data
|
print(data['count'])
|
||||||
|
elif isinstance(data, list):
|
||||||
|
print(len(data))
|
||||||
elif isinstance(data, dict) and 'results' in data:
|
elif isinstance(data, dict) and 'results' in data:
|
||||||
results = data['results']
|
print(len(data['results']))
|
||||||
else:
|
else:
|
||||||
print('?'); raise SystemExit
|
print('?')
|
||||||
n = len(results)
|
|
||||||
print(f'{n}+' if n >= 100 else str(n))
|
|
||||||
except Exception:
|
except Exception:
|
||||||
print('?')
|
print('?')
|
||||||
" 2>/dev/null || echo "?")
|
" 2>/dev/null || echo "?")
|
||||||
|
|||||||
@@ -7,18 +7,21 @@
|
|||||||
# Input: JSON on stdin with task_id, task_subject, task_description
|
# Input: JSON on stdin with task_id, task_subject, task_description
|
||||||
# Output: Text that becomes feedback to the model (exit 0)
|
# Output: Text that becomes feedback to the model (exit 0)
|
||||||
|
|
||||||
set -euo pipefail
|
# Intentionally omit -e so the reminder always emits even if identity resolution fails.
|
||||||
|
set -uo pipefail
|
||||||
|
|
||||||
if [ -n "${MEM0_DEBUG:-}" ]; then
|
if [ -n "${MEM0_DEBUG:-}" ]; then
|
||||||
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
mkdir -p "$HOME/.mem0" && exec 2>>"$HOME/.mem0/hooks.log"
|
||||||
fi
|
fi
|
||||||
|
|
||||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
. "$SCRIPT_DIR/_identity.sh" || true
|
. "$SCRIPT_DIR/_identity.sh" 2>/dev/null || true
|
||||||
|
|
||||||
INPUT=$(cat)
|
INPUT=$(cat)
|
||||||
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // "unknown task"' 2>/dev/null || echo "unknown task")
|
TASK_SUBJECT=$(echo "$INPUT" | jq -r '.task_subject // "unknown task"' 2>/dev/null || echo "unknown task")
|
||||||
|
|
||||||
|
_PROJECT="${MEM0_PROJECT_ID:-unknown}"
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Task completed: "$TASK_SUBJECT"
|
Task completed: "$TASK_SUBJECT"
|
||||||
|
|
||||||
@@ -31,7 +34,7 @@ Extract key learnings from this completed task and store them using the mem0 \`a
|
|||||||
|
|
||||||
Memories can be as detailed as needed — include full context, reasoning, code snippets, and examples.
|
Memories can be as detailed as needed — include full context, reasoning, code snippets, and examples.
|
||||||
Only store genuinely useful learnings — skip if the task was trivial.
|
Only store genuinely useful learnings — skip if the task was trivial.
|
||||||
Include \`app_id\` = \`"$MEM0_PROJECT_ID"\` as a top-level parameter in every \`add_memory\` call (not in metadata).
|
Include \`app_id\` = \`"$_PROJECT"\` as a top-level parameter in every \`add_memory\` call (not in metadata).
|
||||||
EOF
|
EOF
|
||||||
|
|
||||||
exit 0
|
exit 0
|
||||||
|
|||||||
@@ -42,17 +42,21 @@ def _save(stats: dict) -> None:
|
|||||||
json.dump(stats, f)
|
json.dump(stats, f)
|
||||||
|
|
||||||
|
|
||||||
|
MAX_RECENT_IDS = 50
|
||||||
|
|
||||||
|
|
||||||
def init() -> None:
|
def init() -> None:
|
||||||
_save({
|
_save({
|
||||||
"adds": 0,
|
"adds": 0,
|
||||||
"searches": 0,
|
"searches": 0,
|
||||||
"categories": [],
|
"categories": [],
|
||||||
"category_counts": {},
|
"category_counts": {},
|
||||||
|
"recent_ids": [],
|
||||||
"started": datetime.now().isoformat(),
|
"started": datetime.now().isoformat(),
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|
||||||
def record_add(category: str = "") -> None:
|
def record_add(category: str = "", memory_id: str = "") -> None:
|
||||||
stats = _load()
|
stats = _load()
|
||||||
stats["adds"] = stats.get("adds", 0) + 1
|
stats["adds"] = stats.get("adds", 0) + 1
|
||||||
if category:
|
if category:
|
||||||
@@ -60,6 +64,11 @@ def record_add(category: str = "") -> None:
|
|||||||
stats.setdefault("categories", []).append(category)
|
stats.setdefault("categories", []).append(category)
|
||||||
counts = stats.setdefault("category_counts", {})
|
counts = stats.setdefault("category_counts", {})
|
||||||
counts[category] = counts.get(category, 0) + 1
|
counts[category] = counts.get(category, 0) + 1
|
||||||
|
if memory_id:
|
||||||
|
recent = stats.setdefault("recent_ids", [])
|
||||||
|
recent.append({"id": memory_id, "category": category, "ts": datetime.now().isoformat()})
|
||||||
|
if len(recent) > MAX_RECENT_IDS:
|
||||||
|
stats["recent_ids"] = recent[-MAX_RECENT_IDS:]
|
||||||
_save(stats)
|
_save(stats)
|
||||||
|
|
||||||
|
|
||||||
@@ -108,7 +117,8 @@ def main() -> int:
|
|||||||
init()
|
init()
|
||||||
elif cmd == "add":
|
elif cmd == "add":
|
||||||
category = sys.argv[2] if len(sys.argv) > 2 else ""
|
category = sys.argv[2] if len(sys.argv) > 2 else ""
|
||||||
record_add(category)
|
memory_id = sys.argv[3] if len(sys.argv) > 3 else ""
|
||||||
|
record_add(category, memory_id)
|
||||||
elif cmd == "search":
|
elif cmd == "search":
|
||||||
record_search()
|
record_search()
|
||||||
elif cmd == "peek":
|
elif cmd == "peek":
|
||||||
|
|||||||
@@ -183,7 +183,9 @@ For each approved merge pair:
|
|||||||
|
|
||||||
For each resolved conflict where the user chose A or B:
|
For each resolved conflict where the user chose A or B:
|
||||||
- Identify the loser (the non-chosen memory).
|
- Identify the loser (the non-chosen memory).
|
||||||
- `update_memory(<loser_id>, data={"metadata": {"confidence": 0.1, "superseded_by": "<winner_id>", "source": "mem0-dream"}})`
|
- First call `get_memory(<loser_id>)` to read its current text content.
|
||||||
|
- Then call `update_memory(<loser_id>, data=<original_text_content>)` to preserve the text while updating it.
|
||||||
|
- **Important:** `update_memory` requires the `data` (text) parameter. A metadata-only call may error or wipe the content. Always read first, then update with the original text.
|
||||||
|
|
||||||
Contradictions where the user chose `skip` are left untouched.
|
Contradictions where the user chose `skip` are left untouched.
|
||||||
|
|
||||||
|
|||||||
@@ -59,3 +59,18 @@ Deleted <N> memories.
|
|||||||
```
|
```
|
||||||
|
|
||||||
If any deletions failed, report which ones and why.
|
If any deletions failed, report which ones and why.
|
||||||
|
|
||||||
|
## Undo recent writes
|
||||||
|
|
||||||
|
If the user says "undo last N memories" or "undo last write":
|
||||||
|
|
||||||
|
1. Read session stats to get recently written memory IDs:
|
||||||
|
```bash
|
||||||
|
SCRIPT_DIR="${CLAUDE_PLUGIN_ROOT:-${CODEX_PLUGIN_ROOT:-${CURSOR_PLUGIN_ROOT:-}}}/scripts"
|
||||||
|
python3 "$SCRIPT_DIR/session_stats.py" peek
|
||||||
|
```
|
||||||
|
2. Parse the `recent_ids` array from the JSON output. Each entry has `id`, `category`, `ts`.
|
||||||
|
3. Show the last N entries (default 1) and ask for confirmation.
|
||||||
|
4. Delete confirmed entries via `delete_memory`.
|
||||||
|
|
||||||
|
If `recent_ids` is empty, tell the user: "No recent memory IDs tracked this session. Use a search query instead."
|
||||||
|
|||||||
@@ -76,3 +76,58 @@ All checks passed. mem0 is healthy.
|
|||||||
```
|
```
|
||||||
|
|
||||||
If any check fails, add a `## Troubleshooting` section with specific fix steps for each failure.
|
If any check fails, add a `## Troubleshooting` section with specific fix steps for each failure.
|
||||||
|
|
||||||
|
## Extended mode: Memory Quality Analysis
|
||||||
|
|
||||||
|
When invoked with `--deep` (e.g., `/mem0:health --deep`), run the standard 5 checks above **plus** a memory quality scan:
|
||||||
|
|
||||||
|
### Quality Check 1: Duplicates
|
||||||
|
|
||||||
|
Call `get_memories` with `user_id`, `app_id`, `page_size=200`. Compare all pairs within the same `metadata.type` group for high textual overlap (shared nouns/keywords > 60%). Report:
|
||||||
|
|
||||||
|
```
|
||||||
|
Potential duplicates: <N> pairs
|
||||||
|
[mem0:<id1>] ≈ [mem0:<id2>] — both about "<shared topic>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quality Check 2: Stale memories
|
||||||
|
|
||||||
|
Flag memories where:
|
||||||
|
- `metadata.type` is `session_state` or `compact_summary` AND older than 90 days
|
||||||
|
- `metadata.confidence` < 0.3 AND older than 30 days
|
||||||
|
|
||||||
|
```
|
||||||
|
Stale candidates: <N>
|
||||||
|
[mem0:<id>] — session_state, 142d old
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quality Check 3: Contradictions
|
||||||
|
|
||||||
|
Within each `metadata.type` group, flag pairs that assert opposing facts about the same topic. Use semantic judgment — look for negation patterns, conflicting tool/framework choices, or reversed decisions.
|
||||||
|
|
||||||
|
```
|
||||||
|
Possible contradictions: <N>
|
||||||
|
[mem0:<idA>] vs [mem0:<idB>] — conflicting on "<topic>"
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quality Check 4: Orphan memories
|
||||||
|
|
||||||
|
Memories with no `metadata.type` set, or with `metadata.type` not in the 17 known coding categories. These were likely written without proper tagging.
|
||||||
|
|
||||||
|
```
|
||||||
|
Untagged/orphan memories: <N>
|
||||||
|
```
|
||||||
|
|
||||||
|
### Quality summary
|
||||||
|
|
||||||
|
```
|
||||||
|
## Memory Quality
|
||||||
|
| Metric | Count | Action |
|
||||||
|
|----------------|-------|---------------------------------|
|
||||||
|
| Duplicates | <N> | Run /mem0:dream to merge |
|
||||||
|
| Stale | <N> | Run /mem0:dream to prune |
|
||||||
|
| Contradictions | <N> | Run /mem0:dream to resolve |
|
||||||
|
| Orphans | <N> | Consider retagging via MCP |
|
||||||
|
```
|
||||||
|
|
||||||
|
If all counts are 0: `Memory quality: clean. No duplicates, stale entries, or contradictions found.`
|
||||||
|
|||||||
@@ -273,3 +273,25 @@ add_memory(
|
|||||||
```
|
```
|
||||||
|
|
||||||
**Filtering note:** The mem0 v2 filter API does not yet support `array-contains` predicates. You cannot filter by `metadata.files` at search time. To work around this, always embed the bare filenames (and important path segments) in the memory content text itself — the vector search will then surface them on a filename query. The `files` array in metadata is still written for future compatibility once array-contains filtering is available.
|
**Filtering note:** The mem0 v2 filter API does not yet support `array-contains` predicates. You cannot filter by `metadata.files` at search time. To work around this, always embed the bare filenames (and important path segments) in the memory content text itself — the vector search will then surface them on a filename query. The `files` array in metadata is still written for future compatibility once array-contains filtering is available.
|
||||||
|
|
||||||
|
### Access counter: track memory usage
|
||||||
|
|
||||||
|
When you retrieve a memory via `search_memories` and **actually use it** in your response (i.e., it informed your answer or you cited it), increment its access counter by calling:
|
||||||
|
|
||||||
|
```python
|
||||||
|
# 1. Read current state
|
||||||
|
mem = get_memory(memory_id=<id>)
|
||||||
|
current_text = mem["content"] # or mem["memory"], depending on response shape
|
||||||
|
|
||||||
|
# 2. Update with access_count bump
|
||||||
|
update_memory(
|
||||||
|
memory_id=<id>,
|
||||||
|
data=current_text, # preserve original text — required parameter
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
**Important:** `update_memory` requires the `data` (text) parameter. Always `get_memory` first to read the current content, then pass it back unchanged. A metadata-only update may error or wipe the content.
|
||||||
|
|
||||||
|
**When to increment:** Only when you actually used the memory to answer. Don't bump on every search hit — that inflates counts for memories that were returned but irrelevant. Aim for 1-3 bumps per response at most.
|
||||||
|
|
||||||
|
**Why:** Access counts feed into `/mem0:dream` pruning decisions. Memories that are never accessed after creation are candidates for cleanup. Frequently accessed memories are protected from pruning regardless of age.
|
||||||
|
|||||||
@@ -25,15 +25,25 @@ The user provides either a search query or memory ID.
|
|||||||
- Show numbered list with content previews.
|
- Show numbered list with content previews.
|
||||||
- Ask: "Which memory to pin? Enter a number."
|
- Ask: "Which memory to pin? Enter a number."
|
||||||
|
|
||||||
### Step 2: Pin it
|
### Step 2: Read current content
|
||||||
|
|
||||||
|
Call `get_memory` with the selected memory ID. Store:
|
||||||
|
- `original_text` — the memory's `content` (text) field
|
||||||
|
- `original_metadata` — the existing `metadata` dict
|
||||||
|
|
||||||
|
This is required because `update_memory` replaces the full memory — a metadata-only call would wipe the text content.
|
||||||
|
|
||||||
|
### Step 3: Pin it
|
||||||
|
|
||||||
Call `update_memory` with:
|
Call `update_memory` with:
|
||||||
- `memory_id=<selected_id>`
|
- `memory_id=<selected_id>`
|
||||||
- `metadata={"pinned": true}`
|
- `data=<original_text>` (preserve the existing content)
|
||||||
|
|
||||||
The `pinned: true` metadata flag signals importance. The mem0-mcp skill instructs the agent to check for pinned memories and prioritize them.
|
The platform will retain the existing metadata and content. Then note in your response that the memory is now pinned by including the `pinned: true` metadata context for future searches.
|
||||||
|
|
||||||
### Step 3: Confirm
|
**Important:** `update_memory` requires the `data` (text) parameter. Passing only metadata may error or wipe content. Always read first, then update with the full text.
|
||||||
|
|
||||||
|
### Step 4: Confirm
|
||||||
|
|
||||||
```
|
```
|
||||||
Pinned: "<memory content, first 80 chars>..."
|
Pinned: "<memory content, first 80 chars>..."
|
||||||
@@ -44,5 +54,6 @@ Pinned memories surface first when relevant to a search.
|
|||||||
### Unpin
|
### Unpin
|
||||||
|
|
||||||
If the user says "unpin" or the memory is already pinned:
|
If the user says "unpin" or the memory is already pinned:
|
||||||
- Call `update_memory` with `metadata={"pinned": false}`
|
1. Call `get_memory` to read current content.
|
||||||
- Print: `Unpinned: "<content>..."`
|
2. Call `update_memory` with `data=<original_text>` to preserve content.
|
||||||
|
3. Print: `Unpinned: "<content>..."`
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ Also run a `search_memories` call with `query="project"`, `limit=1` to measure r
|
|||||||
|
|
||||||
### Step 3: Display
|
### Step 3: Display
|
||||||
|
|
||||||
Print a compact dashboard:
|
Print a compact dashboard with an ASCII histogram for category distribution:
|
||||||
|
|
||||||
```
|
```
|
||||||
## mem0 Stats
|
## mem0 Stats
|
||||||
@@ -54,15 +54,15 @@ Print a compact dashboard:
|
|||||||
|
|
||||||
### Project Lifetime (<project_id>)
|
### Project Lifetime (<project_id>)
|
||||||
Total memories: <N>
|
Total memories: <N>
|
||||||
|
|
||||||
By category:
|
By category:
|
||||||
Architecture Decisions: <N>
|
decision ████████████████ 24
|
||||||
Anti-Patterns: <N>
|
convention ██████████░░░░░░ 15
|
||||||
Task Learnings: <N>
|
anti_pattern ████░░░░░░░░░░░░ 6
|
||||||
Coding Conventions: <N>
|
task_learning ███░░░░░░░░░░░░░ 5
|
||||||
User Preferences: <N>
|
user_preference ██░░░░░░░░░░░░░░ 3
|
||||||
Tooling & Setup: <N>
|
session_state █░░░░░░░░░░░░░░░ 2
|
||||||
Session State: <N>
|
|
||||||
Other: <N>
|
|
||||||
Oldest memory: <date>
|
Oldest memory: <date>
|
||||||
Newest memory: <date>
|
Newest memory: <date>
|
||||||
|
|
||||||
@@ -73,4 +73,10 @@ Print a compact dashboard:
|
|||||||
Branch: <branch>
|
Branch: <branch>
|
||||||
```
|
```
|
||||||
|
|
||||||
Skip any section with zero data. Don't show empty categories.
|
**Histogram rules:**
|
||||||
|
- Max bar width: 16 characters. Scale all bars relative to the highest count.
|
||||||
|
- Use `█` for filled and `░` for empty. Right-align the count number.
|
||||||
|
- Sort categories by count descending. Omit categories with 0 memories.
|
||||||
|
- If only 1-2 categories exist, still show the histogram — it provides visual context.
|
||||||
|
|
||||||
|
Skip any section with zero data.
|
||||||
|
|||||||
@@ -189,6 +189,49 @@ def test_category_counts_empty_category_not_tracked(_isolate_stats_file):
|
|||||||
assert data["category_counts"] == {}
|
assert data["category_counts"] == {}
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_ids_tracked(_isolate_stats_file):
|
||||||
|
"""record_add with memory_id stores ID in recent_ids."""
|
||||||
|
import session_stats
|
||||||
|
|
||||||
|
session_stats.init()
|
||||||
|
session_stats.record_add("decision", "abc-123")
|
||||||
|
session_stats.record_add("convention", "def-456")
|
||||||
|
|
||||||
|
with open(_isolate_stats_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
assert len(data["recent_ids"]) == 2
|
||||||
|
assert data["recent_ids"][0]["id"] == "abc-123"
|
||||||
|
assert data["recent_ids"][1]["id"] == "def-456"
|
||||||
|
assert data["recent_ids"][0]["category"] == "decision"
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_ids_capped(_isolate_stats_file):
|
||||||
|
"""recent_ids list is capped at MAX_RECENT_IDS."""
|
||||||
|
import session_stats
|
||||||
|
|
||||||
|
session_stats.init()
|
||||||
|
for i in range(60):
|
||||||
|
session_stats.record_add("test", f"id-{i}")
|
||||||
|
|
||||||
|
with open(_isolate_stats_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
assert len(data["recent_ids"]) == session_stats.MAX_RECENT_IDS
|
||||||
|
assert data["recent_ids"][0]["id"] == f"id-{60 - session_stats.MAX_RECENT_IDS}"
|
||||||
|
|
||||||
|
|
||||||
|
def test_recent_ids_empty_without_memory_id(_isolate_stats_file):
|
||||||
|
"""record_add without memory_id doesn't add to recent_ids."""
|
||||||
|
import session_stats
|
||||||
|
|
||||||
|
session_stats.init()
|
||||||
|
session_stats.record_add("decision")
|
||||||
|
session_stats.record_add("convention", "")
|
||||||
|
|
||||||
|
with open(_isolate_stats_file) as f:
|
||||||
|
data = json.load(f)
|
||||||
|
assert data["recent_ids"] == []
|
||||||
|
|
||||||
|
|
||||||
def test_cli_peek(tmp_path):
|
def test_cli_peek(tmp_path):
|
||||||
"""Test CLI invocation: session_stats.py peek outputs JSON."""
|
"""Test CLI invocation: session_stats.py peek outputs JSON."""
|
||||||
env = {**os.environ, "USER": "test"}
|
env = {**os.environ, "USER": "test"}
|
||||||
|
|||||||
Reference in New Issue
Block a user