Merge pull request #449 from AIOSAI/system/devpulse-fix-add-handler-import-to-memory-appsinitpy-for-py
feat(system): fix: add handler import to memory apps/__init__.py for Python 3.10 mock.patch compat
This commit is contained in:
@@ -0,0 +1 @@
|
||||
from . import handlers # noqa: F401
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,596 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: tests/test_search_extras.py
|
||||
# Date: 2026-04-25
|
||||
# Version: 1.0.0
|
||||
# Category: memory/tests
|
||||
# =============================================
|
||||
|
||||
"""Tests for search handler internals (query_executor and vector_search).
|
||||
|
||||
Covers:
|
||||
from aipass.memory.apps.handlers.search.query_executor import encode_query_subprocess
|
||||
from aipass.memory.apps.handlers.search.query_executor import search_vectors_subprocess
|
||||
from aipass.memory.apps.handlers.search.vector_search import search_collection
|
||||
from aipass.memory.apps.handlers.search.vector_search import encode_query
|
||||
from aipass.memory.apps.handlers.search.vector_search import search_all_collections
|
||||
|
||||
Tests subprocess-based encoding/search, ChromaDB collection queries,
|
||||
query encoding via the singleton QueryEncoder, and multi-collection search.
|
||||
All tests use mocks -- no live subprocess, ML model, or ChromaDB access.
|
||||
"""
|
||||
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers: prepare the mock graph needed to import search handlers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _prepare_query_executor_mocks(monkeypatch):
|
||||
"""Insert mocks for query_executor module-level imports.
|
||||
|
||||
Returns a dict of key mock objects so tests can assert against them.
|
||||
"""
|
||||
# Mock the chroma_subprocess and embed_subprocess script paths
|
||||
mock_chroma_script = MagicMock()
|
||||
mock_embed_script = MagicMock()
|
||||
|
||||
return {
|
||||
"chroma_script": mock_chroma_script,
|
||||
"embed_script": mock_embed_script,
|
||||
}
|
||||
|
||||
|
||||
def _import_query_executor(monkeypatch):
|
||||
"""Prepare mocks and import (or reimport) query_executor.
|
||||
|
||||
Returns (module, mocks_dict).
|
||||
"""
|
||||
mocks = _prepare_query_executor_mocks(monkeypatch)
|
||||
|
||||
# Remove cached module so it gets re-imported with our mocks
|
||||
sys.modules.pop("aipass.memory.apps.handlers.search.query_executor", None)
|
||||
|
||||
# Clear parent package attribute so Python re-executes module code
|
||||
parent = sys.modules.get("aipass.memory.apps.handlers.search")
|
||||
if parent is not None and hasattr(parent, "query_executor"):
|
||||
delattr(parent, "query_executor")
|
||||
|
||||
from aipass.memory.apps.handlers.search import query_executor # noqa: E402
|
||||
|
||||
return query_executor, mocks
|
||||
|
||||
|
||||
def _prepare_vector_search_mocks(monkeypatch):
|
||||
"""Insert mocks for vector_search module-level imports.
|
||||
|
||||
Returns a dict of key mock objects for assertions.
|
||||
"""
|
||||
# Mock chromadb client via the chroma module
|
||||
mock_client = MagicMock()
|
||||
mock_chroma = MagicMock()
|
||||
mock_chroma.get_client = MagicMock(return_value=mock_client)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.memory.apps.handlers.storage.chroma",
|
||||
mock_chroma,
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.memory.apps.handlers.storage",
|
||||
MagicMock(),
|
||||
)
|
||||
|
||||
# Mock sentence_transformers and torch for QueryEncoder
|
||||
mock_model = MagicMock()
|
||||
mock_model.encode.return_value = MagicMock(tolist=MagicMock(return_value=[0.1] * 384))
|
||||
mock_model.to.return_value = mock_model
|
||||
|
||||
mock_st_cls = MagicMock(return_value=mock_model)
|
||||
|
||||
mock_sentence_transformers = MagicMock()
|
||||
mock_sentence_transformers.SentenceTransformer = mock_st_cls
|
||||
monkeypatch.setitem(sys.modules, "sentence_transformers", mock_sentence_transformers)
|
||||
|
||||
mock_torch = MagicMock()
|
||||
mock_torch.cuda.is_available.return_value = False
|
||||
monkeypatch.setitem(sys.modules, "torch", mock_torch)
|
||||
|
||||
return {
|
||||
"client": mock_client,
|
||||
"model": mock_model,
|
||||
"st_cls": mock_st_cls,
|
||||
"torch": mock_torch,
|
||||
}
|
||||
|
||||
|
||||
def _import_vector_search(monkeypatch):
|
||||
"""Prepare mocks and import (or reimport) vector_search.
|
||||
|
||||
Returns (module, mocks_dict).
|
||||
"""
|
||||
mocks = _prepare_vector_search_mocks(monkeypatch)
|
||||
|
||||
# Remove cached modules so they get re-imported with our mocks
|
||||
sys.modules.pop("aipass.memory.apps.handlers.search.vector_search", None)
|
||||
|
||||
# Clear parent package attribute so Python re-executes module code
|
||||
parent = sys.modules.get("aipass.memory.apps.handlers.search")
|
||||
if parent is not None and hasattr(parent, "vector_search"):
|
||||
delattr(parent, "vector_search")
|
||||
|
||||
from aipass.memory.apps.handlers.search import vector_search # noqa: E402
|
||||
|
||||
# Reset singletons for a clean slate
|
||||
setattr(vector_search, "_query_encoder", None)
|
||||
setattr(vector_search, "_search_service", None)
|
||||
setattr(vector_search, "_local_services", {})
|
||||
|
||||
return vector_search, mocks
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: encode_query_subprocess
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestEncodeQuerySubprocess:
|
||||
"""Verify encode_query_subprocess subprocess-based encoding."""
|
||||
|
||||
def test_encode_returns_embedding_on_success(self, monkeypatch):
|
||||
"""Successful subprocess returns embedding and dimension."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
fake_output = json.dumps({"success": True, "embeddings": [[0.1, 0.2, 0.3]], "dimension": 3})
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = fake_output
|
||||
|
||||
with patch.object(subprocess, "run", return_value=mock_result) as mock_run:
|
||||
result = mod.encode_query_subprocess("test query")
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["embedding"] == [0.1, 0.2, 0.3]
|
||||
assert result["dimension"] == 3
|
||||
mock_run.assert_called_once()
|
||||
|
||||
def test_encode_returns_error_on_nonzero_exit(self, monkeypatch):
|
||||
"""Non-zero return code produces error dict."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 1
|
||||
mock_result.stderr = "Model not found"
|
||||
|
||||
with patch.object(subprocess, "run", return_value=mock_result):
|
||||
result = mod.encode_query_subprocess("test query")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "Model not found" in result["error"]
|
||||
|
||||
def test_encode_handles_timeout(self, monkeypatch):
|
||||
"""TimeoutExpired produces a timeout error."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
with patch.object(subprocess, "run", side_effect=subprocess.TimeoutExpired(cmd="python", timeout=120)):
|
||||
result = mod.encode_query_subprocess("slow query")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "timed out" in result["error"].lower()
|
||||
|
||||
def test_encode_handles_invalid_json(self, monkeypatch):
|
||||
"""Invalid JSON from subprocess produces error dict."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = "not valid json"
|
||||
|
||||
with patch.object(subprocess, "run", return_value=mock_result):
|
||||
result = mod.encode_query_subprocess("test query")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "json" in result["error"].lower()
|
||||
|
||||
def test_encode_handles_empty_embeddings(self, monkeypatch):
|
||||
"""Response with empty embeddings list produces error."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
fake_output = json.dumps({"success": True, "embeddings": [], "dimension": 384})
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = fake_output
|
||||
|
||||
with patch.object(subprocess, "run", return_value=mock_result):
|
||||
result = mod.encode_query_subprocess("test query")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "no embedding" in result["error"].lower()
|
||||
|
||||
def test_encode_handles_generic_exception(self, monkeypatch):
|
||||
"""Unexpected exception produces error dict."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
with patch.object(subprocess, "run", side_effect=OSError("Cannot execute")):
|
||||
result = mod.encode_query_subprocess("test query")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "Cannot execute" in result["error"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: search_vectors_subprocess
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestSearchVectorsSubprocess:
|
||||
"""Verify search_vectors_subprocess subprocess-based search."""
|
||||
|
||||
def test_search_returns_results_on_success(self, monkeypatch):
|
||||
"""Successful subprocess returns parsed results."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
fake_output = json.dumps(
|
||||
{"success": True, "results": [{"document": "hello", "distance": 0.1}], "total_results": 1}
|
||||
)
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = fake_output
|
||||
|
||||
with patch.object(subprocess, "run", return_value=mock_result) as mock_run:
|
||||
result = mod.search_vectors_subprocess(
|
||||
query_embedding=[0.1, 0.2, 0.3],
|
||||
branch="TEST",
|
||||
n_results=5,
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["total_results"] == 1
|
||||
mock_run.assert_called_once()
|
||||
|
||||
def test_search_returns_error_on_nonzero_exit(self, monkeypatch):
|
||||
"""Non-zero exit code produces error dict."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 1
|
||||
mock_result.stderr = "DB not found"
|
||||
|
||||
with patch.object(subprocess, "run", return_value=mock_result):
|
||||
result = mod.search_vectors_subprocess(query_embedding=[0.1])
|
||||
|
||||
assert result["success"] is False
|
||||
assert "DB not found" in result["error"]
|
||||
|
||||
def test_search_handles_timeout(self, monkeypatch):
|
||||
"""TimeoutExpired produces a timeout error."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
with patch.object(subprocess, "run", side_effect=subprocess.TimeoutExpired(cmd="python", timeout=60)):
|
||||
result = mod.search_vectors_subprocess(query_embedding=[0.1])
|
||||
|
||||
assert result["success"] is False
|
||||
assert "timed out" in result["error"].lower()
|
||||
|
||||
def test_search_handles_invalid_json(self, monkeypatch):
|
||||
"""Invalid JSON from subprocess produces error dict."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = "{{bad json"
|
||||
|
||||
with patch.object(subprocess, "run", return_value=mock_result):
|
||||
result = mod.search_vectors_subprocess(query_embedding=[0.1])
|
||||
|
||||
assert result["success"] is False
|
||||
assert "json" in result["error"].lower()
|
||||
|
||||
def test_search_passes_db_path_as_string(self, monkeypatch):
|
||||
"""db_path is converted to string in the input data."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
fake_output = json.dumps({"success": True, "results": []})
|
||||
mock_result = MagicMock()
|
||||
mock_result.returncode = 0
|
||||
mock_result.stdout = fake_output
|
||||
|
||||
with patch.object(subprocess, "run", return_value=mock_result) as mock_run:
|
||||
mod.search_vectors_subprocess(
|
||||
query_embedding=[0.1],
|
||||
db_path=Path("/tmp/test_chroma"),
|
||||
)
|
||||
|
||||
call_args = mock_run.call_args
|
||||
input_data = json.loads(call_args.kwargs.get("input", call_args[1].get("input", "")))
|
||||
assert input_data["db_path"] == "/tmp/test_chroma"
|
||||
|
||||
def test_search_handles_generic_exception(self, monkeypatch):
|
||||
"""Unexpected exception produces error dict."""
|
||||
mod, mocks = _import_query_executor(monkeypatch)
|
||||
|
||||
with patch.object(subprocess, "run", side_effect=OSError("Cannot execute")):
|
||||
result = mod.search_vectors_subprocess(query_embedding=[0.1])
|
||||
|
||||
assert result["success"] is False
|
||||
assert "Cannot execute" in result["error"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: search_collection (vector_search)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestSearchCollection:
|
||||
"""Verify search_collection wraps SearchService.query_collection."""
|
||||
|
||||
def test_search_returns_results_on_success(self, monkeypatch, tmp_path):
|
||||
"""Successful collection query returns success with results."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mock_collection = MagicMock()
|
||||
mock_collection.query.return_value = {
|
||||
"ids": [["id1", "id2"]],
|
||||
"documents": [["doc1", "doc2"]],
|
||||
"metadatas": [[{"branch": "TEST"}, {"branch": "TEST"}]],
|
||||
"distances": [[0.1, 0.2]],
|
||||
}
|
||||
mocks["client"].get_collection.return_value = mock_collection
|
||||
|
||||
result = mod.search_collection(
|
||||
query_embedding=[0.1] * 384,
|
||||
collection_name="test_collection",
|
||||
n_results=5,
|
||||
db_path=tmp_path / ".chroma",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["count"] == 2
|
||||
assert "doc1" in result["documents"]
|
||||
|
||||
def test_search_returns_error_for_missing_collection(self, monkeypatch, tmp_path):
|
||||
"""Missing collection returns success=False with error message."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mocks["client"].get_collection.side_effect = ValueError("Collection not found")
|
||||
|
||||
result = mod.search_collection(
|
||||
query_embedding=[0.1] * 384,
|
||||
collection_name="nonexistent",
|
||||
db_path=tmp_path / ".chroma",
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "not found" in result["error"].lower()
|
||||
|
||||
def test_search_returns_error_for_empty_embedding(self, monkeypatch):
|
||||
"""Empty query embedding returns error."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
result = mod.search_collection(
|
||||
query_embedding=[],
|
||||
collection_name="test_collection",
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "no query embedding" in result["error"].lower()
|
||||
|
||||
def test_search_accepts_string_db_path(self, monkeypatch, tmp_path):
|
||||
"""String db_path is converted to Path internally."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mock_collection = MagicMock()
|
||||
mock_collection.query.return_value = {
|
||||
"ids": [["id1"]],
|
||||
"documents": [["doc1"]],
|
||||
"metadatas": [[{}]],
|
||||
"distances": [[0.05]],
|
||||
}
|
||||
mocks["client"].get_collection.return_value = mock_collection
|
||||
|
||||
result = mod.search_collection(
|
||||
query_embedding=[0.1] * 384,
|
||||
collection_name="test_collection",
|
||||
db_path=str(tmp_path / ".chroma"),
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
def test_search_passes_where_filter(self, monkeypatch, tmp_path):
|
||||
"""Metadata where filter is forwarded to collection.query."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mock_collection = MagicMock()
|
||||
mock_collection.query.return_value = {
|
||||
"ids": [[]],
|
||||
"documents": [[]],
|
||||
"metadatas": [[]],
|
||||
"distances": [[]],
|
||||
}
|
||||
mocks["client"].get_collection.return_value = mock_collection
|
||||
|
||||
mod.search_collection(
|
||||
query_embedding=[0.1] * 384,
|
||||
collection_name="test_collection",
|
||||
where={"branch": "SEEDGO"},
|
||||
db_path=tmp_path / ".chroma",
|
||||
)
|
||||
|
||||
call_kwargs = mock_collection.query.call_args.kwargs
|
||||
assert call_kwargs["where"] == {"branch": "SEEDGO"}
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: encode_query (vector_search)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestEncodeQuery:
|
||||
"""Verify encode_query uses QueryEncoder singleton."""
|
||||
|
||||
def test_encode_returns_embedding(self, monkeypatch):
|
||||
"""Successful encoding returns embedding with dimension."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
result = mod.encode_query("test query")
|
||||
|
||||
assert result["success"] is True
|
||||
assert len(result["embedding"]) == 384
|
||||
assert result["dimension"] == 384
|
||||
assert result["model"] == "all-MiniLM-L6-v2"
|
||||
|
||||
def test_encode_rejects_empty_query(self, monkeypatch):
|
||||
"""Empty query string returns error."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
result = mod.encode_query("")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "empty" in result["error"].lower()
|
||||
|
||||
def test_encode_rejects_whitespace_only_query(self, monkeypatch):
|
||||
"""Whitespace-only query string returns error."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
result = mod.encode_query(" ")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "empty" in result["error"].lower()
|
||||
|
||||
def test_encode_handles_model_error(self, monkeypatch):
|
||||
"""If model.encode raises, error is caught and returned."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mocks["model"].encode.side_effect = RuntimeError("CUDA out of memory")
|
||||
|
||||
result = mod.encode_query("test query")
|
||||
|
||||
assert result["success"] is False
|
||||
assert "CUDA" in result["error"]
|
||||
|
||||
def test_encode_uses_singleton(self, monkeypatch):
|
||||
"""Successive calls reuse the same QueryEncoder instance."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mod.encode_query("first query")
|
||||
mod.encode_query("second query")
|
||||
|
||||
# SentenceTransformer should only be constructed once
|
||||
mocks["st_cls"].assert_called_once()
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: search_all_collections (vector_search)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestSearchAllCollections:
|
||||
"""Verify search_all_collections aggregates results across collections."""
|
||||
|
||||
def test_search_all_returns_aggregated_results(self, monkeypatch, tmp_path):
|
||||
"""Searching all collections returns results from each."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mock_coll_a = MagicMock()
|
||||
mock_coll_a.name = "coll_a"
|
||||
mock_coll_b = MagicMock()
|
||||
mock_coll_b.name = "coll_b"
|
||||
mocks["client"].list_collections.return_value = [mock_coll_a, mock_coll_b]
|
||||
|
||||
mock_collection = MagicMock()
|
||||
mock_collection.query.return_value = {
|
||||
"ids": [["id1"]],
|
||||
"documents": [["doc1"]],
|
||||
"metadatas": [[{"branch": "TEST"}]],
|
||||
"distances": [[0.1]],
|
||||
}
|
||||
mocks["client"].get_collection.return_value = mock_collection
|
||||
|
||||
result = mod.search_all_collections(
|
||||
query_embedding=[0.1] * 384,
|
||||
n_results=3,
|
||||
db_path=tmp_path / ".chroma",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["collections_searched"] == 2
|
||||
assert result["total_results"] == 2
|
||||
assert "coll_a" in result["results"]
|
||||
assert "coll_b" in result["results"]
|
||||
|
||||
def test_search_all_returns_empty_when_no_collections(self, monkeypatch, tmp_path):
|
||||
"""No collections returns success with empty results."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mocks["client"].list_collections.return_value = []
|
||||
|
||||
result = mod.search_all_collections(
|
||||
query_embedding=[0.1] * 384,
|
||||
db_path=tmp_path / ".chroma",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["results"] == {}
|
||||
assert "no collections" in result["message"].lower()
|
||||
|
||||
def test_search_all_returns_error_for_empty_embedding(self, monkeypatch):
|
||||
"""Empty embedding returns error."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
result = mod.search_all_collections(query_embedding=[])
|
||||
|
||||
assert result["success"] is False
|
||||
assert "no query embedding" in result["error"].lower()
|
||||
|
||||
def test_search_all_handles_exception(self, monkeypatch, tmp_path):
|
||||
"""Exception during search returns error dict."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mocks["client"].list_collections.side_effect = RuntimeError("DB corrupted")
|
||||
|
||||
result = mod.search_all_collections(
|
||||
query_embedding=[0.1] * 384,
|
||||
db_path=tmp_path / ".chroma",
|
||||
)
|
||||
|
||||
assert result["success"] is False
|
||||
assert "DB corrupted" in result["error"]
|
||||
|
||||
def test_search_all_accepts_string_db_path(self, monkeypatch, tmp_path):
|
||||
"""String db_path is accepted and converted."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mocks["client"].list_collections.return_value = []
|
||||
|
||||
result = mod.search_all_collections(
|
||||
query_embedding=[0.1] * 384,
|
||||
db_path=str(tmp_path / ".chroma"),
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
|
||||
def test_search_all_skips_nonexistent_collections(self, monkeypatch, tmp_path):
|
||||
"""Collections that fail get_collection are excluded from results."""
|
||||
mod, mocks = _import_vector_search(monkeypatch)
|
||||
|
||||
mock_coll_a = MagicMock()
|
||||
mock_coll_a.name = "coll_a"
|
||||
mocks["client"].list_collections.return_value = [mock_coll_a]
|
||||
|
||||
# get_collection raises => query_collection returns exists=False
|
||||
mocks["client"].get_collection.side_effect = ValueError("Collection not found")
|
||||
|
||||
result = mod.search_all_collections(
|
||||
query_embedding=[0.1] * 384,
|
||||
db_path=tmp_path / ".chroma",
|
||||
)
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["collections_searched"] == 0
|
||||
assert result["total_results"] == 0
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,492 @@
|
||||
# ===================AIPASS====================
|
||||
# META DATA HEADER
|
||||
# Name: tests/test_watcher.py
|
||||
# Date: 2026-04-25
|
||||
# Version: 1.0.0
|
||||
# Category: memory/tests
|
||||
# =============================================
|
||||
|
||||
"""Tests for the memory watcher handler.
|
||||
|
||||
Covers:
|
||||
from aipass.memory.apps.handlers.monitor.memory_watcher import start_memory_watcher
|
||||
from aipass.memory.apps.handlers.monitor.memory_watcher import stop_memory_watcher
|
||||
from aipass.memory.apps.handlers.monitor.memory_watcher import is_memory_watcher_active
|
||||
from aipass.memory.apps.handlers.monitor.memory_watcher import get_watcher_status
|
||||
from aipass.memory.apps.handlers.monitor.memory_watcher import MemoryFileWatcher
|
||||
|
||||
Tests watcher lifecycle (start/stop/status), the MemoryFileWatcher.on_modified
|
||||
callback, and edge cases like missing watchdog or already-running observers.
|
||||
All tests use mocks -- no live filesystem watchers or infrastructure access.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers: prepare the mock graph needed to import memory_watcher
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _prepare_watcher_mocks(monkeypatch):
|
||||
"""Insert mocks for every module-level import memory_watcher.py touches.
|
||||
|
||||
Returns a dict of key mock objects so tests can assert against them.
|
||||
"""
|
||||
# Mock line_counter
|
||||
mock_update_line_count = MagicMock(return_value={"success": True, "lines": 150})
|
||||
mock_line_counter = MagicMock()
|
||||
mock_line_counter.update_line_count = mock_update_line_count
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.memory.apps.handlers.tracking.line_counter",
|
||||
mock_line_counter,
|
||||
)
|
||||
|
||||
# Mock detector
|
||||
mock_check_single_file = MagicMock(return_value={"success": True, "should_rollover": False})
|
||||
mock_detector = MagicMock()
|
||||
mock_detector.check_single_file = mock_check_single_file
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.memory.apps.handlers.monitor.detector",
|
||||
mock_detector,
|
||||
)
|
||||
|
||||
# Mock watchdog
|
||||
mock_observer_instance = MagicMock()
|
||||
mock_observer_instance.is_alive.return_value = True
|
||||
mock_observer_cls = MagicMock(return_value=mock_observer_instance)
|
||||
|
||||
mock_watchdog_observers = MagicMock()
|
||||
mock_watchdog_observers.Observer = mock_observer_cls
|
||||
|
||||
mock_fse_handler = type("FileSystemEventHandler", (), {"__init__": lambda self: None})
|
||||
|
||||
mock_watchdog_events = MagicMock()
|
||||
mock_watchdog_events.FileSystemEventHandler = mock_fse_handler
|
||||
|
||||
monkeypatch.setitem(sys.modules, "watchdog", MagicMock())
|
||||
monkeypatch.setitem(sys.modules, "watchdog.observers", mock_watchdog_observers)
|
||||
monkeypatch.setitem(sys.modules, "watchdog.events", mock_watchdog_events)
|
||||
|
||||
# Mock rollover orchestrator (lazy import inside on_modified)
|
||||
mock_execute_rollover = MagicMock(return_value={"success": True})
|
||||
mock_orchestrator = MagicMock()
|
||||
mock_orchestrator.execute_rollover = mock_execute_rollover
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.memory.apps.handlers.rollover",
|
||||
MagicMock(),
|
||||
)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.memory.apps.handlers.rollover.orchestrator",
|
||||
mock_orchestrator,
|
||||
)
|
||||
|
||||
return {
|
||||
"update_line_count": mock_update_line_count,
|
||||
"check_single_file": mock_check_single_file,
|
||||
"observer_instance": mock_observer_instance,
|
||||
"observer_cls": mock_observer_cls,
|
||||
"execute_rollover": mock_execute_rollover,
|
||||
}
|
||||
|
||||
|
||||
def _import_watcher(monkeypatch):
|
||||
"""Prepare mocks and import (or reimport) memory_watcher.
|
||||
|
||||
Returns (module, mocks_dict).
|
||||
"""
|
||||
mocks = _prepare_watcher_mocks(monkeypatch)
|
||||
|
||||
# Remove cached module so it gets re-imported with our mocks
|
||||
sys.modules.pop("aipass.memory.apps.handlers.monitor.memory_watcher", None)
|
||||
|
||||
# Also clear parent package's cached attribute so Python re-executes
|
||||
# the module code with fresh mocks instead of returning a stale ref.
|
||||
parent = sys.modules.get("aipass.memory.apps.handlers.monitor")
|
||||
if parent is not None and hasattr(parent, "memory_watcher"):
|
||||
delattr(parent, "memory_watcher")
|
||||
|
||||
from aipass.memory.apps.handlers.monitor import memory_watcher # noqa: E402
|
||||
|
||||
# Reset the global _observer to None for a clean slate each test
|
||||
setattr(memory_watcher, "_observer", None)
|
||||
|
||||
return memory_watcher, mocks
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: start_memory_watcher
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestStartMemoryWatcher:
|
||||
"""Verify start_memory_watcher lifecycle."""
|
||||
|
||||
def test_start_returns_success_with_paths(self, monkeypatch, tmp_path):
|
||||
"""Starting watcher with valid branch paths returns success."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
branch_path = tmp_path / "src" / "aipass" / "test_branch"
|
||||
branch_path.mkdir(parents=True)
|
||||
monkeypatch.setattr(mod, "_get_branch_paths", lambda: [branch_path])
|
||||
|
||||
result = mod.start_memory_watcher()
|
||||
|
||||
assert result["success"] is True
|
||||
assert result["count"] == 1
|
||||
assert str(branch_path) in result["watched_paths"]
|
||||
mocks["observer_cls"].assert_called_once()
|
||||
mocks["observer_instance"].start.assert_called_once()
|
||||
|
||||
def test_start_fails_when_already_running(self, monkeypatch, tmp_path):
|
||||
"""Starting when observer is already alive returns error."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
# Simulate already running observer
|
||||
mock_existing = MagicMock()
|
||||
mock_existing.is_alive.return_value = True
|
||||
setattr(mod, "_observer", mock_existing)
|
||||
|
||||
result = mod.start_memory_watcher()
|
||||
|
||||
assert result["success"] is False
|
||||
assert "already running" in result["error"].lower()
|
||||
|
||||
def test_start_fails_when_no_branches(self, monkeypatch):
|
||||
"""Starting with no branch paths returns error."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
monkeypatch.setattr(mod, "_get_branch_paths", lambda: [])
|
||||
|
||||
result = mod.start_memory_watcher()
|
||||
|
||||
assert result["success"] is False
|
||||
assert "no branch" in result["error"].lower()
|
||||
|
||||
def test_start_handles_schedule_error_gracefully(self, monkeypatch, tmp_path):
|
||||
"""If scheduling a path raises an exception, other paths still work."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
good_path = tmp_path / "good_branch"
|
||||
good_path.mkdir(parents=True)
|
||||
bad_path = tmp_path / "bad_branch"
|
||||
bad_path.mkdir(parents=True)
|
||||
|
||||
call_count = 0
|
||||
|
||||
def _mock_schedule(handler, path, recursive=False):
|
||||
"""Mock observer.schedule that fails on the first call."""
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise OSError("Permission denied")
|
||||
|
||||
mocks["observer_instance"].schedule = _mock_schedule
|
||||
monkeypatch.setattr(mod, "_get_branch_paths", lambda: [bad_path, good_path])
|
||||
|
||||
result = mod.start_memory_watcher()
|
||||
|
||||
assert result["success"] is True
|
||||
# Only the second path should succeed
|
||||
assert result["count"] == 1
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: stop_memory_watcher
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestStopMemoryWatcher:
|
||||
"""Verify stop_memory_watcher lifecycle."""
|
||||
|
||||
def test_stop_returns_success(self, monkeypatch):
|
||||
"""Stopping a running watcher returns success."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
# Set up a running observer
|
||||
mock_obs = MagicMock()
|
||||
mock_obs.is_alive.return_value = True
|
||||
setattr(mod, "_observer", mock_obs)
|
||||
|
||||
result = mod.stop_memory_watcher()
|
||||
|
||||
assert result["success"] is True
|
||||
mock_obs.stop.assert_called_once()
|
||||
mock_obs.join.assert_called_once()
|
||||
assert mod._observer is None # type: ignore[union-attr]
|
||||
|
||||
def test_stop_fails_when_not_running(self, monkeypatch):
|
||||
"""Stopping when no watcher is running returns error."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
result = mod.stop_memory_watcher()
|
||||
|
||||
assert result["success"] is False
|
||||
assert "not running" in result["error"].lower()
|
||||
|
||||
def test_stop_fails_when_observer_not_alive(self, monkeypatch):
|
||||
"""Stopping when observer exists but is not alive returns error."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mock_obs = MagicMock()
|
||||
mock_obs.is_alive.return_value = False
|
||||
setattr(mod, "_observer", mock_obs)
|
||||
|
||||
result = mod.stop_memory_watcher()
|
||||
|
||||
assert result["success"] is False
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: is_memory_watcher_active
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestIsMemoryWatcherActive:
|
||||
"""Verify is_memory_watcher_active boolean checks."""
|
||||
|
||||
def test_active_when_observer_alive(self, monkeypatch):
|
||||
"""Returns True when observer is alive."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mock_obs = MagicMock()
|
||||
mock_obs.is_alive.return_value = True
|
||||
setattr(mod, "_observer", mock_obs)
|
||||
|
||||
assert mod.is_memory_watcher_active() is True
|
||||
|
||||
def test_inactive_when_no_observer(self, monkeypatch):
|
||||
"""Returns False when _observer is None."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
assert mod.is_memory_watcher_active() is False
|
||||
|
||||
def test_inactive_when_observer_not_alive(self, monkeypatch):
|
||||
"""Returns False when observer exists but is_alive returns False."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mock_obs = MagicMock()
|
||||
mock_obs.is_alive.return_value = False
|
||||
setattr(mod, "_observer", mock_obs)
|
||||
|
||||
assert mod.is_memory_watcher_active() is False
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: get_watcher_status
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestGetWatcherStatus:
|
||||
"""Verify get_watcher_status returns correct status info."""
|
||||
|
||||
def test_status_when_inactive(self, monkeypatch):
|
||||
"""Returns inactive status when watcher is not running."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
result = mod.get_watcher_status()
|
||||
|
||||
assert result["active"] is False
|
||||
assert "not running" in result["message"].lower()
|
||||
|
||||
def test_status_when_active(self, monkeypatch, tmp_path):
|
||||
"""Returns active status with directory count when watcher is running."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mock_obs = MagicMock()
|
||||
mock_obs.is_alive.return_value = True
|
||||
setattr(mod, "_observer", mock_obs)
|
||||
|
||||
branch_path = tmp_path / "branch1"
|
||||
branch_path.mkdir()
|
||||
monkeypatch.setattr(mod, "_get_branch_paths", lambda: [branch_path])
|
||||
|
||||
result = mod.get_watcher_status()
|
||||
|
||||
assert result["active"] is True
|
||||
assert result["watched_directories"] == 1
|
||||
assert str(branch_path) in result["paths"]
|
||||
|
||||
def test_status_returns_multiple_paths(self, monkeypatch, tmp_path):
|
||||
"""Returns all watched directory paths."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mock_obs = MagicMock()
|
||||
mock_obs.is_alive.return_value = True
|
||||
setattr(mod, "_observer", mock_obs)
|
||||
|
||||
paths = []
|
||||
for name in ["branch_a", "branch_b", "branch_c"]:
|
||||
p = tmp_path / name
|
||||
p.mkdir()
|
||||
paths.append(p)
|
||||
|
||||
monkeypatch.setattr(mod, "_get_branch_paths", lambda: paths)
|
||||
|
||||
result = mod.get_watcher_status()
|
||||
|
||||
assert result["watched_directories"] == 3
|
||||
assert len(result["paths"]) == 3
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# Tests: MemoryFileWatcher.on_modified
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
class TestMemoryFileWatcherOnModified:
|
||||
"""Verify MemoryFileWatcher.on_modified callback behavior."""
|
||||
|
||||
def test_ignores_directory_events(self, monkeypatch):
|
||||
"""Directory modification events are ignored."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
event = MagicMock()
|
||||
event.is_directory = True
|
||||
event.src_path = "/some/.trinity/local.json"
|
||||
|
||||
watcher.on_modified(event)
|
||||
|
||||
mocks["update_line_count"].assert_not_called()
|
||||
|
||||
def test_ignores_non_memory_files(self, monkeypatch):
|
||||
"""Non-memory files (not in .trinity/) are ignored."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/some/dir/config.json"
|
||||
|
||||
watcher.on_modified(event)
|
||||
|
||||
mocks["update_line_count"].assert_not_called()
|
||||
|
||||
def test_processes_memory_file_modification(self, monkeypatch):
|
||||
"""Valid memory file modification triggers line count update and check."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/some/branch/.trinity/local.json"
|
||||
|
||||
watcher.on_modified(event)
|
||||
|
||||
mocks["update_line_count"].assert_called_once()
|
||||
mocks["check_single_file"].assert_called_once()
|
||||
|
||||
def test_rollover_triggered_when_threshold_exceeded(self, monkeypatch):
|
||||
"""When check_single_file says should_rollover, execute_rollover is called."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mocks["check_single_file"].return_value = {
|
||||
"success": True,
|
||||
"should_rollover": True,
|
||||
"trigger": "lines exceeded 600",
|
||||
}
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/some/branch/.trinity/local.json"
|
||||
|
||||
watcher.on_modified(event)
|
||||
|
||||
mocks["execute_rollover"].assert_called_once()
|
||||
|
||||
def test_skips_recently_modified_file(self, monkeypatch):
|
||||
"""Files already in the recent modifications set are skipped."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
file_path = "/some/branch/.trinity/local.json"
|
||||
watcher._recent_modifications.add(file_path)
|
||||
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = file_path
|
||||
|
||||
watcher.on_modified(event)
|
||||
|
||||
# Should skip and not call update_line_count
|
||||
mocks["update_line_count"].assert_not_called()
|
||||
# The file key should be removed from recent modifications after skip
|
||||
assert file_path not in watcher._recent_modifications
|
||||
|
||||
def test_handles_line_count_update_failure(self, monkeypatch):
|
||||
"""When update_line_count fails, check_single_file is not called."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mocks["update_line_count"].return_value = {
|
||||
"success": False,
|
||||
"error": "File not found",
|
||||
}
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/some/branch/.trinity/local.json"
|
||||
|
||||
watcher.on_modified(event)
|
||||
|
||||
mocks["update_line_count"].assert_called_once()
|
||||
mocks["check_single_file"].assert_not_called()
|
||||
|
||||
def test_handles_check_failure(self, monkeypatch):
|
||||
"""When check_single_file fails, rollover is not triggered."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mocks["check_single_file"].return_value = {
|
||||
"success": False,
|
||||
"error": "Read error",
|
||||
}
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/some/branch/.trinity/local.json"
|
||||
|
||||
watcher.on_modified(event)
|
||||
|
||||
mocks["check_single_file"].assert_called_once()
|
||||
mocks["execute_rollover"].assert_not_called()
|
||||
|
||||
def test_processes_observations_json(self, monkeypatch):
|
||||
"""observations.json files in .trinity/ are also processed."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/some/branch/.trinity/observations.json"
|
||||
|
||||
watcher.on_modified(event)
|
||||
|
||||
mocks["update_line_count"].assert_called_once()
|
||||
|
||||
def test_rollover_exception_does_not_propagate(self, monkeypatch):
|
||||
"""If execute_rollover raises, the exception is caught."""
|
||||
mod, mocks = _import_watcher(monkeypatch)
|
||||
|
||||
mocks["check_single_file"].return_value = {
|
||||
"success": True,
|
||||
"should_rollover": True,
|
||||
"trigger": "lines exceeded",
|
||||
}
|
||||
mocks["execute_rollover"].side_effect = RuntimeError("Rollover crashed")
|
||||
|
||||
watcher = mod.MemoryFileWatcher()
|
||||
event = MagicMock()
|
||||
event.is_directory = False
|
||||
event.src_path = "/some/branch/.trinity/local.json"
|
||||
|
||||
# Should not raise
|
||||
watcher.on_modified(event)
|
||||
|
||||
mocks["execute_rollover"].assert_called_once()
|
||||
@@ -0,0 +1,900 @@
|
||||
"""Tests for seedgo checker sub-functions -- batch 6 (architecture, cli, cli_flags, documentation)."""
|
||||
|
||||
# =================== META ====================
|
||||
# Name: test_checkers_batch6.py
|
||||
# Description: Unit tests for checker sub-functions in architecture, cli, cli_flags, documentation
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-04-25
|
||||
# Modified: 2026-04-25
|
||||
# =============================================
|
||||
|
||||
from typing import List
|
||||
|
||||
import pytest
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
def _lines(text: str) -> List[str]:
|
||||
"""Split text into lines, widening LiteralString to str for pyright."""
|
||||
return text.split("\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_infrastructure(monkeypatch):
|
||||
"""Mock heavy infrastructure imports for standards checkers."""
|
||||
import sys
|
||||
|
||||
mock_logger = MagicMock()
|
||||
mock_json_handler = MagicMock()
|
||||
mock_json_handler.log_operation = MagicMock(return_value=True)
|
||||
|
||||
# -- prax ---------------------------------------------------------------
|
||||
prax_mod = MagicMock()
|
||||
prax_mod.logger = mock_logger
|
||||
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
|
||||
|
||||
# -- seedgo json handler ------------------------------------------------
|
||||
json_pkg = MagicMock()
|
||||
json_pkg.json_handler = mock_json_handler
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
|
||||
json_mod = MagicMock()
|
||||
json_mod.log_operation = mock_json_handler.log_operation
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.seedgo.apps.handlers.json.json_handler",
|
||||
json_mod,
|
||||
)
|
||||
|
||||
# -- bypass handler (used by architecture_check) ------------------------
|
||||
bypass_pkg = MagicMock()
|
||||
bypass_ignore = MagicMock()
|
||||
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
|
||||
bypass_pkg.ignore_handler = bypass_ignore
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.seedgo.apps.handlers.bypass.ignore_handler",
|
||||
bypass_ignore,
|
||||
)
|
||||
|
||||
# Force re-imports so checkers pick up fresh mocks
|
||||
for mod_name in [
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.architecture_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.cli_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.cli_flags_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.documentation_check",
|
||||
]:
|
||||
monkeypatch.delitem(sys.modules, mod_name, raising=False)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 1. architecture_check sub-functions
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
# -- check_layer_location ---------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckLayerLocation:
|
||||
"""Tests for check_layer_location."""
|
||||
|
||||
def test_entry_point_passes(self):
|
||||
"""Entry point path is recognised as the entry-point layer."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_layer_location,
|
||||
)
|
||||
|
||||
result = check_layer_location("/branch/apps/branch.py", True, False, False)
|
||||
assert result["passed"] is True
|
||||
assert "Entry point" in result["message"]
|
||||
|
||||
def test_module_layer_passes(self):
|
||||
"""Module path is recognised as the module layer."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_layer_location,
|
||||
)
|
||||
|
||||
result = check_layer_location("/branch/apps/modules/foo.py", False, True, False)
|
||||
assert result["passed"] is True
|
||||
assert "Module layer" in result["message"]
|
||||
|
||||
def test_handler_layer_passes(self):
|
||||
"""Handler path is recognised as the handler layer."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_layer_location,
|
||||
)
|
||||
|
||||
result = check_layer_location("/branch/apps/handlers/json/j.py", False, False, True)
|
||||
assert result["passed"] is True
|
||||
assert "Handler layer" in result["message"]
|
||||
|
||||
def test_outside_3_layer_fails(self):
|
||||
"""Path outside the 3-layer structure fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_layer_location,
|
||||
)
|
||||
|
||||
result = check_layer_location("/branch/random/foo.py", False, False, False)
|
||||
assert result["passed"] is False
|
||||
assert "not in standard 3-layer" in result["message"]
|
||||
|
||||
|
||||
# -- check_file_size (architecture) -----------------------------------------
|
||||
|
||||
|
||||
class TestArchCheckFileSize:
|
||||
"""Tests for architecture_check.check_file_size."""
|
||||
|
||||
def test_small_file_passes(self):
|
||||
"""Under 300 lines is perfect."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_file_size,
|
||||
)
|
||||
|
||||
lines: list[str] = ["x"] * 100
|
||||
result = check_file_size(lines, "small.py")
|
||||
assert result["passed"] is True
|
||||
assert "perfect" in result["message"]
|
||||
|
||||
def test_medium_file_passes(self):
|
||||
"""300-500 lines is good."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_file_size,
|
||||
)
|
||||
|
||||
lines: list[str] = ["x"] * 350
|
||||
result = check_file_size(lines, "medium.py")
|
||||
assert result["passed"] is True
|
||||
assert "good" in result["message"]
|
||||
|
||||
def test_heavy_file_passes(self):
|
||||
"""500-700 lines is acceptable but heavy."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_file_size,
|
||||
)
|
||||
|
||||
lines: list[str] = ["x"] * 600
|
||||
result = check_file_size(lines, "heavy.py")
|
||||
assert result["passed"] is True
|
||||
assert "getting heavy" in result["message"]
|
||||
|
||||
def test_oversized_file_fails(self):
|
||||
"""700+ lines fails the size check."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_file_size,
|
||||
)
|
||||
|
||||
lines: list[str] = ["x"] * 750
|
||||
result = check_file_size(lines, "huge.py")
|
||||
assert result["passed"] is False
|
||||
assert "consider splitting" in result["message"]
|
||||
|
||||
|
||||
# -- check_handler_independence (architecture) --------------------------------
|
||||
|
||||
|
||||
class TestArchHandlerIndependence:
|
||||
"""Tests for architecture_check.check_handler_independence."""
|
||||
|
||||
def test_clean_handler_passes(self):
|
||||
"""Handler without parent module imports passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"from aipass.prax import logger",
|
||||
"from aipass.cli.apps.modules import display",
|
||||
"",
|
||||
"def do_work():",
|
||||
" return True",
|
||||
]
|
||||
result = check_handler_independence(lines, "/seedgo/apps/handlers/json/j.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_parent_module_import_fails(self):
|
||||
"""Handler importing from its parent branch module fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"from seedgo.apps.modules.audit import run_audit",
|
||||
"",
|
||||
"def do_work():",
|
||||
" return True",
|
||||
]
|
||||
result = check_handler_independence(lines, "/seedgo/apps/handlers/json/j.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "parent module" in result["message"]
|
||||
|
||||
def test_allowed_service_imports_pass(self):
|
||||
"""Prax and CLI service imports are allowed in handlers."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"from prax.apps.modules.logger import info",
|
||||
"from cli.apps.modules.display import header",
|
||||
]
|
||||
result = check_handler_independence(lines, "/seedgo/apps/handlers/json/j.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_import_in_docstring_ignored(self):
|
||||
"""Imports inside docstrings are not flagged."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
'"""',
|
||||
"from seedgo.apps.modules.audit import run_audit",
|
||||
'"""',
|
||||
"def do_work():",
|
||||
" return True",
|
||||
]
|
||||
result = check_handler_independence(lines, "/seedgo/apps/handlers/json/j.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# -- check_domain_organization -----------------------------------------------
|
||||
|
||||
|
||||
class TestCheckDomainOrganization:
|
||||
"""Tests for check_domain_organization."""
|
||||
|
||||
def test_domain_based_passes(self):
|
||||
"""Handler in a domain-named folder passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_domain_organization,
|
||||
)
|
||||
|
||||
result = check_domain_organization("/branch/apps/handlers/json/json_handler.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
assert "json" in result["message"]
|
||||
|
||||
def test_technical_name_fails(self):
|
||||
"""Handler in a technical-named folder (utils) fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_domain_organization,
|
||||
)
|
||||
|
||||
result = check_domain_organization("/branch/apps/handlers/utils/helper.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "Technical organization" in result["message"]
|
||||
|
||||
def test_helpers_name_fails(self):
|
||||
"""Handler in a helpers/ folder fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_domain_organization,
|
||||
)
|
||||
|
||||
result = check_domain_organization("/branch/apps/handlers/helpers/tool.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
|
||||
def test_no_handler_domain_detected(self):
|
||||
"""Path ending at handlers/ with no subdirectory fails detection."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_domain_organization,
|
||||
)
|
||||
|
||||
result = check_domain_organization("/branch/apps/handlers")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "Could not detect" in result["message"]
|
||||
|
||||
|
||||
# -- check_template_baseline -------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckTemplateBaseline:
|
||||
"""Tests for check_template_baseline."""
|
||||
|
||||
def test_no_branch_path_detected(self):
|
||||
"""Path without apps/ cannot resolve a branch."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_template_baseline,
|
||||
)
|
||||
|
||||
result = check_template_baseline("/random/file.py")
|
||||
assert len(result) >= 1
|
||||
assert result[0]["passed"] is False
|
||||
assert "Could not detect branch path" in result[0]["message"]
|
||||
|
||||
def test_missing_citizen_class(self, tmp_path):
|
||||
"""Branch without passport.json fails the citizen_class check."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.architecture_check import (
|
||||
check_template_baseline,
|
||||
)
|
||||
|
||||
apps_dir = tmp_path / "mybranch" / "apps"
|
||||
apps_dir.mkdir(parents=True)
|
||||
entry = apps_dir / "mybranch.py"
|
||||
entry.write_text('"""Entry."""\n', encoding="utf-8")
|
||||
|
||||
result = check_template_baseline(str(entry))
|
||||
assert len(result) >= 1
|
||||
assert result[0]["passed"] is False
|
||||
assert "citizen_class" in result[0]["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2. cli_check sub-functions
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
# -- check_handler_separation ------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckHandlerSeparation:
|
||||
"""Tests for check_handler_separation."""
|
||||
|
||||
def test_clean_handler_passes(self):
|
||||
"""Handler with no console output passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_handler_separation,
|
||||
)
|
||||
|
||||
content = "def compute():\n return 42\n"
|
||||
result = check_handler_separation(content)
|
||||
assert result["passed"] is True
|
||||
assert "No console output" in result["message"]
|
||||
|
||||
def test_console_print_fails(self):
|
||||
"""Handler with console.print() fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_handler_separation,
|
||||
)
|
||||
|
||||
content = "def show():\n console.print('hello')\n"
|
||||
result = check_handler_separation(content)
|
||||
assert result["passed"] is False
|
||||
assert "console.print()" in result["message"]
|
||||
|
||||
def test_bare_print_fails(self):
|
||||
"""Handler with bare print() fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_handler_separation,
|
||||
)
|
||||
|
||||
content = "def show():\n print('hello')\n"
|
||||
result = check_handler_separation(content)
|
||||
assert result["passed"] is False
|
||||
assert "print()" in result["message"]
|
||||
|
||||
def test_cli_import_fails(self):
|
||||
"""Handler importing CLI services fails separation check."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_handler_separation,
|
||||
)
|
||||
|
||||
content = "from aipass.cli.apps.modules.display import header\ndef show():\n pass\n"
|
||||
result = check_handler_separation(content)
|
||||
assert result["passed"] is False
|
||||
assert "CLI services" in result["message"]
|
||||
|
||||
def test_print_in_main_block_ignored(self):
|
||||
"""Print inside if __name__ block is allowed."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_handler_separation,
|
||||
)
|
||||
|
||||
content = "def compute():\n return 42\nif __name__ == '__main__':\n print('test output')\n"
|
||||
result = check_handler_separation(content)
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_console_print_in_string_ignored(self):
|
||||
"""Console.print inside a string literal is not flagged."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_handler_separation,
|
||||
)
|
||||
|
||||
content = "msg = 'use console.print() for output'\n"
|
||||
result = check_handler_separation(content)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# -- check_cli_imports -------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckCliImports:
|
||||
"""Tests for check_cli_imports."""
|
||||
|
||||
def test_has_cli_imports_passes(self):
|
||||
"""Module with CLI service imports passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_cli_imports,
|
||||
)
|
||||
|
||||
content = "from aipass.cli.apps.modules.display import header\n"
|
||||
result = check_cli_imports(content, "/seedgo/apps/modules/audit.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_cli_branch_exempt(self):
|
||||
"""CLI branch itself is exempt from this check."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_cli_imports,
|
||||
)
|
||||
|
||||
content = "from .display import header\n"
|
||||
result = check_cli_imports(content, "/cli/apps/modules/something.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
assert "CLI branch exempt" in result["message"]
|
||||
|
||||
def test_output_without_cli_imports_fails(self):
|
||||
"""Module with output but no CLI imports fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_cli_imports,
|
||||
)
|
||||
|
||||
content = "print('hello world')\n"
|
||||
result = check_cli_imports(content, "/seedgo/apps/modules/audit.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "missing CLI service imports" in result["message"]
|
||||
|
||||
def test_no_output_at_all_passes(self):
|
||||
"""Module with no output at all passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_cli_imports,
|
||||
)
|
||||
|
||||
content = "def compute():\n return 42\n"
|
||||
result = check_cli_imports(content, "/seedgo/apps/modules/audit.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
assert "No CLI output needed" in result["message"]
|
||||
|
||||
def test_shortcut_import_passes(self):
|
||||
"""Shortcut import via cli __init__ passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_cli_imports,
|
||||
)
|
||||
|
||||
content = "from aipass.cli import header\n"
|
||||
result = check_cli_imports(content, "/seedgo/apps/modules/audit.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# -- check_print_usage -------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckPrintUsage:
|
||||
"""Tests for check_print_usage."""
|
||||
|
||||
def test_console_print_passes(self):
|
||||
"""File using console.print passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_print_usage,
|
||||
)
|
||||
|
||||
content = "console.print('hello')\n"
|
||||
lines = _lines(content)
|
||||
result = check_print_usage(content, lines, "/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_bare_print_fails(self):
|
||||
"""Bare print() statement fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_print_usage,
|
||||
)
|
||||
|
||||
content = "print('hello')\n"
|
||||
lines = _lines(content)
|
||||
result = check_print_usage(content, lines, "/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "print() statements" in result["message"]
|
||||
|
||||
def test_parser_print_help_fails(self):
|
||||
"""parser.print_help() usage fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_print_usage,
|
||||
)
|
||||
|
||||
content = "parser.print_help()\n"
|
||||
lines = _lines(content)
|
||||
result = check_print_usage(content, lines, "/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "parser.print_help()" in result["message"]
|
||||
|
||||
def test_sys_stdout_write_fails(self):
|
||||
"""sys.stdout.write() usage fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_print_usage,
|
||||
)
|
||||
|
||||
content = "import sys\nsys.stdout.write('hello')\n"
|
||||
lines = _lines(content)
|
||||
result = check_print_usage(content, lines, "/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "sys.stdout" in result["message"]
|
||||
|
||||
def test_print_in_main_block_ignored(self):
|
||||
"""Print inside if __name__ block returns None (no violation)."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_print_usage,
|
||||
)
|
||||
|
||||
content = "if __name__ == '__main__':\n print('test')\n"
|
||||
lines = _lines(content)
|
||||
result = check_print_usage(content, lines, "/module.py")
|
||||
assert result is None
|
||||
|
||||
def test_no_output_returns_none(self):
|
||||
"""File with no output returns None."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_print_usage,
|
||||
)
|
||||
|
||||
content = "x = 42\n"
|
||||
lines = _lines(content)
|
||||
result = check_print_usage(content, lines, "/module.py")
|
||||
assert result is None
|
||||
|
||||
def test_bypass_rule_skips_line(self):
|
||||
"""Bypassed print line is not flagged."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_print_usage,
|
||||
)
|
||||
|
||||
content = "print('hello')\n"
|
||||
lines = _lines(content)
|
||||
bypass = [{"standard": "cli", "file": "module.py", "lines": [1]}]
|
||||
result = check_print_usage(content, lines, "/module.py", bypass_rules=bypass)
|
||||
assert result is None
|
||||
|
||||
|
||||
# -- check_help_flag ---------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckHelpFlag:
|
||||
"""Tests for check_help_flag."""
|
||||
|
||||
def test_argparse_with_help_passes(self):
|
||||
"""Module with argparse and help/h flags passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_help_flag,
|
||||
)
|
||||
|
||||
content = "import argparse\nparser = argparse.ArgumentParser()\n--help\n-h\n"
|
||||
result = check_help_flag(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_print_help_function_passes(self):
|
||||
"""Module with def print_help passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_help_flag,
|
||||
)
|
||||
|
||||
content = "def print_help():\n pass\n"
|
||||
result = check_help_flag(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_executable_without_help_fails(self):
|
||||
"""Executable module without --help fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_help_flag,
|
||||
)
|
||||
|
||||
content = "if __name__ == '__main__':\n main()\n"
|
||||
result = check_help_flag(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "--help flag not implemented" in result["message"]
|
||||
|
||||
def test_non_executable_returns_none(self):
|
||||
"""Non-executable module returns None (not applicable)."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_help_flag,
|
||||
)
|
||||
|
||||
content = "def compute():\n return 42\n"
|
||||
result = check_help_flag(content)
|
||||
assert result is None
|
||||
|
||||
|
||||
# -- check_duplicate_display_functions ----------------------------------------
|
||||
|
||||
|
||||
class TestCheckDuplicateDisplayFunctions:
|
||||
"""Tests for check_duplicate_display_functions."""
|
||||
|
||||
def test_no_duplicates_passes(self):
|
||||
"""Module without CLI display function duplicates passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_duplicate_display_functions,
|
||||
)
|
||||
|
||||
content = "def compute():\n return 42\n"
|
||||
result = check_duplicate_display_functions(content, "/seedgo/apps/modules/audit.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_duplicate_header_fails(self):
|
||||
"""Module defining its own header() fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_duplicate_display_functions,
|
||||
)
|
||||
|
||||
content = "def header(title):\n print(title)\n"
|
||||
result = check_duplicate_display_functions(content, "/seedgo/apps/modules/audit.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "header" in result["message"]
|
||||
|
||||
def test_duplicate_error_and_warning_fails(self):
|
||||
"""Module defining error() and warning() fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_duplicate_display_functions,
|
||||
)
|
||||
|
||||
content = "def error(msg):\n pass\ndef warning(msg):\n pass\n"
|
||||
result = check_duplicate_display_functions(content, "/seedgo/apps/modules/audit.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "error" in result["message"]
|
||||
|
||||
def test_cli_branch_exempt(self):
|
||||
"""CLI branch is exempt (it defines these functions)."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_duplicate_display_functions,
|
||||
)
|
||||
|
||||
content = "def header(title):\n print(title)\n"
|
||||
result = check_duplicate_display_functions(content, "/cli/apps/modules/display.py")
|
||||
assert result is None
|
||||
|
||||
def test_prax_logger_exempt(self):
|
||||
"""Prax logger module is exempt (it is the logging system)."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_check import (
|
||||
check_duplicate_display_functions,
|
||||
)
|
||||
|
||||
content = "def error(msg):\n pass\n"
|
||||
result = check_duplicate_display_functions(content, "/prax/apps/modules/logger/log.py")
|
||||
assert result is None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 3. cli_flags_check sub-functions
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
# -- check_version_flag ------------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckVersionFlag:
|
||||
"""Tests for check_version_flag."""
|
||||
|
||||
def test_version_flag_found_passes(self):
|
||||
"""Entry point with '--version' string passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_flags_check import (
|
||||
check_version_flag,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"def main():",
|
||||
" if '--version' in args:",
|
||||
" print(VERSION)",
|
||||
]
|
||||
result = check_version_flag(lines, "/branch/apps/branch.py", None)
|
||||
assert result["passed"] is True
|
||||
assert "version flag" in result["message"].lower()
|
||||
|
||||
def test_short_version_flag_passes(self):
|
||||
"""Entry point with '-V' string passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_flags_check import (
|
||||
check_version_flag,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"def main():",
|
||||
" if '-V' in args:",
|
||||
" print(VERSION)",
|
||||
]
|
||||
result = check_version_flag(lines, "/branch/apps/branch.py", None)
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_no_version_flag_fails(self):
|
||||
"""Entry point without any version flag handling fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_flags_check import (
|
||||
check_version_flag,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"def main():",
|
||||
" print('hello')",
|
||||
]
|
||||
result = check_version_flag(lines, "/branch/apps/branch.py", None)
|
||||
assert result["passed"] is False
|
||||
assert "missing --version" in result["message"].lower()
|
||||
|
||||
def test_version_in_docstring_ignored(self):
|
||||
"""Version flag mentioned only in a docstring does not count."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_flags_check import (
|
||||
check_version_flag,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
'"""',
|
||||
"Supports --version flag",
|
||||
'"""',
|
||||
"def main():",
|
||||
" pass",
|
||||
]
|
||||
result = check_version_flag(lines, "/branch/apps/branch.py", None)
|
||||
assert result["passed"] is False
|
||||
|
||||
def test_bypassed_passes(self):
|
||||
"""Bypassed entry point passes regardless."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.cli_flags_check import (
|
||||
check_version_flag,
|
||||
)
|
||||
|
||||
lines: list[str] = ["def main():", " pass"]
|
||||
bypass = [{"standard": "cli_flags", "file": "branch.py"}]
|
||||
result = check_version_flag(lines, "/branch/apps/branch.py", bypass)
|
||||
assert result["passed"] is True
|
||||
assert "Bypassed" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 4. documentation_check sub-functions
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
# -- check_module_docstring --------------------------------------------------
|
||||
|
||||
|
||||
class TestCheckModuleDocstring:
|
||||
"""Tests for check_module_docstring."""
|
||||
|
||||
def test_docstring_present_passes(self):
|
||||
"""File with a module-level docstring passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_module_docstring,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"# META block",
|
||||
"",
|
||||
'"""Module docstring."""',
|
||||
"",
|
||||
"def foo():",
|
||||
]
|
||||
result = check_module_docstring(lines)
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_docstring_missing_fails(self):
|
||||
"""File without a docstring in the first 30 lines fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_module_docstring,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"# Just a comment",
|
||||
"import os",
|
||||
"import sys",
|
||||
"def foo():",
|
||||
" pass",
|
||||
] + ["# more code"] * 30
|
||||
result = check_module_docstring(lines)
|
||||
assert result["passed"] is False
|
||||
assert "Missing module-level docstring" in result["message"]
|
||||
|
||||
def test_single_quote_docstring_passes(self):
|
||||
"""Single-quote triple-quote docstring is accepted."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_module_docstring,
|
||||
)
|
||||
|
||||
lines: list[str] = ["'''Module docstring.'''", "", "def foo():"]
|
||||
result = check_module_docstring(lines)
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_docstring_after_meta_block_passes(self):
|
||||
"""Docstring after META header block is accepted."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_module_docstring,
|
||||
)
|
||||
|
||||
lines: list[str] = [
|
||||
"# ========= META =========",
|
||||
"# Name: foo.py",
|
||||
"# ========================",
|
||||
"",
|
||||
'"""',
|
||||
"Multi-line docstring.",
|
||||
'"""',
|
||||
]
|
||||
result = check_module_docstring(lines)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# -- check_function_docstrings -----------------------------------------------
|
||||
|
||||
|
||||
class TestCheckFunctionDocstrings:
|
||||
"""Tests for check_function_docstrings."""
|
||||
|
||||
def test_all_public_documented_passes(self):
|
||||
"""All public functions with docstrings passes."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_function_docstrings,
|
||||
)
|
||||
|
||||
content = 'def foo():\n """Does foo."""\n pass\ndef bar():\n """Does bar."""\n pass\n'
|
||||
lines = _lines(content)
|
||||
result = check_function_docstrings(content, lines)
|
||||
assert result["passed"] is True
|
||||
assert "2 public functions" in result["message"]
|
||||
|
||||
def test_missing_docstring_fails(self):
|
||||
"""Public function without docstring fails."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_function_docstrings,
|
||||
)
|
||||
|
||||
content = "def foo():\n pass\n"
|
||||
lines = _lines(content)
|
||||
result = check_function_docstrings(content, lines)
|
||||
assert result["passed"] is False
|
||||
assert "foo" in result["message"]
|
||||
|
||||
def test_private_functions_skipped(self):
|
||||
"""Private functions (starting with _) are not checked."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_function_docstrings,
|
||||
)
|
||||
|
||||
content = "def _private_helper():\n pass\n"
|
||||
lines = _lines(content)
|
||||
result = check_function_docstrings(content, lines)
|
||||
assert result["passed"] is True
|
||||
assert "No public functions" in result["message"]
|
||||
|
||||
def test_no_functions_passes(self):
|
||||
"""File with no functions passes vacuously."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_function_docstrings,
|
||||
)
|
||||
|
||||
content = "x = 42\ny = 43\n"
|
||||
lines = _lines(content)
|
||||
result = check_function_docstrings(content, lines)
|
||||
assert result["passed"] is True
|
||||
|
||||
def test_multiline_signature_docstring_found(self):
|
||||
"""Docstring after a multi-line function signature is detected."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.documentation_check import (
|
||||
check_function_docstrings,
|
||||
)
|
||||
|
||||
content = (
|
||||
'def compute(\n arg1: str,\n arg2: int,\n) -> bool:\n """Compute something."""\n return True\n'
|
||||
)
|
||||
lines = _lines(content)
|
||||
result = check_function_docstrings(content, lines)
|
||||
assert result["passed"] is True
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,963 @@
|
||||
"""Tests for seedgo checker handlers — batch 8 (7 checkers)."""
|
||||
|
||||
# =================== META ====================
|
||||
# Name: test_checkers_batch8.py
|
||||
# Description: Unit tests for handlers, log_handler, log_level, log_structure, meta, naming, permission_flags
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-04-25
|
||||
# Modified: 2026-04-25
|
||||
# =============================================
|
||||
|
||||
import pytest
|
||||
from typing import List
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _lines(text: str) -> List[str]:
|
||||
"""Split text into lines, widening LiteralString to str for pyright."""
|
||||
return text.split("\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_infrastructure(monkeypatch):
|
||||
"""Mock heavy infrastructure imports for standards checkers."""
|
||||
import sys
|
||||
|
||||
mock_logger = MagicMock()
|
||||
mock_json_handler = MagicMock()
|
||||
mock_json_handler.log_operation = MagicMock(return_value=True)
|
||||
|
||||
# -- prax ---------------------------------------------------------------
|
||||
prax_mod = MagicMock()
|
||||
prax_mod.logger = mock_logger
|
||||
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
|
||||
|
||||
# -- seedgo json handler ------------------------------------------------
|
||||
json_pkg = MagicMock()
|
||||
json_pkg.json_handler = mock_json_handler
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
|
||||
json_mod = MagicMock()
|
||||
json_mod.log_operation = mock_json_handler.log_operation
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
|
||||
|
||||
# -- bypass handler -----------------------------------------------------
|
||||
bypass_pkg = MagicMock()
|
||||
bypass_ignore = MagicMock()
|
||||
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
|
||||
bypass_pkg.ignore_handler = bypass_ignore
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.seedgo.apps.handlers.bypass.ignore_handler",
|
||||
bypass_ignore,
|
||||
)
|
||||
|
||||
# Force re-imports so checkers pick up fresh mocks
|
||||
for mod_name in [
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.handlers_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.log_handler_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.log_level_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.log_structure_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.meta_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.naming_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.permission_flags_check",
|
||||
]:
|
||||
monkeypatch.delitem(sys.modules, mod_name, raising=False)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 1. handlers_check -- check_handler_independence
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_handler_independence_clean(tmp_path):
|
||||
"""Handler with no cross-handler imports passes."""
|
||||
content = (
|
||||
'"""Clean handler."""\n'
|
||||
"from aipass.seedgo.apps.handlers.json import json_handler\n"
|
||||
"\ndef do_work():\n return True\n"
|
||||
)
|
||||
handler_path = str(tmp_path / "apps" / "handlers" / "audit" / "clean.py")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
result = check_handler_independence(content, _lines(content), handler_path)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_handler_independence_cross_import(tmp_path):
|
||||
"""Handler importing from another handler package fails."""
|
||||
content = (
|
||||
'"""Cross handler import."""\n'
|
||||
"from aipass.seedgo.apps.handlers.error import error_handler\n"
|
||||
"\ndef do_work():\n return True\n"
|
||||
)
|
||||
handler_path = str(tmp_path / "apps" / "handlers" / "audit" / "cross.py")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
result = check_handler_independence(content, _lines(content), handler_path)
|
||||
assert result["passed"] is False
|
||||
assert "Cross-handler imports" in result["message"]
|
||||
|
||||
|
||||
def test_handler_independence_same_package(tmp_path):
|
||||
"""Handler importing from same package passes."""
|
||||
content = (
|
||||
'"""Same package import."""\n'
|
||||
"from aipass.seedgo.apps.handlers.audit import audit_helper\n"
|
||||
"\ndef do_work():\n return True\n"
|
||||
)
|
||||
handler_path = str(tmp_path / "apps" / "handlers" / "audit" / "same.py")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_handler_independence,
|
||||
)
|
||||
|
||||
result = check_handler_independence(content, _lines(content), handler_path)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2. handlers_check -- check_auto_detection
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_auto_detection_not_needed():
|
||||
"""No module_name parameter means auto-detection is not needed (returns None)."""
|
||||
content = "def do_work(file_path):\n return True\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_auto_detection,
|
||||
)
|
||||
|
||||
result = check_auto_detection(content)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_auto_detection_present():
|
||||
"""Handler with module_name and inspect.stack() passes."""
|
||||
content = "import inspect\ndef do_work(module_name=None):\n frame = inspect.stack()\n return True\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_auto_detection,
|
||||
)
|
||||
|
||||
result = check_auto_detection(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_auto_detection_missing():
|
||||
"""Handler with module_name but no inspect.stack() fails."""
|
||||
content = "def do_work(module_name=None):\n return True\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_auto_detection,
|
||||
)
|
||||
|
||||
result = check_auto_detection(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "missing auto-detection" in result["message"]
|
||||
|
||||
|
||||
def test_auto_detection_with_get_caller():
|
||||
"""Handler with _get_caller_module_name passes auto-detection."""
|
||||
content = "def _get_caller_module_name():\n pass\ndef do_work(module_name=None):\n return True\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_auto_detection,
|
||||
)
|
||||
|
||||
result = check_auto_detection(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 3. handlers_check -- check_no_orchestration
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_no_orchestration_clean():
|
||||
"""Handler with no module imports passes."""
|
||||
content = '"""Clean handler."""\n\ndef do_work():\n return True\n'
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_no_orchestration,
|
||||
)
|
||||
|
||||
result = check_no_orchestration(content, _lines(content))
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_no_orchestration_module_import():
|
||||
"""Handler importing from apps.modules fails."""
|
||||
content = (
|
||||
'"""Bad handler."""\nfrom aipass.seedgo.apps.modules import audit_module\n\ndef do_work():\n return True\n'
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_no_orchestration,
|
||||
)
|
||||
|
||||
result = check_no_orchestration(content, _lines(content))
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "orchestration" in result["message"]
|
||||
|
||||
|
||||
def test_no_orchestration_in_docstring():
|
||||
"""Module import inside docstring is not flagged."""
|
||||
content = (
|
||||
'"""\nExample: from aipass.seedgo.apps.modules import audit_module\n"""\n\ndef do_work():\n return True\n'
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.handlers_check import (
|
||||
check_no_orchestration,
|
||||
)
|
||||
|
||||
result = check_no_orchestration(content, _lines(content))
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 4. log_handler_check -- check_no_raw_file_handler
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_no_raw_file_handler_clean():
|
||||
"""File without logging.FileHandler passes."""
|
||||
lines: List[str] = [
|
||||
'"""Clean module."""',
|
||||
"import logging",
|
||||
"logger = logging.getLogger(__name__)",
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_handler_check import (
|
||||
check_no_raw_file_handler,
|
||||
)
|
||||
|
||||
result = check_no_raw_file_handler(lines, "/fake/path.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_no_raw_file_handler_violation():
|
||||
"""File with logging.FileHandler fails."""
|
||||
# seedgo:bypass standard=log_handler reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
'"""Bad module."""',
|
||||
"import logging",
|
||||
'handler = logging.FileHandler("app.log")',
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_handler_check import (
|
||||
check_no_raw_file_handler,
|
||||
)
|
||||
|
||||
result = check_no_raw_file_handler(lines, "/fake/path.py")
|
||||
assert result["passed"] is False
|
||||
assert "FileHandler" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 5. log_handler_check -- check_no_raw_stream_handler
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_no_raw_stream_handler_no_file_logging():
|
||||
"""No file-based logging means stream handler check not applicable."""
|
||||
lines: List[str] = [
|
||||
"import logging",
|
||||
"logger = logging.getLogger(__name__)",
|
||||
"",
|
||||
]
|
||||
content = "\n".join(lines)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_handler_check import (
|
||||
check_no_raw_stream_handler,
|
||||
)
|
||||
|
||||
result = check_no_raw_stream_handler(lines, "/fake/path.py", content)
|
||||
assert result["passed"] is True
|
||||
assert "not applicable" in result["message"]
|
||||
|
||||
|
||||
def test_no_raw_stream_handler_violation():
|
||||
"""StreamHandler with file logging is a violation."""
|
||||
# seedgo:bypass standard=log_handler reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
"import logging",
|
||||
'handler = logging.FileHandler("app.log")',
|
||||
"stream = logging.StreamHandler()",
|
||||
"",
|
||||
]
|
||||
content = "\n".join(lines)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_handler_check import (
|
||||
check_no_raw_stream_handler,
|
||||
)
|
||||
|
||||
result = check_no_raw_stream_handler(lines, "/fake/path.py", content)
|
||||
assert result["passed"] is False
|
||||
assert "StreamHandler" in result["message"]
|
||||
|
||||
|
||||
def test_no_raw_stream_handler_clean():
|
||||
"""File logging present but no StreamHandler passes."""
|
||||
# seedgo:bypass standard=log_handler reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
"import logging",
|
||||
'handler = logging.FileHandler("app.log")',
|
||||
"",
|
||||
]
|
||||
content = "\n".join(lines)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_handler_check import (
|
||||
check_no_raw_stream_handler,
|
||||
)
|
||||
|
||||
result = check_no_raw_stream_handler(lines, "/fake/path.py", content)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 6. log_level_check -- check_error_not_user_input
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_error_not_user_input_clean():
|
||||
"""ERROR used for system failures passes."""
|
||||
lines: List[str] = [
|
||||
'"""Module."""',
|
||||
'logger.error("System crash: %s", error)',
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_level_check import (
|
||||
check_error_not_user_input,
|
||||
)
|
||||
|
||||
result = check_error_not_user_input(lines, "/fake/path.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_error_not_user_input_violation():
|
||||
"""ERROR used for user-input pattern fails."""
|
||||
# seedgo:bypass standard=log_level reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
'"""Module."""',
|
||||
'logger.error("Unknown command: %s", cmd)',
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_level_check import (
|
||||
check_error_not_user_input,
|
||||
)
|
||||
|
||||
result = check_error_not_user_input(lines, "/fake/path.py")
|
||||
assert result["passed"] is False
|
||||
assert "user input" in result["message"]
|
||||
|
||||
|
||||
def test_error_not_user_input_in_docstring():
|
||||
"""ERROR pattern inside a docstring is ignored."""
|
||||
# seedgo:bypass standard=log_level reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
'"""',
|
||||
'logger.error("Unknown command: %s", cmd)',
|
||||
'"""',
|
||||
"pass",
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_level_check import (
|
||||
check_error_not_user_input,
|
||||
)
|
||||
|
||||
result = check_error_not_user_input(lines, "/fake/path.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 7. log_level_check -- check_command_routing_level
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_command_routing_level_no_routing():
|
||||
"""File without command routing returns None."""
|
||||
content = "def do_work():\n pass\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_level_check import (
|
||||
check_command_routing_level,
|
||||
)
|
||||
|
||||
result = check_command_routing_level(content, _lines(content), "/fake/path.py")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_command_routing_level_clean():
|
||||
"""Command routing with proper WARNING level passes."""
|
||||
content = 'def route_command(cmd):\n logger.warning("Unknown command: %s", cmd)\n'
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_level_check import (
|
||||
check_command_routing_level,
|
||||
)
|
||||
|
||||
result = check_command_routing_level(content, _lines(content), "/fake/path.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_command_routing_level_violation():
|
||||
"""Command routing with ERROR for user-input pattern fails."""
|
||||
# seedgo:bypass standard=log_level reason="test data for checker validation"
|
||||
content = 'def route_command(cmd):\n logger.error("Unknown command: %s", cmd)\n'
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_level_check import (
|
||||
check_command_routing_level,
|
||||
)
|
||||
|
||||
result = check_command_routing_level(content, _lines(content), "/fake/path.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "ERROR" in result["message"] or "WARNING" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 8. log_structure_check -- check_branch_post
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_check_branch_post_no_logs(tmp_path):
|
||||
"""Branch with no logs returns empty violations."""
|
||||
branch = tmp_path / "mybranch"
|
||||
branch.mkdir()
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_structure_check import (
|
||||
check_branch_post,
|
||||
)
|
||||
|
||||
violations, scores = check_branch_post(str(branch))
|
||||
assert violations == []
|
||||
assert scores == []
|
||||
|
||||
|
||||
def test_check_branch_post_with_system_logs(tmp_path):
|
||||
"""Branch with local logs and system logs passes."""
|
||||
branch = tmp_path / "mybranch"
|
||||
branch.mkdir()
|
||||
logs_dir = branch / "logs"
|
||||
logs_dir.mkdir()
|
||||
(logs_dir / "app.log").write_text("log line", encoding="utf-8")
|
||||
|
||||
# Create repo-level registry and system_logs
|
||||
registry = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry.write_text("{}", encoding="utf-8")
|
||||
sys_logs = tmp_path / "system_logs"
|
||||
sys_logs.mkdir()
|
||||
(sys_logs / "mybranch_system.log").write_text("system log", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_structure_check import (
|
||||
check_branch_post,
|
||||
)
|
||||
|
||||
violations, scores = check_branch_post(str(branch))
|
||||
assert 100 in scores
|
||||
|
||||
|
||||
def test_check_branch_post_local_no_system(tmp_path):
|
||||
"""Branch with local logs but no system logs is flagged."""
|
||||
branch = tmp_path / "mybranch"
|
||||
branch.mkdir()
|
||||
logs_dir = branch / "logs"
|
||||
logs_dir.mkdir()
|
||||
(logs_dir / "app.log").write_text("log line", encoding="utf-8")
|
||||
|
||||
# Create repo-level registry and system_logs, but no branch-specific system log
|
||||
registry = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry.write_text("{}", encoding="utf-8")
|
||||
sys_logs = tmp_path / "system_logs"
|
||||
sys_logs.mkdir()
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.log_structure_check import (
|
||||
check_branch_post,
|
||||
)
|
||||
|
||||
violations, scores = check_branch_post(str(branch))
|
||||
assert 50 in scores
|
||||
assert len(violations) == 1
|
||||
assert "prax dispatch" in violations[0]["issues"][0]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 9. meta_check -- check_meta_presence
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_meta_presence_valid():
|
||||
"""Content with both header and footer markers passes."""
|
||||
content = (
|
||||
"# =================== AIPass ====================\n"
|
||||
"# Name: test.py\n"
|
||||
"# Description: Test file\n"
|
||||
"# Version: 1.0.0\n"
|
||||
"# Created: 2026-01-01\n"
|
||||
"# Modified: 2026-01-01\n"
|
||||
"# =============================================\n"
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.meta_check import (
|
||||
check_meta_presence,
|
||||
)
|
||||
|
||||
result = check_meta_presence(content)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_meta_presence_missing_header():
|
||||
"""Content without header marker fails."""
|
||||
content = "# Name: test.py\n# =============================================\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.meta_check import (
|
||||
check_meta_presence,
|
||||
)
|
||||
|
||||
result = check_meta_presence(content)
|
||||
assert result["passed"] is False
|
||||
assert "header" in result["message"]
|
||||
|
||||
|
||||
def test_meta_presence_legacy_header():
|
||||
"""Content with legacy META header passes."""
|
||||
content = (
|
||||
"# =================== META ====================\n"
|
||||
"# Name: test.py\n"
|
||||
"# =============================================\n"
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.meta_check import (
|
||||
check_meta_presence,
|
||||
)
|
||||
|
||||
result = check_meta_presence(content)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 10. meta_check -- check_meta_placement
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_meta_placement_at_top():
|
||||
"""META block at line 1 passes."""
|
||||
content = "# =================== AIPass ====================\nrest of file\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.meta_check import (
|
||||
check_meta_placement,
|
||||
)
|
||||
|
||||
result = check_meta_placement(content)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_meta_placement_not_at_top():
|
||||
"""META block not at line 1 fails."""
|
||||
content = '"""Docstring first."""\n# =================== AIPass ====================\n'
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.meta_check import (
|
||||
check_meta_placement,
|
||||
)
|
||||
|
||||
result = check_meta_placement(content)
|
||||
assert result["passed"] is False
|
||||
assert "first line" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 11. meta_check -- check_required_fields
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_required_fields_all_present():
|
||||
"""All required META fields present passes."""
|
||||
content = (
|
||||
"# =================== AIPass ====================\n"
|
||||
"# Name: test.py\n"
|
||||
"# Description: Test file\n"
|
||||
"# Version: 1.0.0\n"
|
||||
"# Created: 2026-01-01\n"
|
||||
"# Modified: 2026-01-01\n"
|
||||
"# =============================================\n"
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.meta_check import (
|
||||
check_required_fields,
|
||||
)
|
||||
|
||||
results = check_required_fields(content, "test.py")
|
||||
assert all(r["passed"] for r in results)
|
||||
|
||||
|
||||
def test_required_fields_missing_version():
|
||||
"""Missing Version field fails."""
|
||||
content = (
|
||||
"# =================== AIPass ====================\n"
|
||||
"# Name: test.py\n"
|
||||
"# Description: Test file\n"
|
||||
"# Created: 2026-01-01\n"
|
||||
"# Modified: 2026-01-01\n"
|
||||
"# =============================================\n"
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.meta_check import (
|
||||
check_required_fields,
|
||||
)
|
||||
|
||||
results = check_required_fields(content, "test.py")
|
||||
version_result = [r for r in results if "Version" in r["name"]]
|
||||
assert len(version_result) == 1
|
||||
assert version_result[0]["passed"] is False
|
||||
|
||||
|
||||
def test_required_fields_wrong_name():
|
||||
"""Name field not matching filename fails."""
|
||||
content = (
|
||||
"# =================== AIPass ====================\n"
|
||||
"# Name: wrong_name.py\n"
|
||||
"# Description: Test file\n"
|
||||
"# Version: 1.0.0\n"
|
||||
"# Created: 2026-01-01\n"
|
||||
"# Modified: 2026-01-01\n"
|
||||
"# =============================================\n"
|
||||
)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.meta_check import (
|
||||
check_required_fields,
|
||||
)
|
||||
|
||||
results = check_required_fields(content, "test.py")
|
||||
name_result = [r for r in results if "Name" in r["name"]]
|
||||
assert len(name_result) == 1
|
||||
assert name_result[0]["passed"] is False
|
||||
assert "wrong_name.py" in name_result[0]["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 12. naming_check -- check_file_naming
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_file_naming_snake_case(tmp_path):
|
||||
"""Snake_case filename passes."""
|
||||
f = tmp_path / "good_module.py"
|
||||
f.write_text("pass", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_file_naming,
|
||||
)
|
||||
|
||||
result = check_file_naming(str(f), f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_file_naming_bad_case(tmp_path):
|
||||
"""Uppercase filename fails."""
|
||||
f = tmp_path / "BadModule.py"
|
||||
f.write_text("pass", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_file_naming,
|
||||
)
|
||||
|
||||
result = check_file_naming(str(f), f)
|
||||
assert result["passed"] is False
|
||||
assert "invalid characters" in result["message"]
|
||||
|
||||
|
||||
def test_file_naming_redundant_prefix(tmp_path):
|
||||
"""Filename with redundant parent dir prefix fails."""
|
||||
parent = tmp_path / "audit"
|
||||
parent.mkdir()
|
||||
f = parent / "audit_ops.py"
|
||||
f.write_text("pass", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_file_naming,
|
||||
)
|
||||
|
||||
result = check_file_naming(str(f), f)
|
||||
assert result["passed"] is False
|
||||
assert "redundant prefix" in result["message"]
|
||||
|
||||
|
||||
def test_file_naming_init(tmp_path):
|
||||
"""__init__.py passes (Python-reserved)."""
|
||||
f = tmp_path / "__init__.py"
|
||||
f.write_text("", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_file_naming,
|
||||
)
|
||||
|
||||
result = check_file_naming(str(f), f)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 13. naming_check -- check_function_naming
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_function_naming_all_snake():
|
||||
"""All snake_case functions pass."""
|
||||
content = "def do_work():\n pass\n\ndef get_data():\n pass\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_function_naming,
|
||||
)
|
||||
|
||||
result = check_function_naming(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_function_naming_camel_case():
|
||||
"""CamelCase function fails."""
|
||||
content = "def DoWork():\n pass\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_function_naming,
|
||||
)
|
||||
|
||||
result = check_function_naming(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "DoWork" in result["message"]
|
||||
|
||||
|
||||
def test_function_naming_no_functions():
|
||||
"""No functions returns None."""
|
||||
content = "X = 42\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_function_naming,
|
||||
)
|
||||
|
||||
result = check_function_naming(content)
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_function_naming_dunder_skipped():
|
||||
"""Dunder methods are skipped from violation checks but still counted."""
|
||||
content = "class Foo:\n def __init__(self):\n pass\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_function_naming,
|
||||
)
|
||||
|
||||
result = check_function_naming(content)
|
||||
# __init__ is found but skipped from violation checks, so it passes
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 14. naming_check -- check_constant_naming
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_constant_naming_upper():
|
||||
"""UPPER_CASE constants pass."""
|
||||
content = 'MY_CONST = "hello"\nANOTHER = 42\n'
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_constant_naming,
|
||||
)
|
||||
|
||||
result = check_constant_naming(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_constant_naming_lowercase():
|
||||
"""Lowercase constants fail."""
|
||||
content = 'my_const = "hello"\n'
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_constant_naming,
|
||||
)
|
||||
|
||||
result = check_constant_naming(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "my_const" in result["message"]
|
||||
|
||||
|
||||
def test_constant_naming_function_call_ignored():
|
||||
"""Constants assigned via function call are ignored."""
|
||||
content = "logger = logging.getLogger(__name__)\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_constant_naming,
|
||||
)
|
||||
|
||||
result = check_constant_naming(content)
|
||||
# Function call assignment is skipped, no constants to check
|
||||
assert result is None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 15. naming_check -- check_class_naming
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_class_naming_pascal():
|
||||
"""PascalCase class passes."""
|
||||
content = "class MyClass:\n pass\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_class_naming,
|
||||
)
|
||||
|
||||
result = check_class_naming(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_class_naming_snake():
|
||||
"""snake_case class fails."""
|
||||
content = "class my_class:\n pass\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_class_naming,
|
||||
)
|
||||
|
||||
result = check_class_naming(content)
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "my_class" in result["message"]
|
||||
|
||||
|
||||
def test_class_naming_no_classes():
|
||||
"""No classes returns None."""
|
||||
content = "def do_work():\n pass\n"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.naming_check import (
|
||||
check_class_naming,
|
||||
)
|
||||
|
||||
result = check_class_naming(content)
|
||||
assert result is None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 16. permission_flags_check -- check_no_dangerous_flags
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_no_dangerous_flags_clean():
|
||||
"""File with only approved permission flags passes."""
|
||||
lines: List[str] = [
|
||||
'"""Clean module."""',
|
||||
'"--permission-mode bypassPermissions"',
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.permission_flags_check import (
|
||||
check_no_dangerous_flags,
|
||||
)
|
||||
|
||||
result = check_no_dangerous_flags(lines, "/fake/path.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_no_dangerous_flags_violation():
|
||||
"""File with prohibited permission bypass flag fails."""
|
||||
# seedgo:bypass standard=permission_flags reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
'"""Module."""',
|
||||
'cmd = "--dangerously-skip-permissions"',
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.permission_flags_check import (
|
||||
check_no_dangerous_flags,
|
||||
)
|
||||
|
||||
result = check_no_dangerous_flags(lines, "/fake/path.py")
|
||||
assert result["passed"] is False
|
||||
assert "Dangerous" in result["message"]
|
||||
|
||||
|
||||
def test_no_dangerous_flags_in_docstring():
|
||||
"""Prohibited flag inside docstring is ignored."""
|
||||
# seedgo:bypass standard=permission_flags reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
'"""',
|
||||
"Use --dangerously-skip-permissions for testing",
|
||||
'"""',
|
||||
"pass",
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.permission_flags_check import (
|
||||
check_no_dangerous_flags,
|
||||
)
|
||||
|
||||
result = check_no_dangerous_flags(lines, "/fake/path.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_no_dangerous_flags_skip_permissions():
|
||||
"""File with --skip-permissions fails."""
|
||||
# seedgo:bypass standard=permission_flags reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
'"""Module."""',
|
||||
'cmd = "--skip-permissions"',
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.permission_flags_check import (
|
||||
check_no_dangerous_flags,
|
||||
)
|
||||
|
||||
result = check_no_dangerous_flags(lines, "/fake/path.py")
|
||||
assert result["passed"] is False
|
||||
|
||||
|
||||
def test_no_dangerous_flags_bypass_rule():
|
||||
"""Prohibited flag bypassed by rule passes."""
|
||||
# seedgo:bypass standard=permission_flags reason="test data for checker validation"
|
||||
lines: List[str] = [
|
||||
'"""Module."""',
|
||||
'cmd = "--dangerously-skip-permissions"',
|
||||
"",
|
||||
]
|
||||
bypass_rules = [{"standard": "permission_flags", "file": "/fake/path.py"}]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.permission_flags_check import (
|
||||
check_no_dangerous_flags,
|
||||
)
|
||||
|
||||
result = check_no_dangerous_flags(lines, "/fake/path.py", bypass_rules=bypass_rules)
|
||||
assert result["passed"] is True
|
||||
@@ -0,0 +1,710 @@
|
||||
"""Tests for seedgo checker handlers — batch 9 (readme_check, trigger_check)."""
|
||||
|
||||
# =================== META ====================
|
||||
# Name: test_checkers_batch9.py
|
||||
# Description: Unit tests for readme_check and trigger_check
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-04-25
|
||||
# Modified: 2026-04-25
|
||||
# =============================================
|
||||
|
||||
import pytest
|
||||
from typing import List
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _lines(text: str) -> List[str]:
|
||||
"""Split text into lines, widening LiteralString to str for pyright."""
|
||||
return text.split("\n")
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_infrastructure(monkeypatch):
|
||||
"""Mock heavy infrastructure imports for standards checkers."""
|
||||
import sys
|
||||
|
||||
mock_logger = MagicMock()
|
||||
mock_json_handler = MagicMock()
|
||||
mock_json_handler.log_operation = MagicMock(return_value=True)
|
||||
|
||||
# -- prax ---------------------------------------------------------------
|
||||
prax_mod = MagicMock()
|
||||
prax_mod.logger = mock_logger
|
||||
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
|
||||
|
||||
# -- seedgo json handler ------------------------------------------------
|
||||
json_pkg = MagicMock()
|
||||
json_pkg.json_handler = mock_json_handler
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
|
||||
json_mod = MagicMock()
|
||||
json_mod.log_operation = mock_json_handler.log_operation
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
|
||||
|
||||
# -- bypass handler -----------------------------------------------------
|
||||
bypass_pkg = MagicMock()
|
||||
bypass_ignore = MagicMock()
|
||||
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
|
||||
bypass_pkg.ignore_handler = bypass_ignore
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.seedgo.apps.handlers.bypass.ignore_handler",
|
||||
bypass_ignore,
|
||||
)
|
||||
|
||||
# Force re-imports so checkers pick up fresh mocks
|
||||
for mod_name in [
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.readme_check",
|
||||
"aipass.seedgo.apps.handlers.aipass_standards.trigger_check",
|
||||
]:
|
||||
monkeypatch.delitem(sys.modules, mod_name, raising=False)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 1. readme_check -- check_readme_exists
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_readme_exists_present(tmp_path):
|
||||
"""README.md exists passes."""
|
||||
readme = tmp_path / "README.md"
|
||||
readme.write_text("# Branch\n", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_readme_exists,
|
||||
)
|
||||
|
||||
result = check_readme_exists(readme)
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_readme_exists_missing(tmp_path):
|
||||
"""README.md missing fails."""
|
||||
readme = tmp_path / "README.md"
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_readme_exists,
|
||||
)
|
||||
|
||||
result = check_readme_exists(readme)
|
||||
assert result["passed"] is False
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2. readme_check -- check_required_sections
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_required_sections_all_present():
|
||||
"""README with all required section groups passes."""
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"",
|
||||
"## Architecture",
|
||||
"Some content here.",
|
||||
"",
|
||||
"## Commands",
|
||||
"- cmd1",
|
||||
"",
|
||||
"## Integration Points",
|
||||
"Details here.",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_required_sections,
|
||||
)
|
||||
|
||||
result = check_required_sections(lines, "/fake/apps/entry.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_required_sections_missing_commands():
|
||||
"""README missing Commands/Usage section fails."""
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"",
|
||||
"## Architecture",
|
||||
"Some content.",
|
||||
"",
|
||||
"## Depends On",
|
||||
"Details.",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_required_sections,
|
||||
)
|
||||
|
||||
result = check_required_sections(lines, "/fake/apps/entry.py")
|
||||
assert result["passed"] is False
|
||||
assert "Commands/Usage" in result["message"]
|
||||
|
||||
|
||||
def test_required_sections_alternate_names():
|
||||
"""README with alternate section names (Usage, Directory Structure, Provides To) passes."""
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"",
|
||||
"## Directory Structure",
|
||||
"Tree here.",
|
||||
"",
|
||||
"## Usage",
|
||||
"- usage1",
|
||||
"",
|
||||
"## Provides To",
|
||||
"Other branches.",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_required_sections,
|
||||
)
|
||||
|
||||
result = check_required_sections(lines, "/fake/apps/entry.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 3. readme_check -- check_last_updated_freshness
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_last_updated_freshness_present(tmp_path):
|
||||
"""README with recent Last Updated passes."""
|
||||
from datetime import datetime
|
||||
|
||||
today = datetime.now().strftime("%Y-%m-%d")
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
f"*Last Updated: {today}*",
|
||||
"",
|
||||
]
|
||||
branch_root = tmp_path / "mybranch"
|
||||
branch_root.mkdir()
|
||||
apps_dir = branch_root / "apps"
|
||||
apps_dir.mkdir()
|
||||
(apps_dir / "entry.py").write_text("pass", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_last_updated_freshness,
|
||||
)
|
||||
|
||||
result = check_last_updated_freshness(lines, branch_root, "/fake/apps/entry.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_last_updated_freshness_missing():
|
||||
"""README without Last Updated date fails."""
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"No date here.",
|
||||
"",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_last_updated_freshness,
|
||||
)
|
||||
|
||||
# branch_root doesn't matter since date is missing
|
||||
from pathlib import Path
|
||||
|
||||
result = check_last_updated_freshness(lines, Path("/nonexistent"), "/fake/apps/entry.py")
|
||||
assert result["passed"] is False
|
||||
assert "Last Updated" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 4. readme_check -- check_directory_tree
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_directory_tree_accurate(tmp_path):
|
||||
"""Directory tree listing valid directories passes."""
|
||||
branch_root = tmp_path / "mybranch"
|
||||
apps_dir = branch_root / "apps"
|
||||
modules_dir = apps_dir / "modules"
|
||||
modules_dir.mkdir(parents=True)
|
||||
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"",
|
||||
"## Architecture",
|
||||
"",
|
||||
"```",
|
||||
"mybranch/",
|
||||
" apps/",
|
||||
" modules/",
|
||||
"```",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_directory_tree,
|
||||
)
|
||||
|
||||
result = check_directory_tree(lines, branch_root, "/fake/apps/entry.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_directory_tree_no_tree_section():
|
||||
"""README without a tree block passes (optional)."""
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"",
|
||||
"## Other Section",
|
||||
"Some content.",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_directory_tree,
|
||||
)
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
result = check_directory_tree(lines, Path("/nonexistent"), "/fake/apps/entry.py")
|
||||
assert result["passed"] is True
|
||||
assert "optional" in result["message"].lower() or "No directory" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 5. readme_check -- check_module_list
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_module_list_all_mentioned(tmp_path):
|
||||
"""All modules in apps/modules/ mentioned in README passes."""
|
||||
branch_root = tmp_path / "mybranch"
|
||||
modules_dir = branch_root / "apps" / "modules"
|
||||
modules_dir.mkdir(parents=True)
|
||||
(modules_dir / "audit.py").write_text("pass", encoding="utf-8")
|
||||
(modules_dir / "report.py").write_text("pass", encoding="utf-8")
|
||||
(modules_dir / "__init__.py").write_text("", encoding="utf-8")
|
||||
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"This branch has audit and report modules.",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_module_list,
|
||||
)
|
||||
|
||||
result = check_module_list(lines, branch_root, "/fake/apps/entry.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_module_list_missing_module(tmp_path):
|
||||
"""Module not mentioned in README fails."""
|
||||
branch_root = tmp_path / "mybranch"
|
||||
modules_dir = branch_root / "apps" / "modules"
|
||||
modules_dir.mkdir(parents=True)
|
||||
(modules_dir / "secret_module.py").write_text("pass", encoding="utf-8")
|
||||
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"No modules mentioned here.",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_module_list,
|
||||
)
|
||||
|
||||
result = check_module_list(lines, branch_root, "/fake/apps/entry.py")
|
||||
assert result["passed"] is False
|
||||
assert "secret_module" in result["message"]
|
||||
|
||||
|
||||
def test_module_list_no_modules_dir(tmp_path):
|
||||
"""No apps/modules/ directory passes (skipped)."""
|
||||
branch_root = tmp_path / "mybranch"
|
||||
branch_root.mkdir()
|
||||
|
||||
lines: List[str] = ["# Branch"]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_module_list,
|
||||
)
|
||||
|
||||
result = check_module_list(lines, branch_root, "/fake/apps/entry.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 6. readme_check -- check_command_list
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_command_list_present():
|
||||
"""Commands section with content passes."""
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"",
|
||||
"## Commands",
|
||||
"- `audit` - Run audit",
|
||||
"- `report` - Generate report",
|
||||
"",
|
||||
"## Other",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_command_list,
|
||||
)
|
||||
|
||||
result = check_command_list(lines, "/fake/apps/entry.py")
|
||||
assert result["passed"] is True
|
||||
assert "2 content lines" in result["message"]
|
||||
|
||||
|
||||
def test_command_list_empty():
|
||||
"""Commands section with no content fails."""
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"",
|
||||
"## Commands",
|
||||
"",
|
||||
"## Other Section",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_command_list,
|
||||
)
|
||||
|
||||
result = check_command_list(lines, "/fake/apps/entry.py")
|
||||
assert result["passed"] is False
|
||||
assert "empty" in result["message"].lower()
|
||||
|
||||
|
||||
def test_command_list_missing():
|
||||
"""No Commands section at all fails."""
|
||||
lines: List[str] = [
|
||||
"# Branch",
|
||||
"",
|
||||
"## Architecture",
|
||||
"Content here.",
|
||||
]
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.readme_check import (
|
||||
check_command_list,
|
||||
)
|
||||
|
||||
result = check_command_list(lines, "/fake/apps/entry.py")
|
||||
assert result["passed"] is False
|
||||
assert "No Commands" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 7. trigger_check -- is_handler_layer
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_is_handler_layer_true():
|
||||
"""File in handlers/ directory is handler layer."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
is_handler_layer,
|
||||
)
|
||||
|
||||
assert is_handler_layer("/src/aipass/seedgo/apps/handlers/audit/ops.py") is True
|
||||
|
||||
|
||||
def test_is_handler_layer_false():
|
||||
"""File in modules/ directory is not handler layer."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
is_handler_layer,
|
||||
)
|
||||
|
||||
assert is_handler_layer("/src/aipass/seedgo/apps/modules/audit.py") is False
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 8. trigger_check -- is_trigger_handler
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_is_trigger_handler_true():
|
||||
"""File in trigger handlers/events/ is a trigger handler."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
is_trigger_handler,
|
||||
)
|
||||
|
||||
assert is_trigger_handler("/apps/handlers/events/trigger_on_audit.py") is True
|
||||
|
||||
|
||||
def test_is_trigger_handler_false():
|
||||
"""File not in trigger handlers/events/ is not a trigger handler."""
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
is_trigger_handler,
|
||||
)
|
||||
|
||||
assert is_trigger_handler("/apps/modules/audit.py") is False
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 9. trigger_check -- check_no_logger_imports
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_no_logger_imports_clean():
|
||||
"""Handler without prax logger imports passes."""
|
||||
content = "def handle_event(**kwargs):\n pass\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_no_logger_imports,
|
||||
)
|
||||
|
||||
result = check_no_logger_imports(content, lines, "/fake/handler.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_no_logger_imports_violation():
|
||||
"""Handler importing prax logger fails."""
|
||||
content = "from prax import logger\n\ndef handle_event(**kwargs):\n pass\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_no_logger_imports,
|
||||
)
|
||||
|
||||
result = check_no_logger_imports(content, lines, "/fake/handler.py")
|
||||
assert result["passed"] is False
|
||||
assert "recursion" in result["message"]
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 10. trigger_check -- check_no_print_statements
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_no_print_statements_clean():
|
||||
"""Handler without print statements passes."""
|
||||
content = "def handle_event(**kwargs):\n return True\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_no_print_statements,
|
||||
)
|
||||
|
||||
result = check_no_print_statements(content, lines, "/fake/handler.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_no_print_statements_violation():
|
||||
"""Handler with print() fails."""
|
||||
content = 'def handle_event(**kwargs):\n print("debug")\n'
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_no_print_statements,
|
||||
)
|
||||
|
||||
result = check_no_print_statements(content, lines, "/fake/handler.py")
|
||||
assert result["passed"] is False
|
||||
assert "print()" in result["message"]
|
||||
|
||||
|
||||
def test_no_print_in_main_block_ok():
|
||||
"""print() inside __main__ block is allowed."""
|
||||
content = 'def handle_event(**kwargs):\n return True\n\nif __name__ == "__main__":\n print("testing")\n'
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_no_print_statements,
|
||||
)
|
||||
|
||||
result = check_no_print_statements(content, lines, "/fake/handler.py")
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 11. trigger_check -- check_trigger_import_pattern
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_trigger_import_pattern_correct():
|
||||
"""Correct trigger import pattern passes."""
|
||||
content = 'from trigger import trigger\n\ndef do_work():\n trigger.fire("event")\n'
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_trigger_import_pattern,
|
||||
)
|
||||
|
||||
result = check_trigger_import_pattern(content, lines, "/fake/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_trigger_import_pattern_missing():
|
||||
"""trigger.fire() without import fails."""
|
||||
content = 'def do_work():\n trigger.fire("event")\n'
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_trigger_import_pattern,
|
||||
)
|
||||
|
||||
result = check_trigger_import_pattern(content, lines, "/fake/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "missing proper import" in result["message"]
|
||||
|
||||
|
||||
def test_trigger_import_pattern_no_trigger():
|
||||
"""File not using trigger returns None."""
|
||||
content = "def do_work():\n return True\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_trigger_import_pattern,
|
||||
)
|
||||
|
||||
result = check_trigger_import_pattern(content, lines, "/fake/module.py")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_trigger_import_pattern_trigger_branch():
|
||||
"""Trigger branch file is exempt (self-reference)."""
|
||||
content = 'def fire(event):\n trigger.fire("event")\n'
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_trigger_import_pattern,
|
||||
)
|
||||
|
||||
result = check_trigger_import_pattern(content, lines, "/src/aipass/trigger/apps/modules/core.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 12. trigger_check -- check_handler_naming
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_handler_naming_correct():
|
||||
"""Handler function with handle_ prefix passes."""
|
||||
content = "def handle_audit_complete(**kwargs):\n pass\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_handler_naming,
|
||||
)
|
||||
|
||||
result = check_handler_naming(content, lines, "/fake/handler.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is True
|
||||
|
||||
|
||||
def test_handler_naming_bad():
|
||||
"""Handler function without handle_ prefix fails."""
|
||||
content = "def onHandleEvent(**kwargs):\n pass\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_handler_naming,
|
||||
)
|
||||
|
||||
result = check_handler_naming(content, lines, "/fake/handler.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
|
||||
|
||||
def test_handler_naming_no_handlers():
|
||||
"""File with no handler functions returns None."""
|
||||
content = "def do_work():\n pass\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_handler_naming,
|
||||
)
|
||||
|
||||
result = check_handler_naming(content, lines, "/fake/handler.py")
|
||||
assert result is None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 13. trigger_check -- check_missing_trigger_events
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_missing_trigger_events_lifecycle():
|
||||
"""Lifecycle function without trigger.fire() is flagged."""
|
||||
content = "def create_branch():\n pass\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_missing_trigger_events,
|
||||
)
|
||||
|
||||
result = check_missing_trigger_events(content, lines, "/fake/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert "create_" in result["message"]
|
||||
|
||||
|
||||
def test_missing_trigger_events_with_fire():
|
||||
"""Lifecycle function with trigger.fire() passes (returns None)."""
|
||||
content = 'def create_branch():\n trigger.fire("branch_created")\n'
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_missing_trigger_events,
|
||||
)
|
||||
|
||||
result = check_missing_trigger_events(content, lines, "/fake/module.py")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_missing_trigger_events_no_patterns():
|
||||
"""File with no event-like patterns returns None."""
|
||||
content = "def do_work():\n return True\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_missing_trigger_events,
|
||||
)
|
||||
|
||||
result = check_missing_trigger_events(content, lines, "/fake/module.py")
|
||||
assert result is None
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 14. trigger_check -- find_pattern_lines (via check_missing_trigger_events)
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_find_pattern_lines_detects_unlink():
|
||||
"""Inline .unlink() without trigger.fire() is flagged."""
|
||||
content = "def cleanup():\n path.unlink()\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_missing_trigger_events,
|
||||
)
|
||||
|
||||
result = check_missing_trigger_events(content, lines, "/fake/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert ".unlink()" in result["message"]
|
||||
|
||||
|
||||
def test_find_pattern_lines_detects_rename():
|
||||
"""Inline .rename() without trigger.fire() is flagged."""
|
||||
content = "def move_file():\n path.rename(new_path)\n"
|
||||
lines = _lines(content)
|
||||
|
||||
from aipass.seedgo.apps.handlers.aipass_standards.trigger_check import (
|
||||
check_missing_trigger_events,
|
||||
)
|
||||
|
||||
result = check_missing_trigger_events(content, lines, "/fake/module.py")
|
||||
assert result is not None
|
||||
assert result["passed"] is False
|
||||
assert ".rename()" in result["message"]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,607 @@
|
||||
"""Tests for seedgo handler functions (audit_display, diagnostics, json extras, readme, hooks_ext)."""
|
||||
|
||||
# =================== META ====================
|
||||
# Name: test_handler_functions.py
|
||||
# Description: Unit tests for handler-level functions across multiple handler packages
|
||||
# Version: 1.0.0
|
||||
# Created: 2026-04-25
|
||||
# Modified: 2026-04-25
|
||||
# =============================================
|
||||
|
||||
import json
|
||||
import pytest
|
||||
from typing import Dict
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _mock_infrastructure(monkeypatch):
|
||||
"""Mock heavy infrastructure imports for handler functions."""
|
||||
import sys
|
||||
|
||||
mock_logger = MagicMock()
|
||||
mock_json_handler = MagicMock()
|
||||
mock_json_handler.log_operation = MagicMock(return_value=True)
|
||||
|
||||
# -- prax ---------------------------------------------------------------
|
||||
prax_mod = MagicMock()
|
||||
prax_mod.logger = mock_logger
|
||||
monkeypatch.setitem(sys.modules, "aipass.prax", prax_mod)
|
||||
|
||||
# -- seedgo json handler ------------------------------------------------
|
||||
json_pkg = MagicMock()
|
||||
json_pkg.json_handler = mock_json_handler
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json", json_pkg)
|
||||
json_mod = MagicMock()
|
||||
json_mod.log_operation = mock_json_handler.log_operation
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.json.json_handler", json_mod)
|
||||
|
||||
# -- bypass handler -----------------------------------------------------
|
||||
bypass_pkg = MagicMock()
|
||||
bypass_ignore = MagicMock()
|
||||
bypass_ignore.get_template_ignore_patterns = MagicMock(return_value=[])
|
||||
bypass_ignore.get_audit_ignore_patterns = MagicMock(return_value=[])
|
||||
bypass_pkg.ignore_handler = bypass_ignore
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.bypass", bypass_pkg)
|
||||
monkeypatch.setitem(
|
||||
sys.modules,
|
||||
"aipass.seedgo.apps.handlers.bypass.ignore_handler",
|
||||
bypass_ignore,
|
||||
)
|
||||
|
||||
# -- cli (console for audit_display) ------------------------------------
|
||||
mock_console = MagicMock()
|
||||
cli_mod = MagicMock()
|
||||
cli_mod.console = mock_console
|
||||
monkeypatch.setitem(sys.modules, "aipass.cli", cli_mod)
|
||||
|
||||
# -- cli.apps.modules (warning function for hooks_ext) ------------------
|
||||
cli_apps = MagicMock()
|
||||
cli_apps_modules = MagicMock()
|
||||
cli_apps_modules.warning = MagicMock()
|
||||
cli_apps.modules = cli_apps_modules
|
||||
cli_mod.apps = cli_apps
|
||||
monkeypatch.setitem(sys.modules, "aipass.cli.apps", cli_apps)
|
||||
monkeypatch.setitem(sys.modules, "aipass.cli.apps.modules", cli_apps_modules)
|
||||
|
||||
# -- file handler (for hooks_ext) ---------------------------------------
|
||||
file_handler_mod = MagicMock()
|
||||
file_handler_mod.read_lines_safe = MagicMock(return_value=[])
|
||||
file_handler_mod.read_text_safe = MagicMock(return_value=None)
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.file", file_handler_mod)
|
||||
|
||||
# -- hooks handler (for hooks_ext) --------------------------------------
|
||||
hooks_handler_mod = MagicMock()
|
||||
hooks_handler_mod.run_pytest_file = MagicMock(return_value=(3, 0, 1.5))
|
||||
monkeypatch.setitem(sys.modules, "aipass.seedgo.apps.handlers.hooks", hooks_handler_mod)
|
||||
|
||||
# -- rich.table (for hooks_ext) -----------------------------------------
|
||||
rich_table_mod = MagicMock()
|
||||
monkeypatch.setitem(sys.modules, "rich.table", rich_table_mod)
|
||||
monkeypatch.setitem(sys.modules, "rich", MagicMock())
|
||||
monkeypatch.setitem(sys.modules, "rich.console", MagicMock())
|
||||
|
||||
# Force re-imports so handler modules pick up fresh mocks
|
||||
for mod_name in [
|
||||
"aipass.seedgo.apps.handlers.audit.audit_display",
|
||||
"aipass.seedgo.apps.handlers.diagnostics.diagnostics_check",
|
||||
"aipass.seedgo.apps.handlers.json.json_handler",
|
||||
"aipass.seedgo.apps.handlers.readme.readme_ops",
|
||||
"aipass.seedgo.apps.handlers.readme.readme_generator",
|
||||
"aipass.seedgo.apps.modules.hooks_ext",
|
||||
]:
|
||||
monkeypatch.delitem(sys.modules, mod_name, raising=False)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 1. audit_display -- print_branch_summary
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_print_branch_summary_basic():
|
||||
"""print_branch_summary runs without error on minimal audit result."""
|
||||
from aipass.seedgo.apps.handlers.audit.audit_display import print_branch_summary
|
||||
|
||||
audit_result: Dict = {
|
||||
"branch": {"name": "seedgo"},
|
||||
"scores": {"meta": 100, "naming": 90},
|
||||
"average": 95,
|
||||
"files_checked": 10,
|
||||
"results": {},
|
||||
}
|
||||
# Should not raise
|
||||
print_branch_summary(audit_result)
|
||||
|
||||
|
||||
def test_print_branch_summary_with_violations():
|
||||
"""print_branch_summary handles violation lists."""
|
||||
from aipass.seedgo.apps.handlers.audit.audit_display import print_branch_summary
|
||||
|
||||
audit_result: Dict = {
|
||||
"branch": {"name": "testbranch"},
|
||||
"scores": {"meta": 60, "naming": 80},
|
||||
"average": 70,
|
||||
"files_checked": 5,
|
||||
"results": {
|
||||
"meta": {"checks": [{"name": "META block present", "passed": False, "message": "Missing META block"}]}
|
||||
},
|
||||
"meta_violations": [{"path": "file.py", "score": 50, "issues": ["Missing META block"]}],
|
||||
}
|
||||
# Should not raise
|
||||
print_branch_summary(audit_result)
|
||||
|
||||
|
||||
def test_print_branch_summary_with_system_averages():
|
||||
"""print_branch_summary handles optional system averages."""
|
||||
from aipass.seedgo.apps.handlers.audit.audit_display import print_branch_summary
|
||||
|
||||
audit_result: Dict = {
|
||||
"branch": {"name": "seedgo"},
|
||||
"scores": {"meta": 100},
|
||||
"average": 100,
|
||||
"files_checked": 1,
|
||||
"results": {},
|
||||
}
|
||||
system_averages: Dict[str, int] = {"meta": 90}
|
||||
# Should not raise
|
||||
print_branch_summary(audit_result, system_averages, 90)
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 2. diagnostics_check -- check_directory
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_check_directory_missing(tmp_path):
|
||||
"""check_directory on nonexistent directory returns error."""
|
||||
from aipass.seedgo.apps.handlers.diagnostics.diagnostics_check import (
|
||||
check_directory,
|
||||
)
|
||||
|
||||
result = check_directory(str(tmp_path / "nonexistent"))
|
||||
assert result["total_files"] == 0
|
||||
assert "error" in result
|
||||
assert "not found" in result["error"].lower()
|
||||
|
||||
|
||||
def test_check_directory_exists(tmp_path):
|
||||
"""check_directory on existing directory calls pyright."""
|
||||
# Create a Python file
|
||||
py_file = tmp_path / "example.py"
|
||||
py_file.write_text("x = 1\n", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.diagnostics.diagnostics_check import (
|
||||
check_directory,
|
||||
)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(
|
||||
stdout=json.dumps(
|
||||
{
|
||||
"generalDiagnostics": [],
|
||||
"summary": {"filesAnalyzed": 1},
|
||||
}
|
||||
),
|
||||
stderr="",
|
||||
)
|
||||
result = check_directory(str(tmp_path))
|
||||
|
||||
assert result["total_errors"] == 0
|
||||
assert result["total_files"] == 1
|
||||
|
||||
|
||||
def test_check_directory_with_errors(tmp_path):
|
||||
"""check_directory reports errors from pyright output."""
|
||||
py_file = tmp_path / "bad.py"
|
||||
py_file.write_text("x: int = 'not_int'\n", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.diagnostics.diagnostics_check import (
|
||||
check_directory,
|
||||
)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(
|
||||
stdout=json.dumps(
|
||||
{
|
||||
"generalDiagnostics": [
|
||||
{
|
||||
"file": str(py_file),
|
||||
"severity": "error",
|
||||
"range": {"start": {"line": 0}},
|
||||
"message": "Type mismatch",
|
||||
"rule": "reportAssignment",
|
||||
}
|
||||
],
|
||||
"summary": {"filesAnalyzed": 1},
|
||||
}
|
||||
),
|
||||
stderr="",
|
||||
)
|
||||
result = check_directory(str(tmp_path))
|
||||
|
||||
assert result["total_errors"] == 1
|
||||
assert result["files_with_errors"] == 1
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 3. json_handler -- increment_counter
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def _get_real_json_handler(tmp_path, monkeypatch):
|
||||
"""Import the real json_handler module with a tmp_path JSON directory."""
|
||||
import importlib
|
||||
import sys
|
||||
|
||||
# Remove ALL json_handler mocks/caches so a fresh import is forced
|
||||
keys_to_remove = [k for k in sys.modules if "json_handler" in k or "handlers.json" in k]
|
||||
for key in keys_to_remove:
|
||||
sys.modules.pop(key, None)
|
||||
|
||||
# Ensure prax logger mock is in place for the real module
|
||||
prax = MagicMock()
|
||||
sys.modules["aipass.prax"] = prax
|
||||
|
||||
# Fresh import of the real module
|
||||
jh_mod = importlib.import_module("aipass.seedgo.apps.handlers.json.json_handler")
|
||||
|
||||
monkeypatch.setattr(jh_mod, "_BRANCH_ROOT", tmp_path)
|
||||
monkeypatch.setattr(jh_mod, "_BRANCH_NAME", "test")
|
||||
monkeypatch.setattr(jh_mod, "JSON_DIR", tmp_path / "test_json")
|
||||
return jh_mod
|
||||
|
||||
|
||||
def test_increment_counter(tmp_path, monkeypatch):
|
||||
"""increment_counter increments a named counter in data JSON."""
|
||||
jh = _get_real_json_handler(tmp_path, monkeypatch)
|
||||
|
||||
result = jh.increment_counter("testmod", "runs", 1)
|
||||
assert result is True
|
||||
|
||||
# Verify counter was set
|
||||
data = jh.load_json("testmod", "data")
|
||||
assert data is not None
|
||||
assert data["runs"] == 1
|
||||
|
||||
# Increment again
|
||||
jh.increment_counter("testmod", "runs", 5)
|
||||
data = jh.load_json("testmod", "data")
|
||||
assert data is not None
|
||||
assert data["runs"] == 6
|
||||
|
||||
|
||||
def test_increment_counter_new_counter(tmp_path, monkeypatch):
|
||||
"""increment_counter creates a new counter if it does not exist."""
|
||||
jh = _get_real_json_handler(tmp_path, monkeypatch)
|
||||
|
||||
result = jh.increment_counter("testmod", "new_counter", 10)
|
||||
assert result is True
|
||||
|
||||
data = jh.load_json("testmod", "data")
|
||||
assert data is not None
|
||||
assert data["new_counter"] == 10
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 4. json_handler -- update_data_metrics
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_update_data_metrics(tmp_path, monkeypatch):
|
||||
"""update_data_metrics sets arbitrary metrics in data JSON."""
|
||||
jh = _get_real_json_handler(tmp_path, monkeypatch)
|
||||
|
||||
result = jh.update_data_metrics("testmod", score=95, status="ok")
|
||||
assert result is True
|
||||
|
||||
data = jh.load_json("testmod", "data")
|
||||
assert data is not None
|
||||
assert data["score"] == 95
|
||||
assert data["status"] == "ok"
|
||||
|
||||
|
||||
def test_update_data_metrics_overwrites(tmp_path, monkeypatch):
|
||||
"""update_data_metrics overwrites existing metrics."""
|
||||
jh = _get_real_json_handler(tmp_path, monkeypatch)
|
||||
|
||||
jh.update_data_metrics("testmod", score=50)
|
||||
jh.update_data_metrics("testmod", score=99)
|
||||
|
||||
data = jh.load_json("testmod", "data")
|
||||
assert data is not None
|
||||
assert data["score"] == 99
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 5. readme_ops -- resolve_branch
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_resolve_branch_found(tmp_path, monkeypatch):
|
||||
"""resolve_branch finds a branch in the registry."""
|
||||
registry_data = {
|
||||
"branches": [
|
||||
{"name": "seedgo", "path": "/src/aipass/seedgo"},
|
||||
{"name": "drone", "path": "/src/aipass/drone"},
|
||||
]
|
||||
}
|
||||
registry_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry_path.write_text(json.dumps(registry_data), encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme import readme_ops
|
||||
|
||||
monkeypatch.setattr(readme_ops, "_find_registry", lambda: registry_path)
|
||||
|
||||
result = readme_ops.resolve_branch("@seedgo")
|
||||
assert result is not None
|
||||
assert result["name"] == "seedgo"
|
||||
|
||||
|
||||
def test_resolve_branch_not_found(tmp_path, monkeypatch):
|
||||
"""resolve_branch returns None for unknown branch."""
|
||||
registry_data = {"branches": [{"name": "seedgo"}]}
|
||||
registry_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry_path.write_text(json.dumps(registry_data), encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme import readme_ops
|
||||
|
||||
monkeypatch.setattr(readme_ops, "_find_registry", lambda: registry_path)
|
||||
|
||||
result = readme_ops.resolve_branch("@nonexistent")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_resolve_branch_no_registry(tmp_path, monkeypatch):
|
||||
"""resolve_branch returns None when registry does not exist."""
|
||||
from aipass.seedgo.apps.handlers.readme import readme_ops
|
||||
|
||||
monkeypatch.setattr(readme_ops, "_find_registry", lambda: tmp_path / "MISSING.json")
|
||||
|
||||
result = readme_ops.resolve_branch("@seedgo")
|
||||
assert result is None
|
||||
|
||||
|
||||
def test_resolve_branch_alias(tmp_path, monkeypatch):
|
||||
"""resolve_branch resolves aliases."""
|
||||
registry_data = {
|
||||
"branches": [
|
||||
{"name": "seedgo", "aliases": ["@sg", "@standards"]},
|
||||
]
|
||||
}
|
||||
registry_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry_path.write_text(json.dumps(registry_data), encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme import readme_ops
|
||||
|
||||
monkeypatch.setattr(readme_ops, "_find_registry", lambda: registry_path)
|
||||
|
||||
result = readme_ops.resolve_branch("@sg")
|
||||
assert result is not None
|
||||
assert result["name"] == "seedgo"
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 6. readme_ops -- get_all_branches
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_get_all_branches(tmp_path, monkeypatch):
|
||||
"""get_all_branches returns all branches from registry."""
|
||||
registry_data = {
|
||||
"branches": [
|
||||
{"name": "seedgo"},
|
||||
{"name": "drone"},
|
||||
{"name": "spawn"},
|
||||
]
|
||||
}
|
||||
registry_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry_path.write_text(json.dumps(registry_data), encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme import readme_ops
|
||||
|
||||
monkeypatch.setattr(readme_ops, "_find_registry", lambda: registry_path)
|
||||
|
||||
branches = readme_ops.get_all_branches()
|
||||
assert len(branches) == 3
|
||||
|
||||
|
||||
def test_get_all_branches_empty_registry(tmp_path, monkeypatch):
|
||||
"""get_all_branches returns empty list when registry has no branches."""
|
||||
registry_path = tmp_path / "AIPASS_REGISTRY.json"
|
||||
registry_path.write_text(json.dumps({"branches": []}), encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme import readme_ops
|
||||
|
||||
monkeypatch.setattr(readme_ops, "_find_registry", lambda: registry_path)
|
||||
|
||||
branches = readme_ops.get_all_branches()
|
||||
assert branches == []
|
||||
|
||||
|
||||
def test_get_all_branches_no_registry(tmp_path, monkeypatch):
|
||||
"""get_all_branches returns empty list when registry is missing."""
|
||||
from aipass.seedgo.apps.handlers.readme import readme_ops
|
||||
|
||||
monkeypatch.setattr(readme_ops, "_find_registry", lambda: tmp_path / "MISSING.json")
|
||||
|
||||
branches = readme_ops.get_all_branches()
|
||||
assert branches == []
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 7. readme_generator -- generate_commands_section
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_generate_commands_section_no_entry_point(tmp_path):
|
||||
"""generate_commands_section returns empty when no entry point exists."""
|
||||
branch_dir = tmp_path / "mybranch"
|
||||
apps_dir = branch_dir / "apps"
|
||||
apps_dir.mkdir(parents=True)
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme.readme_generator import (
|
||||
generate_commands_section,
|
||||
)
|
||||
|
||||
result = generate_commands_section(str(branch_dir))
|
||||
assert result == ""
|
||||
|
||||
|
||||
def test_generate_commands_section_with_help(tmp_path):
|
||||
"""generate_commands_section parses help output."""
|
||||
branch_dir = tmp_path / "mybranch"
|
||||
apps_dir = branch_dir / "apps"
|
||||
apps_dir.mkdir(parents=True)
|
||||
entry = apps_dir / "mybranch.py"
|
||||
entry.write_text("pass", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme.readme_generator import (
|
||||
generate_commands_section,
|
||||
)
|
||||
|
||||
with patch("subprocess.run") as mock_run:
|
||||
mock_run.return_value = MagicMock(
|
||||
stdout="Commands: audit, report, check\n",
|
||||
stderr="",
|
||||
)
|
||||
result = generate_commands_section(str(branch_dir))
|
||||
|
||||
assert "audit" in result
|
||||
assert "report" in result
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 8. readme_generator -- generate_header_section
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_generate_header_section_with_passport(tmp_path):
|
||||
"""generate_header_section reads passport.json and produces header."""
|
||||
branch_dir = tmp_path / "mybranch"
|
||||
trinity_dir = branch_dir / ".trinity"
|
||||
trinity_dir.mkdir(parents=True)
|
||||
passport = {
|
||||
"branch_info": {
|
||||
"branch_name": "MYBRANCH",
|
||||
"path": str(branch_dir),
|
||||
"profile": "library",
|
||||
"created": "2026-01-01",
|
||||
"role": "Testing branch",
|
||||
}
|
||||
}
|
||||
(trinity_dir / "passport.json").write_text(json.dumps(passport), encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme.readme_generator import (
|
||||
generate_header_section,
|
||||
)
|
||||
|
||||
result = generate_header_section(str(branch_dir))
|
||||
assert "MYBRANCH" in result
|
||||
assert "library" in result
|
||||
assert "Testing branch" in result
|
||||
|
||||
|
||||
def test_generate_header_section_no_passport(tmp_path):
|
||||
"""generate_header_section returns empty when passport is missing."""
|
||||
branch_dir = tmp_path / "mybranch"
|
||||
branch_dir.mkdir()
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme.readme_generator import (
|
||||
generate_header_section,
|
||||
)
|
||||
|
||||
result = generate_header_section(str(branch_dir))
|
||||
assert result == ""
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 9. readme_generator -- update_readme_auto_sections
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_update_readme_auto_sections_dry_run(tmp_path):
|
||||
"""update_readme_auto_sections in dry_run mode does not modify the file."""
|
||||
branch_dir = tmp_path / "mybranch"
|
||||
branch_dir.mkdir()
|
||||
readme = branch_dir / "README.md"
|
||||
original = "# Branch\n<!-- AUTO:LAST_UPDATED -->\n*Last Updated: 2025-01-01*\n<!-- /AUTO:LAST_UPDATED -->\n"
|
||||
readme.write_text(original, encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme.readme_generator import (
|
||||
update_readme_auto_sections,
|
||||
)
|
||||
|
||||
result = update_readme_auto_sections(str(branch_dir), dry_run=True)
|
||||
assert result["dry_run"] is True
|
||||
# File should not be modified in dry run
|
||||
assert readme.read_text(encoding="utf-8") == original
|
||||
|
||||
|
||||
def test_update_readme_auto_sections_no_readme(tmp_path):
|
||||
"""update_readme_auto_sections reports error when README is missing."""
|
||||
branch_dir = tmp_path / "mybranch"
|
||||
branch_dir.mkdir()
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme.readme_generator import (
|
||||
update_readme_auto_sections,
|
||||
)
|
||||
|
||||
result = update_readme_auto_sections(str(branch_dir))
|
||||
assert "README.md not found" in result["errors"]
|
||||
|
||||
|
||||
def test_update_readme_auto_sections_missing_markers(tmp_path):
|
||||
"""update_readme_auto_sections reports missing markers."""
|
||||
branch_dir = tmp_path / "mybranch"
|
||||
branch_dir.mkdir()
|
||||
readme = branch_dir / "README.md"
|
||||
readme.write_text("# Branch\nNo markers here.\n", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.handlers.readme.readme_generator import (
|
||||
update_readme_auto_sections,
|
||||
)
|
||||
|
||||
result = update_readme_auto_sections(str(branch_dir))
|
||||
# Some sections should report missing markers
|
||||
assert len(result["missing_markers"]) > 0 or len(result["updated"]) == 0
|
||||
|
||||
|
||||
# ===========================================================================
|
||||
# 10. hooks_ext -- run_hooks_test
|
||||
# ===========================================================================
|
||||
|
||||
|
||||
def test_run_hooks_test_no_files(tmp_path):
|
||||
"""run_hooks_test with no test files shows warning."""
|
||||
from aipass.seedgo.apps.modules.hooks_ext import run_hooks_test
|
||||
|
||||
# repo_root with no test_hooks*.py files
|
||||
run_hooks_test(tmp_path)
|
||||
# Should call warning() -- verify through mock
|
||||
import sys
|
||||
|
||||
cli_modules = sys.modules["aipass.cli.apps.modules"]
|
||||
cli_modules.warning.assert_called()
|
||||
|
||||
|
||||
def test_run_hooks_test_with_files(tmp_path):
|
||||
"""run_hooks_test with test files builds and prints table."""
|
||||
import sys
|
||||
|
||||
# Create the expected directory structure
|
||||
test_dir = tmp_path / "src" / "aipass" / "seedgo" / "tests"
|
||||
test_dir.mkdir(parents=True)
|
||||
(test_dir / "test_hooks_basic.py").write_text("pass", encoding="utf-8")
|
||||
|
||||
from aipass.seedgo.apps.modules.hooks_ext import run_hooks_test
|
||||
|
||||
run_hooks_test(tmp_path)
|
||||
|
||||
# Verify console.print was called (table output)
|
||||
cli_mod = sys.modules["aipass.cli"]
|
||||
assert cli_mod.console.print.called
|
||||
Reference in New Issue
Block a user