chore: import upstream snapshot with attribution
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
pi-agent-plugin checks / lint (push) Has been cancelled
pi-agent-plugin checks / test (20) (push) Has been cancelled
pi-agent-plugin checks / test (22) (push) Has been cancelled
pi-agent-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / check_changes (push) Has been cancelled
TypeScript SDK CI / changelog_check (push) Has been cancelled
ci / changelog_check (push) Has been cancelled
ci / check_changes (push) Has been cancelled
ci / build_mem0 (3.10) (push) Has been cancelled
ci / build_mem0 (3.11) (push) Has been cancelled
ci / build_mem0 (3.12) (push) Has been cancelled
CLI Node CI / lint (push) Has been cancelled
CLI Node CI / test (20) (push) Has been cancelled
CLI Node CI / test (22) (push) Has been cancelled
CLI Node CI / build (push) Has been cancelled
CLI Python CI / lint (push) Has been cancelled
CLI Python CI / test (3.10) (push) Has been cancelled
CLI Python CI / test (3.11) (push) Has been cancelled
CLI Python CI / test (3.12) (push) Has been cancelled
CLI Python CI / build (push) Has been cancelled
openclaw checks / lint (push) Has been cancelled
openclaw checks / test (20) (push) Has been cancelled
openclaw checks / test (22) (push) Has been cancelled
openclaw checks / build (push) Has been cancelled
opencode-plugin checks / build (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / build_ts_sdk (22) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (20) (push) Has been cancelled
TypeScript SDK CI / integration_ts_sdk (22) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,298 @@
|
||||
"""Tests for parse_export_file.py — mem0 export file parser."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
import subprocess
|
||||
import sys
|
||||
|
||||
SCRIPTS_DIR = os.path.join(os.path.dirname(__file__), "..", "scripts")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Direct function tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_parse_blocks_single_valid_block():
|
||||
"""parse_blocks returns one record for a single valid block."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
content = """\
|
||||
---
|
||||
id: abc123
|
||||
created_at: 2024-01-15T10:00:00Z
|
||||
type: task_learnings
|
||||
confidence: 0.85
|
||||
branch: main
|
||||
files: src/foo.py, src/bar.py
|
||||
categories: coding_conventions, task_learnings
|
||||
---
|
||||
Always use context managers when opening files.
|
||||
"""
|
||||
records = parse_blocks(content)
|
||||
assert len(records) == 1
|
||||
r = records[0]
|
||||
assert r["id"] == "abc123"
|
||||
assert r["type"] == "task_learnings"
|
||||
assert r["confidence"] == "0.85"
|
||||
assert r["branch"] == "main"
|
||||
assert r["files"] == ["src/foo.py", "src/bar.py"]
|
||||
assert r["categories"] == ["coding_conventions", "task_learnings"]
|
||||
assert "Always use context managers" in r["content"]
|
||||
|
||||
|
||||
def test_parse_blocks_multiple_blocks():
|
||||
"""parse_blocks returns the correct number of records for multiple blocks."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
content = """\
|
||||
---
|
||||
id: mem001
|
||||
type: architecture_decisions
|
||||
confidence: 0.9
|
||||
branch: main
|
||||
files:
|
||||
categories: architecture_decisions
|
||||
---
|
||||
Use hexagonal architecture for the core domain.
|
||||
|
||||
---
|
||||
id: mem002
|
||||
type: anti_patterns
|
||||
confidence: 0.75
|
||||
branch: feat/refactor
|
||||
files: src/legacy.py
|
||||
categories: anti_patterns
|
||||
---
|
||||
Avoid direct database calls from view layer.
|
||||
|
||||
---
|
||||
id: mem003
|
||||
type: coding_conventions
|
||||
confidence: 0.8
|
||||
branch: main
|
||||
files: src/utils.py, src/helpers.py
|
||||
categories:
|
||||
---
|
||||
Use snake_case for all Python identifiers.
|
||||
"""
|
||||
records = parse_blocks(content)
|
||||
assert len(records) == 3
|
||||
|
||||
assert records[0]["id"] == "mem001"
|
||||
assert records[0]["categories"] == ["architecture_decisions"]
|
||||
assert "hexagonal architecture" in records[0]["content"]
|
||||
|
||||
assert records[1]["id"] == "mem002"
|
||||
assert records[1]["files"] == ["src/legacy.py"]
|
||||
assert "direct database calls" in records[1]["content"]
|
||||
|
||||
assert records[2]["id"] == "mem003"
|
||||
assert records[2]["files"] == ["src/utils.py", "src/helpers.py"]
|
||||
assert records[2]["categories"] == []
|
||||
assert "snake_case" in records[2]["content"]
|
||||
|
||||
|
||||
def test_parse_blocks_missing_optional_fields():
|
||||
"""parse_blocks uses defaults when optional fields are absent."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
# confidence, branch, files, categories all absent
|
||||
content = """\
|
||||
---
|
||||
id: xyz789
|
||||
type: task_learnings
|
||||
---
|
||||
Run tests before committing.
|
||||
"""
|
||||
records = parse_blocks(content)
|
||||
assert len(records) == 1
|
||||
r = records[0]
|
||||
assert r["id"] == "xyz789"
|
||||
assert r["confidence"] == "" # default empty string
|
||||
assert r["branch"] == "" # default empty string
|
||||
assert r["files"] == [] # default empty list
|
||||
assert r["categories"] == [] # default empty list
|
||||
assert "Run tests" in r["content"]
|
||||
|
||||
|
||||
def test_parse_blocks_filters_empty_content():
|
||||
"""parse_blocks skips blocks whose content is empty or whitespace-only."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
content = """\
|
||||
---
|
||||
id: empty1
|
||||
type: task_learnings
|
||||
---
|
||||
|
||||
---
|
||||
id: real1
|
||||
type: task_learnings
|
||||
---
|
||||
This block has real content.
|
||||
|
||||
---
|
||||
id: empty2
|
||||
type: coding_conventions
|
||||
---
|
||||
|
||||
"""
|
||||
records = parse_blocks(content)
|
||||
# Only the block with actual content should be returned
|
||||
assert len(records) == 1
|
||||
assert records[0]["id"] == "real1"
|
||||
assert "real content" in records[0]["content"]
|
||||
|
||||
|
||||
def test_parse_blocks_round_trip():
|
||||
"""Content formatted by export matches what parse_blocks expects."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
# Simulate the exact format produced by the export skill
|
||||
memory_id = "test-id-001"
|
||||
created_at = "2024-06-01T12:00:00Z"
|
||||
mem_type = "architecture_decisions"
|
||||
confidence = "0.92"
|
||||
branch = "feat/new-feature"
|
||||
files = ["src/main.py", "tests/test_main.py"]
|
||||
categories = ["architecture_decisions", "coding_conventions"]
|
||||
memory_content = "Use dependency injection for all service classes."
|
||||
|
||||
# Format exactly as the export skill would
|
||||
block = (
|
||||
"---\n"
|
||||
f"id: {memory_id}\n"
|
||||
f"created_at: {created_at}\n"
|
||||
f"type: {mem_type}\n"
|
||||
f"confidence: {confidence}\n"
|
||||
f"branch: {branch}\n"
|
||||
f"files: {', '.join(files)}\n"
|
||||
f"categories: {', '.join(categories)}\n"
|
||||
"---\n"
|
||||
f"{memory_content}\n"
|
||||
"\n"
|
||||
)
|
||||
|
||||
records = parse_blocks(block)
|
||||
assert len(records) == 1
|
||||
r = records[0]
|
||||
assert r["id"] == memory_id
|
||||
assert r["type"] == mem_type
|
||||
assert r["confidence"] == confidence
|
||||
assert r["branch"] == branch
|
||||
assert r["files"] == files
|
||||
assert r["categories"] == categories
|
||||
assert r["content"] == memory_content
|
||||
|
||||
|
||||
def test_parse_blocks_multiline_content():
|
||||
"""parse_blocks correctly captures multi-line memory content."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
content = """\
|
||||
---
|
||||
id: multi001
|
||||
type: task_learnings
|
||||
---
|
||||
Line one of the memory.
|
||||
Line two of the memory.
|
||||
|
||||
Line four after blank line.
|
||||
"""
|
||||
records = parse_blocks(content)
|
||||
assert len(records) == 1
|
||||
assert "Line one" in records[0]["content"]
|
||||
assert "Line two" in records[0]["content"]
|
||||
assert "Line four" in records[0]["content"]
|
||||
|
||||
|
||||
def test_parse_blocks_empty_input():
|
||||
"""parse_blocks returns empty list for empty input."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
assert parse_blocks("") == []
|
||||
assert parse_blocks(" \n ") == []
|
||||
|
||||
|
||||
def test_parse_blocks_no_blocks():
|
||||
"""parse_blocks returns empty list for content without any --- delimiters."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
assert parse_blocks("Just some text without any delimiters.") == []
|
||||
|
||||
|
||||
def test_parse_blocks_value_with_colon():
|
||||
"""parse_blocks handles values that themselves contain colons."""
|
||||
from parse_export_file import parse_blocks
|
||||
|
||||
content = """\
|
||||
---
|
||||
id: colon-test
|
||||
type: task_learnings
|
||||
created_at: 2024-01-01T10:00:00Z
|
||||
---
|
||||
Timestamp values contain colons and should parse correctly.
|
||||
"""
|
||||
records = parse_blocks(content)
|
||||
assert len(records) == 1
|
||||
assert records[0]["id"] == "colon-test"
|
||||
# created_at field should be captured (it's in the record if present)
|
||||
assert "2024-01-01T10:00:00Z" in records[0].get("created_at", "")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# CLI / subprocess tests
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_main_cli_outputs_json(tmp_path):
|
||||
"""Running parse_export_file.py as a script outputs valid JSON."""
|
||||
export_file = tmp_path / "mem0-export-test.md"
|
||||
export_file.write_text("""\
|
||||
---
|
||||
id: cli-test-001
|
||||
type: task_learnings
|
||||
confidence: 0.8
|
||||
branch: main
|
||||
files:
|
||||
categories: task_learnings
|
||||
---
|
||||
Prefer composition over inheritance.
|
||||
""")
|
||||
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(SCRIPTS_DIR, "parse_export_file.py"), str(export_file)],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
records = json.loads(result.stdout)
|
||||
assert isinstance(records, list)
|
||||
assert len(records) == 1
|
||||
assert records[0]["id"] == "cli-test-001"
|
||||
|
||||
|
||||
def test_main_cli_no_args_exits_zero():
|
||||
"""Running parse_export_file.py with no arguments exits 0 and prints []."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(SCRIPTS_DIR, "parse_export_file.py")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == "[]"
|
||||
|
||||
|
||||
def test_main_cli_missing_file_exits_zero(tmp_path):
|
||||
"""Running parse_export_file.py with a missing file exits 0 and prints []."""
|
||||
result = subprocess.run(
|
||||
[sys.executable, os.path.join(SCRIPTS_DIR, "parse_export_file.py"),
|
||||
str(tmp_path / "nonexistent.md")],
|
||||
capture_output=True,
|
||||
text=True,
|
||||
)
|
||||
assert result.returncode == 0
|
||||
assert result.stdout.strip() == "[]"
|
||||
Reference in New Issue
Block a user