forked from Rockachopa/Timmy-time-dashboard
## Thinking Engine Tests (#1314) - New: tests/timmy/test_thinking_engine.py — 117 tests across 21 test classes - Covers ThinkingEngine core + all 4 mixin classes: - engine.py: init, idle detection, store/retrieve, pruning, dedup, continuity, context assembly, novel thought generation, think_once, journal, broadcast - _distillation.py: should_distill, build_distill_prompt, parse_facts_response, filter_and_store_facts, maybe_distill - _issue_filing.py: references_real_files, get_recent_thoughts_for_issues, build_issue_classify_prompt, parse_issue_items, file_single_issue - _seeds_mixin.py: pick_seed_type, gather_seed, all seed sources, check_workspace - _snapshot.py: system snapshot, memory context, update_memory - _db.py: get_conn, row_to_thought, Thought dataclass - seeds.py: constants, prompt template, think tag regex - Targets 80%+ coverage of engine.py's 430 lines ## Stack Manifest (#986) - New: docs/stack_manifest.json — 8 categories, 40+ tools with pinned versions - LLM Inference, Coding Agents, Image Gen, Music/Voice, Orchestration, Nostr+Lightning+Bitcoin, Memory/KG, Streaming/Content - Schema: {tool, version, role, install_command, license, status} - New: src/timmy/stack_manifest.py — query_stack() runtime tool - Category and tool filtering (case-insensitive, partial match) - Manifest caching, graceful error handling - New: tests/timmy/test_stack_manifest.py — 24 tests - Registered query_stack in tool registry + tool catalog - Total: 141 new tests, all passing
407 lines
14 KiB
Python
407 lines
14 KiB
Python
"""Tests for timmy.stack_manifest — sovereign tech stack query tool.
|
|
|
|
Issue: #986
|
|
"""
|
|
|
|
import json
|
|
from pathlib import Path
|
|
from unittest.mock import patch
|
|
|
|
import pytest
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Helpers
|
|
# ---------------------------------------------------------------------------
|
|
|
|
_MINI_MANIFEST = {
|
|
"version": "1.0.0",
|
|
"categories": [
|
|
{
|
|
"id": "llm_inference",
|
|
"name": "Local LLM Inference",
|
|
"description": "On-device language model serving",
|
|
"tools": [
|
|
{
|
|
"tool": "Ollama",
|
|
"version": "0.18.2",
|
|
"role": "Primary local LLM runtime",
|
|
"install_command": "curl -fsSL https://ollama.com/install.sh | sh",
|
|
"license": "MIT",
|
|
"status": "active",
|
|
},
|
|
{
|
|
"tool": "mlx-lm",
|
|
"version": "0.31.1",
|
|
"role": "Apple MLX native inference",
|
|
"install_command": "pip install mlx-lm==0.31.1",
|
|
"license": "MIT",
|
|
"status": "active",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"id": "agent_orchestration",
|
|
"name": "Agent Orchestration",
|
|
"description": "Multi-agent coordination",
|
|
"tools": [
|
|
{
|
|
"tool": "FastMCP",
|
|
"version": "3.1.1",
|
|
"role": "MCP server framework",
|
|
"install_command": "pip install fastmcp==3.1.1",
|
|
"license": "MIT",
|
|
"status": "active",
|
|
},
|
|
{
|
|
"tool": "Agno",
|
|
"version": "2.5.10",
|
|
"role": "Core agent framework",
|
|
"install_command": "pip install agno==2.5.10",
|
|
"license": "MIT",
|
|
"status": "active",
|
|
},
|
|
],
|
|
},
|
|
{
|
|
"id": "nostr_lightning",
|
|
"name": "Nostr + Lightning + Bitcoin",
|
|
"description": "Sovereign identity and value transfer",
|
|
"tools": [
|
|
{
|
|
"tool": "LND",
|
|
"version": "0.20.1",
|
|
"role": "Lightning Network Daemon",
|
|
"install_command": "brew install lnd",
|
|
"license": "MIT",
|
|
"status": "active",
|
|
},
|
|
{
|
|
"tool": "exo-experimental",
|
|
"version": "1.0",
|
|
"role": "Test tool",
|
|
"install_command": "pip install exo",
|
|
"license": "GPL-3.0",
|
|
"status": "experimental",
|
|
},
|
|
],
|
|
},
|
|
],
|
|
}
|
|
|
|
|
|
def _write_manifest(tmp_path: Path, data: dict | None = None) -> Path:
|
|
"""Write a test manifest file and return its path."""
|
|
path = tmp_path / "stack_manifest.json"
|
|
path.write_text(json.dumps(data or _MINI_MANIFEST, indent=2))
|
|
return path
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# _load_manifest
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestLoadManifest:
|
|
"""Manifest loading and caching."""
|
|
|
|
def test_loads_from_file(self, tmp_path):
|
|
from timmy.stack_manifest import _load_manifest
|
|
|
|
path = _write_manifest(tmp_path)
|
|
data = _load_manifest(path)
|
|
assert data["version"] == "1.0.0"
|
|
assert len(data["categories"]) == 3
|
|
|
|
def test_raises_on_missing_file(self, tmp_path):
|
|
from timmy.stack_manifest import _load_manifest
|
|
|
|
with pytest.raises(FileNotFoundError):
|
|
_load_manifest(tmp_path / "nonexistent.json")
|
|
|
|
def test_raises_on_invalid_json(self, tmp_path):
|
|
from timmy.stack_manifest import _load_manifest
|
|
|
|
bad = tmp_path / "bad.json"
|
|
bad.write_text("{invalid json")
|
|
with pytest.raises(json.JSONDecodeError):
|
|
_load_manifest(bad)
|
|
|
|
def test_caching_works(self, tmp_path):
|
|
from timmy.stack_manifest import _load_manifest, _reset_cache
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
# Override the module-level path for caching test
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
data1 = _load_manifest()
|
|
data2 = _load_manifest()
|
|
assert data1 is data2 # Same object — cached
|
|
_reset_cache()
|
|
|
|
def test_reset_cache_clears(self, tmp_path):
|
|
from timmy.stack_manifest import _load_manifest, _reset_cache
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
_load_manifest(path)
|
|
_reset_cache()
|
|
from timmy import stack_manifest
|
|
|
|
assert stack_manifest._manifest_cache is None
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# query_stack — no filters
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestQueryStackNoFilters:
|
|
"""query_stack() with no arguments — full summary."""
|
|
|
|
def test_returns_all_tools(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack()
|
|
_reset_cache()
|
|
assert "6 tool(s) matched" in result # 2 + 2 + 2 (all tools counted)
|
|
assert "Ollama" in result
|
|
assert "FastMCP" in result
|
|
assert "LND" in result
|
|
|
|
def test_includes_manifest_version(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack()
|
|
_reset_cache()
|
|
assert "v1.0.0" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# query_stack — category filter
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestQueryStackCategoryFilter:
|
|
"""query_stack(category=...) filtering."""
|
|
|
|
def test_filter_by_category_id(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(category="llm_inference")
|
|
_reset_cache()
|
|
assert "Ollama" in result
|
|
assert "mlx-lm" in result
|
|
assert "FastMCP" not in result
|
|
|
|
def test_filter_by_partial_category(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(category="nostr")
|
|
_reset_cache()
|
|
assert "LND" in result
|
|
assert "Ollama" not in result
|
|
|
|
def test_filter_by_category_name(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(category="Agent Orchestration")
|
|
_reset_cache()
|
|
assert "FastMCP" in result
|
|
assert "Agno" in result
|
|
|
|
def test_no_matching_category(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(category="quantum_computing")
|
|
_reset_cache()
|
|
assert "No category matching" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# query_stack — tool filter
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestQueryStackToolFilter:
|
|
"""query_stack(tool=...) filtering."""
|
|
|
|
def test_filter_by_exact_tool(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(tool="Ollama")
|
|
_reset_cache()
|
|
assert "Ollama" in result
|
|
assert "0.18.2" in result
|
|
assert "FastMCP" not in result
|
|
|
|
def test_filter_by_partial_tool(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(tool="mcp")
|
|
_reset_cache()
|
|
assert "FastMCP" in result
|
|
|
|
def test_case_insensitive_tool(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(tool="ollama")
|
|
_reset_cache()
|
|
assert "Ollama" in result
|
|
|
|
def test_no_matching_tool(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(tool="nonexistent-tool")
|
|
_reset_cache()
|
|
assert "No tool matching" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# query_stack — combined filters
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestQueryStackCombinedFilters:
|
|
"""query_stack(category=..., tool=...) combined filtering."""
|
|
|
|
def test_category_and_tool(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(category="nostr", tool="LND")
|
|
_reset_cache()
|
|
assert "LND" in result
|
|
assert "1 tool(s) matched" in result
|
|
|
|
def test_category_and_tool_no_match(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(category="llm_inference", tool="LND")
|
|
_reset_cache()
|
|
assert "No tools found" in result
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# query_stack — error handling
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestQueryStackErrors:
|
|
"""Error handling in query_stack."""
|
|
|
|
def test_missing_manifest(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", tmp_path / "missing.json"):
|
|
result = query_stack()
|
|
_reset_cache()
|
|
assert "not found" in result.lower()
|
|
|
|
def test_invalid_manifest(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
bad = tmp_path / "bad.json"
|
|
bad.write_text("{broken")
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", bad):
|
|
result = query_stack()
|
|
_reset_cache()
|
|
assert "invalid JSON" in result
|
|
|
|
def test_empty_manifest(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path, {"version": "1.0.0", "categories": []})
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack()
|
|
_reset_cache()
|
|
assert "empty" in result.lower()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Output format
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
class TestOutputFormat:
|
|
"""Verify output formatting."""
|
|
|
|
def test_includes_install_command(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(tool="Ollama")
|
|
_reset_cache()
|
|
assert "Install:" in result
|
|
assert "curl -fsSL" in result
|
|
|
|
def test_includes_license(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(tool="Ollama")
|
|
_reset_cache()
|
|
assert "License: MIT" in result
|
|
|
|
def test_experimental_status_badge(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(tool="exo-experimental")
|
|
_reset_cache()
|
|
assert "[EXPERIMENTAL]" in result
|
|
|
|
def test_includes_role(self, tmp_path):
|
|
from timmy.stack_manifest import _reset_cache, query_stack
|
|
|
|
_reset_cache()
|
|
path = _write_manifest(tmp_path)
|
|
with patch("timmy.stack_manifest._MANIFEST_PATH", path):
|
|
result = query_stack(tool="Agno")
|
|
_reset_cache()
|
|
assert "Role:" in result
|
|
assert "Core agent framework" in result
|