chore: import upstream snapshot with attribution
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
Security / Dependency audit (pip-audit) (push) Has been cancelled
Security / CodeQL (javascript-typescript) (push) Has been cancelled
Security / CodeQL (python) (push) Has been cancelled
Security / Secret scan (gitleaks) (push) Has been cancelled
rust / test (ubuntu) (push) Has been cancelled
rust / simulator e2e (macos-latest) (push) Has been cancelled
rust / simulator e2e (ubuntu-latest) (push) Has been cancelled
rust / simulator e2e (windows-latest) (push) Has been cancelled
rust / wheels (aarch64-apple-darwin) (push) Has been cancelled
rust / wheels (x86_64-unknown-linux-gnu) (push) Has been cancelled
rust / wheels (x86_64-apple-darwin) (push) Has been cancelled
rust / audit (push) Has been cancelled
rust / parity (nightly, allowed to fail during Phase 0) (push) Has been cancelled
CI / commitlint (push) Has been skipped
Dev Containers / validate (.devcontainer/devcontainer.json, default) (push) Failing after 0s
Dev Containers / validate (.devcontainer/memory-stack/devcontainer.json, memory-stack) (push) Failing after 0s
Dev Containers / validate-worktree (push) Failing after 0s
CI / changes (push) Failing after 4s
Deploy Documentation / validate (push) Has been skipped
Deploy Documentation / deploy (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, claude) (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, codex) (push) Failing after 1s
Install Native E2E / install-native (ubuntu-latest) (push) Failing after 1s
OpenCode Plugin / typecheck + build + test (push) Failing after 1s
Init Native E2E / init-native (ubuntu-latest, copilot) (push) Failing after 1s
Release Please / release-please (push) Failing after 1s
Wrap E2E / docker-wrap-e2e (push) Failing after 1s
Wrap Native E2E / wrap-native (ubuntu-latest) (push) Failing after 1s
Init E2E / docker-init-e2e (push) Failing after 4s
Merge Conflicts / merge-conflicts (push) Failing after 4s
CI / lint (push) Has been cancelled
CI / build-wheel (push) Has been cancelled
CI / build-wheel-windows (push) Has been cancelled
CI / prefetch-model (push) Has been cancelled
CI / test-dashboard-ui (push) Has been cancelled
CI / test (1) (push) Has been cancelled
CI / test (2) (push) Has been cancelled
CI / test (3) (push) Has been cancelled
CI / test (4) (push) Has been cancelled
CI / test-extras (push) Has been cancelled
CI / test-agno (push) Has been cancelled
CI / build (push) Has been cancelled
CI / workflow-validation (push) Has been cancelled
CI / docker-native-e2e (push) Has been cancelled
CI / windows-native-wrapper (push) Has been cancelled
CI / macos-native-wrapper (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-manifest (map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-nonroot name:code-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim name:code-slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-code-slim-nonroot name:code-slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-nonroot name:nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim name:slim]) (push) Has been cancelled
Docker / docker-build (map[name:amd64 platform:linux/amd64 runs_on:ubuntu-24.04], map[bake_target:runtime-slim-nonroot name:slim-nonroot]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime name:]) (push) Has been cancelled
Docker / docker-build (map[name:arm64 platform:linux/arm64 runs_on:ubuntu-24.04-arm], map[bake_target:runtime-code name:code]) (push) Has been cancelled
Docker / promote-latest (push) Has been cancelled
Init Native E2E / init-native (macos-latest, claude) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, codex) (push) Has been cancelled
Init Native E2E / init-native (macos-latest, copilot) (push) Has been cancelled
Install Native E2E / install-native (macos-latest) (push) Has been cancelled
Wrap Native E2E / wrap-native (macos-latest) (push) Has been cancelled
This commit is contained in:
@@ -0,0 +1,472 @@
|
||||
"""Tests for memory CLI commands.
|
||||
|
||||
These tests use real SQLite databases (temp files) - no mocks.
|
||||
"""
|
||||
|
||||
import asyncio
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
from click.testing import CliRunner
|
||||
|
||||
from headroom.cli.main import main
|
||||
from headroom.memory.adapters.sqlite import SQLiteMemoryStore
|
||||
from headroom.memory.models import Memory
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def runner() -> CliRunner:
|
||||
"""Create a CLI test runner."""
|
||||
return CliRunner()
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_db(tmp_path: Path) -> str:
|
||||
"""Create a temporary database path."""
|
||||
return str(tmp_path / "test_memory.db")
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def populated_db(temp_db: str) -> str:
|
||||
"""Create a database with sample memories."""
|
||||
store = SQLiteMemoryStore(temp_db)
|
||||
|
||||
# Create memories at different scopes and ages
|
||||
memories = [
|
||||
# USER scope (no session/agent/turn)
|
||||
Memory(
|
||||
id="user-mem-001",
|
||||
content="User prefers TypeScript over JavaScript",
|
||||
user_id="test-user",
|
||||
session_id=None,
|
||||
agent_id=None,
|
||||
turn_id=None,
|
||||
importance=0.9,
|
||||
created_at=datetime.now() - timedelta(days=5),
|
||||
valid_from=datetime.now() - timedelta(days=5),
|
||||
),
|
||||
# SESSION scope
|
||||
Memory(
|
||||
id="session-mem-001",
|
||||
content="Working on authentication feature",
|
||||
user_id="test-user",
|
||||
session_id="session-123",
|
||||
agent_id=None,
|
||||
turn_id=None,
|
||||
importance=0.7,
|
||||
created_at=datetime.now() - timedelta(hours=2),
|
||||
valid_from=datetime.now() - timedelta(hours=2),
|
||||
),
|
||||
Memory(
|
||||
id="session-mem-002",
|
||||
content="Database uses PostgreSQL",
|
||||
user_id="test-user",
|
||||
session_id="session-123",
|
||||
agent_id=None,
|
||||
turn_id=None,
|
||||
importance=0.6,
|
||||
created_at=datetime.now() - timedelta(days=10),
|
||||
valid_from=datetime.now() - timedelta(days=10),
|
||||
),
|
||||
# AGENT scope
|
||||
Memory(
|
||||
id="agent-mem-001",
|
||||
content="Agent is exploring code structure",
|
||||
user_id="test-user",
|
||||
session_id="session-123",
|
||||
agent_id="agent-456",
|
||||
turn_id=None,
|
||||
importance=0.4,
|
||||
created_at=datetime.now() - timedelta(hours=1),
|
||||
valid_from=datetime.now() - timedelta(hours=1),
|
||||
),
|
||||
# TURN scope (ephemeral)
|
||||
Memory(
|
||||
id="turn-mem-001",
|
||||
content="Tool output from grep search",
|
||||
user_id="test-user",
|
||||
session_id="session-123",
|
||||
agent_id="agent-456",
|
||||
turn_id="turn-789",
|
||||
importance=0.2,
|
||||
created_at=datetime.now() - timedelta(minutes=5),
|
||||
valid_from=datetime.now() - timedelta(minutes=5),
|
||||
),
|
||||
# Low importance memory for pruning tests
|
||||
Memory(
|
||||
id="low-importance-001",
|
||||
content="Temporary note",
|
||||
user_id="test-user",
|
||||
session_id="session-123",
|
||||
agent_id=None,
|
||||
turn_id=None,
|
||||
importance=0.1,
|
||||
created_at=datetime.now() - timedelta(days=45),
|
||||
valid_from=datetime.now() - timedelta(days=45),
|
||||
),
|
||||
]
|
||||
|
||||
for mem in memories:
|
||||
asyncio.run(store.save(mem))
|
||||
|
||||
return temp_db
|
||||
|
||||
|
||||
class TestMemoryList:
|
||||
"""Tests for 'headroom memory list' command."""
|
||||
|
||||
def test_list_all(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""List all memories."""
|
||||
result = runner.invoke(main, ["memory", "list", "--db-path", populated_db])
|
||||
assert result.exit_code == 0
|
||||
# IDs are truncated to 8 chars in display, check for partial matches
|
||||
assert "user-mem" in result.output
|
||||
assert "session-" in result.output # "session-mem" truncated to "session-"
|
||||
|
||||
def test_list_with_limit(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""List with limit."""
|
||||
result = runner.invoke(main, ["memory", "list", "--db-path", populated_db, "--limit", "2"])
|
||||
assert result.exit_code == 0
|
||||
# Should show limited results
|
||||
assert "2 shown" in result.output or "Memories" in result.output
|
||||
|
||||
def test_list_by_scope(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Filter by scope level."""
|
||||
result = runner.invoke(
|
||||
main, ["memory", "list", "--db-path", populated_db, "--scope", "USER"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "TypeScript" in result.output # USER scope memory content
|
||||
|
||||
def test_list_empty_db(self, runner: CliRunner, temp_db: str) -> None:
|
||||
"""List from empty database."""
|
||||
# Initialize empty db
|
||||
SQLiteMemoryStore(temp_db)
|
||||
result = runner.invoke(main, ["memory", "list", "--db-path", temp_db])
|
||||
assert result.exit_code == 0
|
||||
assert "No memories found" in result.output
|
||||
|
||||
|
||||
class TestMemoryShow:
|
||||
"""Tests for 'headroom memory show' command."""
|
||||
|
||||
def test_show_by_id(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Show memory by full ID."""
|
||||
result = runner.invoke(main, ["memory", "show", "--db-path", populated_db, "user-mem-001"])
|
||||
assert result.exit_code == 0
|
||||
assert "TypeScript" in result.output
|
||||
assert "0.9" in result.output or "0.90" in result.output # importance
|
||||
|
||||
def test_show_by_partial_id(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Show memory by partial ID."""
|
||||
result = runner.invoke(main, ["memory", "show", "--db-path", populated_db, "user-mem"])
|
||||
assert result.exit_code == 0
|
||||
assert "TypeScript" in result.output
|
||||
|
||||
def test_show_json_output(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Show memory as JSON."""
|
||||
result = runner.invoke(
|
||||
main, ["memory", "show", "--db-path", populated_db, "user-mem-001", "--json"]
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
# Should be valid JSON
|
||||
data = json.loads(result.output)
|
||||
assert data["id"] == "user-mem-001"
|
||||
assert "TypeScript" in data["content"]
|
||||
|
||||
def test_show_not_found(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Show non-existent memory."""
|
||||
result = runner.invoke(
|
||||
main, ["memory", "show", "--db-path", populated_db, "nonexistent-id"]
|
||||
)
|
||||
assert result.exit_code != 0 or "not found" in result.output.lower()
|
||||
|
||||
|
||||
class TestMemoryStats:
|
||||
"""Tests for 'headroom memory stats' command."""
|
||||
|
||||
def test_stats(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Show stats for populated database."""
|
||||
result = runner.invoke(main, ["memory", "stats", "--db-path", populated_db])
|
||||
assert result.exit_code == 0
|
||||
assert "Total" in result.output or "Memories" in result.output
|
||||
assert "6" in result.output # 6 memories
|
||||
|
||||
def test_stats_empty_db(self, runner: CliRunner, temp_db: str) -> None:
|
||||
"""Stats for empty database."""
|
||||
SQLiteMemoryStore(temp_db)
|
||||
result = runner.invoke(main, ["memory", "stats", "--db-path", temp_db])
|
||||
assert result.exit_code == 0
|
||||
assert "0" in result.output
|
||||
|
||||
|
||||
class TestMemoryEdit:
|
||||
"""Tests for 'headroom memory edit' command."""
|
||||
|
||||
def test_edit_content(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Edit memory content."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"memory",
|
||||
"edit",
|
||||
"--db-path",
|
||||
populated_db,
|
||||
"user-mem-001",
|
||||
"--content",
|
||||
"Updated content",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify change
|
||||
show_result = runner.invoke(
|
||||
main, ["memory", "show", "--db-path", populated_db, "user-mem-001"]
|
||||
)
|
||||
assert "Updated content" in show_result.output
|
||||
|
||||
def test_edit_importance(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Edit memory importance."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "edit", "--db-path", populated_db, "user-mem-001", "--importance", "0.5"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify change
|
||||
show_result = runner.invoke(
|
||||
main, ["memory", "show", "--db-path", populated_db, "user-mem-001"]
|
||||
)
|
||||
assert "0.5" in show_result.output
|
||||
|
||||
def test_edit_not_found(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Edit non-existent memory."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "edit", "--db-path", populated_db, "nonexistent", "--content", "test"],
|
||||
)
|
||||
assert result.exit_code != 0 or "not found" in result.output.lower()
|
||||
|
||||
|
||||
class TestMemoryDelete:
|
||||
"""Tests for 'headroom memory delete' command."""
|
||||
|
||||
def test_delete_single(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Delete single memory with force."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "delete", "--db-path", populated_db, "turn-mem-001", "--force"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify deleted
|
||||
show_result = runner.invoke(
|
||||
main, ["memory", "show", "--db-path", populated_db, "turn-mem-001"]
|
||||
)
|
||||
assert "not found" in show_result.output.lower() or show_result.exit_code != 0
|
||||
|
||||
def test_delete_multiple(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Delete multiple memories."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
[
|
||||
"memory",
|
||||
"delete",
|
||||
"--db-path",
|
||||
populated_db,
|
||||
"turn-mem-001",
|
||||
"agent-mem-001",
|
||||
"--force",
|
||||
],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
def test_delete_requires_confirmation(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Delete prompts for confirmation without --force."""
|
||||
# Invoke delete and say no to confirmation
|
||||
runner.invoke(
|
||||
main,
|
||||
["memory", "delete", "--db-path", populated_db, "turn-mem-001"],
|
||||
input="n\n", # Say no
|
||||
)
|
||||
# Verify memory still exists since we said no
|
||||
show_result = runner.invoke(
|
||||
main, ["memory", "show", "--db-path", populated_db, "turn-mem-001"]
|
||||
)
|
||||
# Memory should still exist since we said no
|
||||
assert "Tool output" in show_result.output or show_result.exit_code == 0
|
||||
|
||||
|
||||
class TestMemoryPrune:
|
||||
"""Tests for 'headroom memory prune' command."""
|
||||
|
||||
def test_prune_dry_run(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Prune with dry-run shows what would be deleted."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "prune", "--db-path", populated_db, "--older-than", "30d", "--dry-run"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
assert "would" in result.output.lower() or "dry" in result.output.lower()
|
||||
|
||||
def test_prune_by_age(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Prune old memories."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "prune", "--db-path", populated_db, "--older-than", "30d", "--force"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
# Should have deleted the 45-day old memory
|
||||
|
||||
def test_prune_by_scope(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Prune by scope level."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "prune", "--db-path", populated_db, "--scope", "TURN", "--force"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify TURN memories are gone
|
||||
list_result = runner.invoke(
|
||||
main, ["memory", "list", "--db-path", populated_db, "--scope", "TURN"]
|
||||
)
|
||||
assert "No memories found" in list_result.output or "turn-mem" not in list_result.output
|
||||
|
||||
def test_prune_low_importance(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Prune low importance memories."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "prune", "--db-path", populated_db, "--low-importance", "0.3", "--force"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
|
||||
class TestMemoryPurge:
|
||||
"""Tests for 'headroom memory purge' command."""
|
||||
|
||||
def test_purge_requires_confirm_flag(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Purge requires --confirm flag."""
|
||||
result = runner.invoke(main, ["memory", "purge", "--db-path", populated_db])
|
||||
assert result.exit_code != 0 or "confirm" in result.output.lower()
|
||||
|
||||
def test_purge_with_confirm(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Purge deletes all memories."""
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "purge", "--db-path", populated_db, "--confirm"],
|
||||
input="y\n", # Confirm
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify empty
|
||||
stats_result = runner.invoke(main, ["memory", "stats", "--db-path", populated_db])
|
||||
assert "0" in stats_result.output
|
||||
|
||||
|
||||
class TestMemoryExportImport:
|
||||
"""Tests for export/import commands."""
|
||||
|
||||
def test_export_to_stdout(self, runner: CliRunner, populated_db: str) -> None:
|
||||
"""Export memories to stdout."""
|
||||
result = runner.invoke(main, ["memory", "export", "--db-path", populated_db])
|
||||
assert result.exit_code == 0
|
||||
# Should be valid JSON array
|
||||
data = json.loads(result.output)
|
||||
assert isinstance(data, list)
|
||||
assert len(data) == 6
|
||||
|
||||
def test_export_to_file(self, runner: CliRunner, populated_db: str, tmp_path: Path) -> None:
|
||||
"""Export memories to file."""
|
||||
output_file = tmp_path / "export.json"
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "export", "--db-path", populated_db, "--output", str(output_file)],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify file
|
||||
with open(output_file) as f:
|
||||
data = json.load(f)
|
||||
assert len(data) == 6
|
||||
|
||||
def test_import_from_file(self, runner: CliRunner, temp_db: str, tmp_path: Path) -> None:
|
||||
"""Import memories from file."""
|
||||
# Create import file
|
||||
import_data = [
|
||||
{
|
||||
"id": "imported-001",
|
||||
"content": "Imported memory",
|
||||
"user_id": "test-user",
|
||||
"importance": 0.8,
|
||||
"created_at": datetime.now().isoformat(),
|
||||
"valid_from": datetime.now().isoformat(),
|
||||
}
|
||||
]
|
||||
import_file = tmp_path / "import.json"
|
||||
with open(import_file, "w") as f:
|
||||
json.dump(import_data, f)
|
||||
|
||||
# Initialize empty db
|
||||
SQLiteMemoryStore(temp_db)
|
||||
|
||||
result = runner.invoke(
|
||||
main,
|
||||
["memory", "import", "--db-path", temp_db, str(import_file), "--force"],
|
||||
)
|
||||
assert result.exit_code == 0
|
||||
|
||||
# Verify imported
|
||||
show_result = runner.invoke(main, ["memory", "show", "--db-path", temp_db, "imported-001"])
|
||||
assert "Imported memory" in show_result.output
|
||||
|
||||
def test_export_import_roundtrip(
|
||||
self, runner: CliRunner, populated_db: str, tmp_path: Path
|
||||
) -> None:
|
||||
"""Export and import should be lossless."""
|
||||
export_file = tmp_path / "roundtrip.json"
|
||||
new_db = str(tmp_path / "new.db")
|
||||
|
||||
# Export
|
||||
runner.invoke(
|
||||
main, ["memory", "export", "--db-path", populated_db, "--output", str(export_file)]
|
||||
)
|
||||
|
||||
# Import to new db
|
||||
SQLiteMemoryStore(new_db)
|
||||
runner.invoke(main, ["memory", "import", "--db-path", new_db, str(export_file), "--force"])
|
||||
|
||||
# Compare stats
|
||||
orig_stats = runner.invoke(main, ["memory", "stats", "--db-path", populated_db])
|
||||
new_stats = runner.invoke(main, ["memory", "stats", "--db-path", new_db])
|
||||
|
||||
# Should have same count
|
||||
assert "6" in orig_stats.output
|
||||
assert "6" in new_stats.output
|
||||
|
||||
|
||||
class TestMemoryHelp:
|
||||
"""Tests for help output."""
|
||||
|
||||
def test_memory_help(self, runner: CliRunner) -> None:
|
||||
"""Memory group shows help."""
|
||||
result = runner.invoke(main, ["memory", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "list" in result.output
|
||||
assert "show" in result.output
|
||||
assert "stats" in result.output
|
||||
assert "edit" in result.output
|
||||
assert "delete" in result.output
|
||||
assert "prune" in result.output
|
||||
assert "purge" in result.output
|
||||
assert "export" in result.output
|
||||
assert "import" in result.output
|
||||
|
||||
def test_list_help(self, runner: CliRunner) -> None:
|
||||
"""List command shows help."""
|
||||
result = runner.invoke(main, ["memory", "list", "--help"])
|
||||
assert result.exit_code == 0
|
||||
assert "--limit" in result.output
|
||||
assert "--scope" in result.output
|
||||
assert "--since" in result.output
|
||||
Reference in New Issue
Block a user