S55: test quality expansion, spawn template READMEs, gitignore exceptions (#127)

* feat(devpulse): S55: test quality expansion, spawn template READMEs, gitignore exceptions

Co-Authored-By: @devpulse <devpulse@aipass>

* feat: S55 — test quality 10-category expansion, spawn template READMEs, gitignore exceptions

Seedgo: test_quality checker expanded from 8 to 48 items across 10 categories
(CLI routing, conftest, error resilience, contracts, init, mocking).
Custom function scanner (test_map) built per DPLAN-0060. Audit display
truncation fixed. 5 new reference templates in seedgo/templates/.

Spawn: 23 READMEs added to builder template — every directory documented.
New dirs: docs.local/, logs/, templates/, sub_agent_drops/, custom_config/.
Dead .spawn files (.backup_ignore.json, .migrations.json) archived.
verify_branch.py archived (redundant with seedgo architecture checker).

Repo: .gitignore exceptions added so spawn template files bypass system-wide
ignores. Template is now fully trackable by git.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: @devpulse <devpulse@aipass>
Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
AIPass
2026-03-25 00:16:39 -07:00
committed by GitHub
co-authored by Claude Opus 4.6 @devpulse
parent f19f7ac12d
commit f49f9e2a06
48 changed files with 4829 additions and 4 deletions
+26
View File
@@ -52,6 +52,32 @@ docs.local/
# Disabled files (AIPass convention: rename with (disabled) instead of delete)
*(disabled)
# Spawn template exceptions (template files must be tracked for public repo)
!src/aipass/spawn/templates/builder/.trinity/
!src/aipass/spawn/templates/builder/.trinity/**
!src/aipass/spawn/templates/builder/.ai_mail.local/
!src/aipass/spawn/templates/builder/.ai_mail.local/**
!src/aipass/spawn/templates/builder/.archive/
!src/aipass/spawn/templates/builder/.archive/**
!src/aipass/spawn/templates/builder/.spawn/
!src/aipass/spawn/templates/builder/.spawn/**
!src/aipass/spawn/templates/builder/.claude/
!src/aipass/spawn/templates/builder/.claude/**
!src/aipass/spawn/templates/builder/*_json/
!src/aipass/spawn/templates/builder/*_json/**
!src/aipass/spawn/templates/builder/logs/
!src/aipass/spawn/templates/builder/logs/**
!src/aipass/spawn/templates/builder/artifacts/
!src/aipass/spawn/templates/builder/artifacts/**
!src/aipass/spawn/templates/builder/dropbox/
!src/aipass/spawn/templates/builder/dropbox/**
!src/aipass/spawn/templates/builder/tools/
!src/aipass/spawn/templates/builder/tools/**
!src/aipass/spawn/templates/builder/docs.local/
!src/aipass/spawn/templates/builder/docs.local/**
!src/aipass/spawn/templates/builder/DASHBOARD.local.json
!src/aipass/spawn/templates/builder/STATUS.local.md
# OS
.DS_Store
@@ -0,0 +1,623 @@
# =================== AIPass ====================
# Name: test_json_handler_template.py
# Description: Universal JSON Handler Test Template (DPLAN-0059)
# Version: 1.0.0
# Created: 2026-03-25
# Modified: 2026-03-25
# =============================================
"""
Universal JSON Handler Test Template
Copy this file to any AIPass branch's tests/ directory.
Change BRANCH_MODULE below. Run with pytest.
Covers 43 tests across 8 groups:
- _create_default / default templates (4)
- validate_json_structure (10)
- get_json_path (3)
- ensure_json_exists (5)
- load_json (4)
- save_json (5)
- log_operation (7)
- ensure_module_jsons (5)
"""
import importlib
import json
import sys
import types
from datetime import datetime
from pathlib import Path
from typing import Any
import pytest
# ============ BRANCH CONFIG ============
# Change these two lines when deploying to a branch:
BRANCH_MODULE = "seedgo" # e.g. "prax", "drone", "backup", "cli", etc.
# For commons: "commons" (import path is different: aipass -> just commons)
# For skills: "skills" (import path is different: aipass -> just skills)
# =======================================
# ---------------------------------------------------------------------------
# Dynamic import with cross-branch guard bypass
# ---------------------------------------------------------------------------
# Every branch has an import guard in apps/handlers/__init__.py that blocks
# cross-branch imports. When this template lives in its target branch, the
# guard passes naturally. When testing from devpulse (or any other branch),
# we pre-inject an empty handlers __init__ module to skip the guard.
if BRANCH_MODULE in ("commons", "skills"):
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_json_pkg = f"{BRANCH_MODULE}.apps.handlers.json"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
else:
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
_json_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers.json"
_json_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.json.json_handler"
# If the handlers package is not yet loaded, inject a stub to avoid the guard.
# The stub needs __path__ set so Python treats it as a package for sub-imports.
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
# Resolve the real filesystem path for the handlers package
if BRANCH_MODULE in ("commons", "skills"):
_handlers_dir = (
Path(__file__).resolve().parents[3] / BRANCH_MODULE / "apps" / "handlers"
)
else:
_handlers_dir = (
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
)
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_mod = importlib.import_module(_json_mod_path)
json_handler = _mod
# ---------------------------------------------------------------------------
# JSON_DIR variable discovery
# ---------------------------------------------------------------------------
# Branches use different names: JSON_DIR, BACKUP_JSON_DIR, PRAX_JSON_DIR,
# BRANCH_JSON_DIR, _JSON_DIR, AI_MAIL_JSON_DIR, etc.
# We find the right one at import time so the isolation fixture can patch it.
_JSON_DIR_ATTR: str | None = None
_JSON_DIR_CANDIDATES = [
f"{BRANCH_MODULE.upper()}_JSON_DIR", # SEEDGO_JSON_DIR, BACKUP_JSON_DIR, etc.
"JSON_DIR", # seedgo, daemon, memory, cli, drone
"BRANCH_JSON_DIR", # commons
f"{BRANCH_MODULE}_json", # unlikely but covered
"_JSON_DIR", # spawn
]
for _candidate in _JSON_DIR_CANDIDATES:
if hasattr(_mod, _candidate):
_JSON_DIR_ATTR = _candidate
break
if _JSON_DIR_ATTR is None:
pytest.skip(
f"Cannot find JSON_DIR attribute on {BRANCH_MODULE}.json_handler — "
f"tried: {_JSON_DIR_CANDIDATES}",
allow_module_level=True,
)
# ---------------------------------------------------------------------------
# Default factory discovery
# ---------------------------------------------------------------------------
# Branches use: _create_default, _get_default_template, _get_default,
# _default_template, load_template, or per-type _default_config/_default_data/_default_log.
def _get_default_for_type(json_type: str, module_name: str = "test_mod") -> Any:
"""Call whichever default factory the branch exposes."""
# Single-function factories (most branches)
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
"load_template",
):
fn = getattr(_mod, fn_name, None)
if fn is not None:
return fn(json_type, module_name)
# Per-type factories (drone pattern)
if json_type == "config" and hasattr(_mod, "_default_config"):
return _mod._default_config(module_name)
if json_type == "data" and hasattr(_mod, "_default_data"):
return _mod._default_data(module_name)
if json_type == "log" and hasattr(_mod, "_default_log"):
return _mod._default_log(module_name)
return None
def _has_default_factory() -> bool:
"""Return True if the branch has any callable default factory."""
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
"load_template",
"_default_config",
):
if hasattr(_mod, fn_name):
return True
return False
def _default_factory_raises_on_unknown() -> bool:
"""Return True if the default factory raises ValueError for unknown types."""
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
):
fn = getattr(_mod, fn_name, None)
if fn is not None:
try:
fn("__nonexistent_type__", "test_mod")
except ValueError:
return True
except Exception:
return False
return False
# load_template reads files — may raise FileNotFoundError, not ValueError
# Per-type factories don't have a single entry point for unknown types
return False
# ---------------------------------------------------------------------------
# Isolation fixture
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def isolate_json_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect JSON operations to tmp_path for test isolation."""
assert _JSON_DIR_ATTR is not None
original_value = getattr(_mod, _JSON_DIR_ATTR)
# Some branches store JSON_DIR as a string (commons), others as Path
if isinstance(original_value, str):
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, str(tmp_path))
else:
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, tmp_path)
return tmp_path
# ---------------------------------------------------------------------------
# Helper: resolve JSON dir as Path regardless of branch type
# ---------------------------------------------------------------------------
def _json_dir_as_path(tmp_path: Path) -> Path:
"""Return the patched JSON dir as a Path (handles str-typed branches)."""
assert _JSON_DIR_ATTR is not None
val = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(val, str):
return Path(val)
return val
# ============================================================================
# Group 1 — _create_default / default templates (4 tests)
# ============================================================================
def test_default_config_returns_dict_with_required_keys() -> None: # JH-001
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("config", "test_mod")
assert isinstance(result, dict), "Config default must be a dict"
assert "module_name" in result, "Config default must have module_name"
assert "version" in result, "Config default must have version"
assert "config" in result, "Config default must have config"
def test_default_data_returns_dict_with_date_keys() -> None: # JH-002
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("data", "test_mod")
assert isinstance(result, dict), "Data default must be a dict"
assert "created" in result, "Data default must have created"
assert "last_updated" in result, "Data default must have last_updated"
def test_default_log_returns_empty_list() -> None: # JH-003
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("log", "test_mod")
assert isinstance(result, list), "Log default must be a list"
assert len(result) == 0, "Log default must be empty"
def test_default_unknown_type_raises_value_error() -> None: # JH-004
if not _default_factory_raises_on_unknown():
pytest.skip("Branch default factory does not raise ValueError for unknown types")
with pytest.raises(ValueError, match="[Uu]nknown"):
_get_default_for_type("__nonexistent__", "test_mod")
# ============================================================================
# Group 2 — validate_json_structure (10 tests)
# ============================================================================
def test_validate_valid_config() -> None: # JH-005
data = {"module_name": "x", "version": "1.0.0", "config": {}}
assert json_handler.validate_json_structure(data, "config") is True
def test_validate_config_missing_key() -> None: # JH-006
data = {"module_name": "x", "version": "1.0.0"} # missing config
assert json_handler.validate_json_structure(data, "config") is False
def test_validate_config_not_dict() -> None: # JH-007
assert json_handler.validate_json_structure([1, 2, 3], "config") is False
def test_validate_valid_data() -> None: # JH-008
data = {"created": "2026-01-01", "last_updated": "2026-01-01"}
assert json_handler.validate_json_structure(data, "data") is True
def test_validate_data_missing_key() -> None: # JH-009
data = {"created": "2026-01-01"} # missing last_updated
assert json_handler.validate_json_structure(data, "data") is False
def test_validate_data_not_dict() -> None: # JH-010
assert json_handler.validate_json_structure("not a dict", "data") is False
def test_validate_valid_log() -> None: # JH-011
assert json_handler.validate_json_structure([], "log") is True
assert json_handler.validate_json_structure([{"entry": 1}], "log") is True
def test_validate_log_not_list() -> None: # JH-012
assert json_handler.validate_json_structure({"not": "a list"}, "log") is False
def test_validate_unknown_type_returns_false() -> None: # JH-013
assert json_handler.validate_json_structure({}, "nonexistent_type") is False
def test_validate_none_input_returns_false() -> None: # JH-014
assert json_handler.validate_json_structure(None, "config") is False
assert json_handler.validate_json_structure(None, "data") is False
assert json_handler.validate_json_structure(None, "log") is False
# ============================================================================
# Group 3 — get_json_path (3 tests)
# ============================================================================
def test_get_json_path_returns_path_type(tmp_path: Path) -> None: # JH-015
result = json_handler.get_json_path("mymod", "config")
# Some branches return str (commons), most return Path
assert isinstance(result, (Path, str)), "get_json_path must return Path or str"
def test_get_json_path_filename_pattern(tmp_path: Path) -> None: # JH-016
result = json_handler.get_json_path("mymod", "config")
name = Path(result).name if isinstance(result, str) else result.name
assert name == "mymod_config.json", f"Expected mymod_config.json, got {name}"
def test_get_json_path_different_combos_differ(tmp_path: Path) -> None: # JH-017
path_a = str(json_handler.get_json_path("alpha", "log"))
path_b = str(json_handler.get_json_path("beta", "data"))
assert path_a != path_b, "Different module/type combos must produce different paths"
# ============================================================================
# Group 4 — ensure_json_exists (5 tests)
# ============================================================================
def test_ensure_creates_file_when_missing(tmp_path: Path) -> None: # JH-018
result = json_handler.ensure_json_exists("ens_mod", "config")
assert result is True
json_dir = _json_dir_as_path(tmp_path)
created = json_dir / "ens_mod_config.json"
assert created.exists(), "ensure_json_exists must create the file"
def test_ensure_preserves_valid_existing_file(tmp_path: Path) -> None: # JH-019
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "keep_data.json"
original = {
"created": "2025-01-01",
"last_updated": "2025-06-01",
"custom_key": "preserve_me",
}
target.write_text(json.dumps(original), encoding="utf-8")
json_handler.ensure_json_exists("keep", "data")
data = json.loads(target.read_text(encoding="utf-8"))
assert data["custom_key"] == "preserve_me", "Valid existing file must not be overwritten"
def test_ensure_regenerates_corrupt_json(tmp_path: Path) -> None: # JH-020
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "bad_log.json"
target.write_bytes(b"\x00\x01NOT VALID JSON{{{")
json_handler.ensure_json_exists("bad", "log")
data = json.loads(target.read_text(encoding="utf-8"))
assert isinstance(data, list), "Corrupt JSON must be regenerated to valid log (list)"
def test_ensure_regenerates_invalid_structure(tmp_path: Path) -> None: # JH-021
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "wrong_config.json"
target.write_text(json.dumps({"wrong": "structure"}), encoding="utf-8")
json_handler.ensure_json_exists("wrong", "config")
data = json.loads(target.read_text(encoding="utf-8"))
assert "module_name" in data, "Invalid structure must be regenerated with correct keys"
assert "version" in data
assert "config" in data
def test_ensure_returns_bool(tmp_path: Path) -> None: # JH-022
result = json_handler.ensure_json_exists("bool_mod", "data")
assert isinstance(result, bool), "ensure_json_exists must return bool"
assert result is True
# ============================================================================
# Group 5 — load_json (4 tests)
# ============================================================================
def test_load_creates_default_when_missing(tmp_path: Path) -> None: # JH-023
result = json_handler.load_json("fresh_mod", "log")
assert result is not None, "load_json must auto-create and return content"
assert isinstance(result, list), "Default log must be a list"
def test_load_returns_existing_content(tmp_path: Path) -> None: # JH-024
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
payload = {"created": "2025-01-01", "last_updated": "2025-06-15", "x": 42}
target = json_dir / "exist_data.json"
target.write_text(json.dumps(payload), encoding="utf-8")
result = json_handler.load_json("exist", "data")
assert isinstance(result, dict)
assert result["x"] == 42, "load_json must return existing file content"
def test_load_returns_dict_for_config(tmp_path: Path) -> None: # JH-025
result = json_handler.load_json("cfg_mod", "config")
assert isinstance(result, dict), "load_json for config must return dict"
def test_load_returns_list_for_log(tmp_path: Path) -> None: # JH-026
result = json_handler.load_json("log_mod", "log")
assert isinstance(result, list), "load_json for log must return list"
# ============================================================================
# Group 6 — save_json (5 tests)
# ============================================================================
def test_save_roundtrip(tmp_path: Path) -> None: # JH-027
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
data = {"module_name": "rt", "version": "1.0.0", "config": {"key": "val"}}
json_handler.save_json("rt", "config", data)
loaded = json_handler.load_json("rt", "config")
assert loaded is not None
assert loaded["config"]["key"] == "val", "Saved data must be readable via load_json"
def test_save_returns_true(tmp_path: Path) -> None: # JH-028
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
data = {"module_name": "sv", "version": "1.0.0", "config": {}}
result = json_handler.save_json("sv", "config", data)
assert result is True, "save_json must return True on success"
def test_save_rejects_invalid_structure(tmp_path: Path) -> None: # JH-029
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
with pytest.raises(ValueError, match="[Ii]nvalid"):
json_handler.save_json("bad", "config", {"missing": "keys"})
def test_save_data_updates_last_updated(tmp_path: Path) -> None: # JH-030
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
today = datetime.now().date().isoformat()
data = {"created": "2025-01-01", "last_updated": "2025-01-01"}
json_handler.save_json("ts", "data", data)
on_disk = json.loads(
(json_dir / "ts_data.json").read_text(encoding="utf-8")
)
assert on_disk["last_updated"] == today, "Saving data type must auto-stamp last_updated"
def test_save_writes_valid_json_to_disk(tmp_path: Path) -> None: # JH-031
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
entries = [{"timestamp": "t1", "operation": "test"}]
json_handler.save_json("disk", "log", entries)
raw = (json_dir / "disk_log.json").read_text(encoding="utf-8")
parsed = json.loads(raw) # must not raise
assert isinstance(parsed, list), "Saved file must be valid JSON on disk"
assert len(parsed) == 1
# ============================================================================
# Group 7 — log_operation (7 tests)
# ============================================================================
def test_log_operation_appends_entry(tmp_path: Path) -> None: # JH-032
json_handler.log_operation("deploy", module_name="logmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "logmod_log.json").read_text(encoding="utf-8")
)
assert len(log) >= 1, "log_operation must append at least one entry"
assert log[-1]["operation"] == "deploy"
def test_log_operation_returns_bool(tmp_path: Path) -> None: # JH-033
result = json_handler.log_operation("test_op", module_name="boolmod")
assert isinstance(result, bool), "log_operation must return bool"
assert result is True
def test_log_operation_entry_has_timestamp(tmp_path: Path) -> None: # JH-034
json_handler.log_operation("check_ts", module_name="tsmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "tsmod_log.json").read_text(encoding="utf-8")
)
assert "timestamp" in log[-1], "Log entry must have a timestamp field"
def test_log_operation_includes_data_when_provided(tmp_path: Path) -> None: # JH-035
json_handler.log_operation(
"with_data", data={"count": 5}, module_name="datamod"
)
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "datamod_log.json").read_text(encoding="utf-8")
)
assert "data" in log[-1], "Log entry must include data dict when provided"
assert log[-1]["data"]["count"] == 5
def test_log_operation_multiple_calls_accumulate(tmp_path: Path) -> None: # JH-039
json_handler.log_operation("first", module_name="accmod")
json_handler.log_operation("second", module_name="accmod")
json_handler.log_operation("third", module_name="accmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "accmod_log.json").read_text(encoding="utf-8")
)
assert len(log) >= 3, "Multiple log_operation calls must accumulate entries"
ops = [e["operation"] for e in log[-3:]]
assert ops == ["first", "second", "third"]
def test_log_operation_fifo_rotation(tmp_path: Path) -> None: # JH-040
# Find the max log entries constant
max_entries = getattr(_mod, "MAX_LOG_ENTRIES", getattr(_mod, "max_log_entries", None))
if max_entries is None:
# Try to find it by checking common names
for attr in ("MAX_LOG_ENTRIES", "max_log_entries", "LOG_MAX_ENTRIES", "_MAX_LOG_ENTRIES"):
max_entries = getattr(_mod, attr, None)
if max_entries is not None:
break
if max_entries is None:
pytest.skip("Cannot find max_log_entries constant on module")
# Fill to max + 5
for i in range(max_entries + 5):
json_handler.log_operation(f"op_{i}", module_name="fifomod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "fifomod_log.json").read_text(encoding="utf-8")
)
assert len(log) <= max_entries, f"Log must not exceed {max_entries} entries after rotation"
# First entries should have been rotated out
assert log[-1]["operation"] == f"op_{max_entries + 4}", "Most recent entry must be last"
def test_log_operation_empty_dict_not_attached(tmp_path: Path) -> None: # JH-041
json_handler.log_operation("no_data", data={}, module_name="emptymod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "emptymod_log.json").read_text(encoding="utf-8")
)
entry = log[-1]
# Empty dict should either not be attached or be an empty dict
# The key test: the entry should not have a non-empty "data" field from an empty input
if "data" in entry:
assert entry["data"] == {} or entry["data"] is None, "Empty dict data should not create non-empty data field"
# ============================================================================
# Group 8 — ensure_module_jsons (5 tests)
# ============================================================================
def test_ensure_module_jsons_creates_all_three(tmp_path: Path) -> None: # JH-036
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("triple")
json_dir = _json_dir_as_path(tmp_path)
assert (json_dir / "triple_config.json").exists(), "Config file must exist"
assert (json_dir / "triple_data.json").exists(), "Data file must exist"
assert (json_dir / "triple_log.json").exists(), "Log file must exist"
def test_ensure_module_jsons_returns_true(tmp_path: Path) -> None: # JH-037
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
result = json_handler.ensure_module_jsons("retmod")
assert result is True, "ensure_module_jsons must return True"
def test_ensure_module_jsons_files_pass_validation(tmp_path: Path) -> None: # JH-038
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("valid_mod")
json_dir = _json_dir_as_path(tmp_path)
config = json.loads(
(json_dir / "valid_mod_config.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(config, "config") is True
data = json.loads(
(json_dir / "valid_mod_data.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(data, "data") is True
log = json.loads(
(json_dir / "valid_mod_log.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(log, "log") is True
def test_ensure_module_jsons_data_has_correct_keys(tmp_path: Path) -> None: # JH-042
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("keymod")
json_dir = _json_dir_as_path(tmp_path)
data = json.loads(
(json_dir / "keymod_data.json").read_text(encoding="utf-8")
)
assert "created" in data, "Data file must have 'created' key"
assert "last_updated" in data, "Data file must have 'last_updated' key"
def test_ensure_module_jsons_log_is_empty_list(tmp_path: Path) -> None: # JH-043
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("listmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "listmod_log.json").read_text(encoding="utf-8")
)
assert isinstance(log, list), "Log file must be a list"
assert len(log) == 0, "Initial log file must be an empty list"
+14
View File
@@ -78,6 +78,15 @@
"standard": "testing",
"reason": "Checker file for the test_coverage standard — not a test file despite 'test' in name"
},
{
"file": "test_quality_check.py",
"standard": "testing",
"reason": "Checker file for the test_quality standard — not a test file despite 'test' in name"
},
{
"file": "tests/test_json_handler.py",
"reason": "Universal JSON handler test template (DPLAN-0059). Canonical source at seedgo/templates/. Test files contain string patterns that trigger code-quality checkers."
},
{
"file": "apps/handlers/audit/audit_display.py",
"standard": "cli",
@@ -158,6 +167,11 @@
"standard": "naming",
"reason": "Checker false positive — these are local function variables, not module-level constants"
},
{
"file": "apps/handlers/aipass_standards/test_quality_check.py",
"standard": "naming",
"reason": "Checker false positive — 'points' is a local variable inside _calculate_score(), not a module-level constant"
},
{
"file": "handlers/aipass_proof/",
"standard": "dead_code",
@@ -0,0 +1,148 @@
# Test Quality Standards
**Status:** v3.0 — 10 standard categories (48 items)
**Date:** 2026-03-24
---
## What It Is
The test quality standard evaluates whether a branch's test files cover 10 standard test categories (48 total items). It scans ALL `test_*.py` files and `conftest.py` in a branch's `tests/` directory. This is a static analysis check; it does not run pytest, only inspects test file contents for detection patterns.
---
## Why It Matters
Standard test categories ensure every branch tests its shared infrastructure (json_handler, CLI routing), error handling, contracts, and fixtures consistently. Coverage breadth across categories means reliable, predictable behavior across the ecosystem.
---
## The 10 Categories
### 1. JSON Handler (8 items)
| Item | Detection Patterns |
|------|-------------------|
| default_factory | `_create_default`, `_get_default_template`, `_get_default`, `_default_template`, `load_template`, `_default_config` |
| validate | `validate_json_structure` |
| get_path | `get_json_path` |
| ensure_exists | `ensure_json_exists` |
| load | `load_json` |
| save | `save_json` |
| log_operation | `log_operation` |
| ensure_module | `ensure_module_jsons` |
### 2. CLI Routing (9 items)
| Item | Detection Patterns |
|------|-------------------|
| help_flag | `--help` |
| short_help | `"-h"`, `'-h'` |
| help_word | `"help"`, `'help'` |
| no_args | `test_no_args`, `test_introspection`, `no_args` |
| unknown_command | `unknown_command`, `invalid_command`, `unrecognized` |
| return_bool | `is True`, `is False` |
| print_help | `print_help` |
| print_introspection | `print_introspection` |
| output_capture | `capsys`, `capfd`, `StringIO` |
### 3. Conftest Fixtures (6 items)
| Item | Detection Patterns |
|------|-------------------|
| temp_dir | `tmp_path`, `temp_test_dir`, `temp_dir` |
| sample_data | `sample_test_data`, `sample_data` |
| mock_infrastructure | `mock_infrastructure`, `autouse` |
| mock_logger | `mock_logger`, `mock_log` |
| mock_json_handler | `mock_json_handler`, `mock_json` |
| cleanup | `rmtree`, `yield`, `teardown` |
### 4. Error Resilience (4 items)
| Item | Detection Patterns |
|------|-------------------|
| missing_file | `FileNotFoundError`, `missing_file`, `file_not_found` |
| corrupt_json | `JSONDecodeError`, `corrupt`, `malformed` |
| empty_file | `empty_file`, `empty_content` |
| nonexistent_dir | `nonexistent`, `missing_dir`, `not_a_dir` |
### 5. Return Type Contracts (4 items)
| Item | Detection Patterns |
|------|-------------------|
| command_returns_bool | `isinstance(result, bool)`, `returns_bool`, `return_type` |
| paths_return_path | `isinstance(result, Path)`, `pathlib.Path` |
| ensure_returns_bool | `ensure_json_exists`, `is True` |
| load_correct_type | `isinstance(result, dict)`, `isinstance(data, dict)` |
### 6. Exception Contracts (3 items)
| Item | Detection Patterns |
|------|-------------------|
| create_default_raises | `pytest.raises(ValueError)`, `ValueError`, `_create_default` |
| save_invalid_raises | `pytest.raises`, `save_json` |
| invalid_mode_raises | `pytest.raises(ValueError)`, `invalid_mode`, `invalid_type` |
### 7. Data Structure Contracts (3 items)
| Item | Detection Patterns |
|------|-------------------|
| config_keys | `module_name`, `config_keys` |
| data_keys | `last_updated`, `data_keys` |
| log_entry_field | `log_entry`, `operation` |
### 8. Success/Failure Paths (4 items)
| Item | Detection Patterns |
|------|-------------------|
| known_routes_true | `assert result is True`, `== True` |
| unknown_returns_false | `assert result is False`, `== False` |
| help_preempts | `--help` |
| no_args_triggers | `print_introspection` |
### 9. Init/Provisioning (4 items)
| Item | Detection Patterns |
|------|-------------------|
| creates_files | `.exists()`, `ensure_json_exists` |
| auto_creates_dir | `mkdir`, `makedirs` |
| no_overwrite | `overwrite`, `no_clobber`, `already_exists` |
| returns_dict | `isinstance(result, dict)`, `json_type` |
### 10. Infrastructure Mocking (3 items)
| Item | Detection Patterns |
|------|-------------------|
| autouse_fixtures | `autouse=True`, `autouse` |
| sys_modules_mock | `sys.modules` |
| reimport_after_mock | `importlib.reload`, `reload(` |
---
## Scoring
Score = (items_covered / 48) x 100
- **Overall pass threshold:** 75% (36+ of 48 items)
- **No test files:** 0%
- **All 48 items covered:** 100%
---
## Reference Templates
| Template | Categories Covered |
|----------|-------------------|
| `seedgo/templates/test_json_handler_template.py` | JSON Handler |
| `seedgo/templates/test_cli_routing_template.py` | CLI Routing, Success/Failure Paths |
| `seedgo/templates/test_conftest_template.py` | Conftest Fixtures, Infrastructure Mocking |
| `seedgo/templates/test_error_resilience_template.py` | Error Resilience |
| `seedgo/templates/test_contracts_template.py` | Return Type, Exception, Data Structure Contracts |
| `seedgo/templates/test_init_provisioning_template.py` | Init/Provisioning |
---
## Bypass
Bypass rules are configured in `.seedgo/bypass.json`. Supports:
- **Standard-level bypass:** Skip the entire `test_quality` standard for a branch
- **File-level bypass:** Match by file path substring
---
## Reference
- **Checker:** `test_quality_check.py`
- **Scope:** `branch_level`
- **Entry point:** `check_branch(branch_path, bypass_rules)`
- **Standard label:** `TEST_QUALITY`
@@ -0,0 +1,404 @@
# =================== AIPass ====================
# Name: test_quality_check.py
# Description: Test Quality Standards Checker — 10 standard categories
# Version: 3.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
Test Quality Standards Checker Handler
Branch-level checker that scans ALL test files in a branch's tests/
directory and evaluates coverage across 10 standard test categories.
Does NOT require specific filenames. Does NOT run pytest -- analyses
test files statically via text scan.
Scoring model:
Score = (total_items_covered / total_items) * 100
"""
from pathlib import Path
from aipass.prax import logger
from aipass.seedgo.apps.handlers.json import json_handler
AUDIT_SCOPE = "branch_level"
# -- Standard test categories and their detection patterns --------------------
STANDARD_CATEGORIES: dict[str, dict[str, list[str]]] = {
# Category 1: JSON Handler (8 items)
"json_handler": {
"default_factory": [
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
"load_template",
"_default_config",
],
"validate": ["validate_json_structure"],
"get_path": ["get_json_path"],
"ensure_exists": ["ensure_json_exists"],
"load": ["load_json"],
"save": ["save_json"],
"log_operation": ["log_operation"],
"ensure_module": ["ensure_module_jsons"],
},
# Category 2: CLI Routing (9 items)
"cli_routing": {
"help_flag": ["--help"],
"short_help": ['"-h"', "'-h'"],
"help_word": ['"help"', "'help'"],
"no_args": ["test_no_args", "test_introspection", "no_args"],
"unknown_command": ["unknown_command", "invalid_command", "unrecognized"],
"return_bool": ["is True", "is False"],
"print_help": ["print_help"],
"print_introspection": ["print_introspection"],
"output_capture": ["capsys", "capfd", "StringIO"],
},
# Category 3: Conftest Fixtures (6 items)
"conftest_fixtures": {
"temp_dir": ["tmp_path", "temp_test_dir", "temp_dir"],
"sample_data": ["sample_test_data", "sample_data"],
"mock_infrastructure": ["mock_infrastructure", "autouse"],
"mock_logger": ["mock_logger", "mock_log"],
"mock_json_handler": ["mock_json_handler", "mock_json"],
"cleanup": ["rmtree", "yield", "teardown"],
},
# Category 4: Error Resilience (4 items)
"error_resilience": {
"missing_file": ["FileNotFoundError", "missing_file", "file_not_found"],
"corrupt_json": ["JSONDecodeError", "corrupt", "malformed"],
"empty_file": ["empty_file", "empty_content"],
"nonexistent_dir": ["nonexistent", "missing_dir", "not_a_dir"],
},
# Category 5: Return Type Contracts (4 items)
"return_type_contracts": {
"command_returns_bool": [
"isinstance(result, bool)",
"returns_bool",
"return_type",
],
"paths_return_path": ["isinstance(result, Path)", "pathlib.Path"],
"ensure_returns_bool": ["ensure_json_exists", "is True"],
"load_correct_type": ["isinstance(result, dict)", "isinstance(data, dict)"],
},
# Category 6: Exception Contracts (3 items)
"exception_contracts": {
"create_default_raises": [
"pytest.raises(ValueError)",
"ValueError",
"_create_default",
],
"save_invalid_raises": ["pytest.raises", "save_json"],
"invalid_mode_raises": [
"pytest.raises(ValueError)",
"invalid_mode",
"invalid_type",
],
},
# Category 7: Data Structure Contracts (3 items)
"data_structure_contracts": {
"config_keys": ["module_name", "config_keys"],
"data_keys": ["last_updated", "data_keys"],
"log_entry_field": ["log_entry", "operation"],
},
# Category 8: Success/Failure Paths (4 items)
"success_failure_paths": {
"known_routes_true": ["assert result is True", "== True"],
"unknown_returns_false": ["assert result is False", "== False"],
"help_preempts": ["--help"],
"no_args_triggers": ["print_introspection"],
},
# Category 9: Init/Provisioning (4 items)
"init_provisioning": {
"creates_files": [".exists()", "ensure_json_exists"],
"auto_creates_dir": ["mkdir", "makedirs"],
"no_overwrite": ["overwrite", "no_clobber", "already_exists"],
"returns_dict": ["isinstance(result, dict)", "json_type"],
},
# Category 10: Infrastructure Mocking (3 items)
"infrastructure_mocking": {
"autouse_fixtures": ["autouse=True", "autouse"],
"sys_modules_mock": ["sys.modules"],
"reimport_after_mock": ["importlib.reload", "reload("],
},
}
TOTAL_ITEMS = sum(
len(items) for items in STANDARD_CATEGORIES.values()
)
# =============================================
# BYPASS HELPER
# =============================================
def is_bypassed(
file_path: str,
standard: str,
line: int | None = None,
bypass_rules: list | None = None,
) -> bool:
"""Check if a violation should be bypassed."""
if not bypass_rules:
return False
for rule in bypass_rules:
if rule.get("standard") and rule.get("standard") != standard:
continue
rule_file = rule.get("file", "")
if rule_file and rule_file not in file_path:
continue
rule_lines = rule.get("lines", [])
if rule_lines and line is not None and line not in rule_lines:
continue
return True
return False
# =============================================
# FILE HELPERS
# =============================================
def _read_file_safe(path: Path) -> str:
"""Read a file, returning empty string on any error."""
try:
return path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
logger.info("Cannot read %s for test quality analysis", path)
return ""
def _find_all_test_files(branch_path: Path) -> list[Path]:
"""Find all test files and conftest.py in the branch's tests/ directory.
Scans for any test_*.py file plus conftest.py -- no naming requirements.
"""
tests_dir = branch_path / "tests"
if not tests_dir.is_dir():
return []
results: list[Path] = []
for p in sorted(tests_dir.iterdir()):
if not p.is_file() or p.suffix != ".py":
continue
if p.name.startswith("test_") or p.name == "conftest.py":
results.append(p)
return results
# =============================================
# ANALYSIS
# =============================================
def _detect_all_coverage(
file_sources: list[tuple[str, str]],
) -> dict[str, dict[str, str | None]]:
"""Scan test file sources for coverage across all standard categories.
For each category, for each item, checks if ANY pattern matches in ANY
source file. Returns the first file that covers each item.
Args:
file_sources: List of (filename, source_text) tuples.
Returns:
dict mapping category -> {item -> covering_filename or None}
"""
coverage: dict[str, dict[str, str | None]] = {}
for category, items in STANDARD_CATEGORIES.items():
coverage[category] = {}
for item_name, patterns in items.items():
covering_file: str | None = None
for filename, source in file_sources:
for pattern in patterns:
if pattern in source:
covering_file = filename
break
if covering_file is not None:
break
coverage[category][item_name] = covering_file
return coverage
# =============================================
# BRANCH-LEVEL CHECK
# =============================================
def check_branch(branch_path: str, bypass_rules: list | None = None) -> dict:
"""Run test quality analysis on a branch.
Scans all test_*.py and conftest.py files in tests/ and evaluates
coverage across 10 standard test categories.
Score = total items covered / total items.
Args:
branch_path: Path to branch root directory.
bypass_rules: Optional list of bypass rules from .seedgo/bypass.json.
Returns:
dict: {passed, score, checks, standard: 'TEST_QUALITY'}
"""
checks: list[dict] = []
bp = Path(branch_path)
# Check if entire standard is bypassed
if is_bypassed(branch_path, "test_quality", bypass_rules=bypass_rules):
return {
"passed": True,
"checks": [
{
"name": "Bypassed",
"passed": True,
"message": "Standard bypassed via .seedgo/bypass.json",
}
],
"score": 100,
"standard": "TEST_QUALITY",
}
# Validate branch path exists
if not bp.is_dir():
return {
"passed": False,
"checks": [
{
"name": "Branch exists",
"passed": False,
"message": f"Branch directory not found: {branch_path}",
}
],
"score": 0,
"standard": "TEST_QUALITY",
}
# Phase 1: Find all test files
test_files = _find_all_test_files(bp)
if not test_files:
checks.append({
"name": "Test files",
"passed": False,
"message": "No test_*.py or conftest.py files found in tests/ directory",
})
json_handler.log_operation(
"check_completed",
{
"branch": branch_path,
"score": 0,
"standard": "test_quality",
"test_files": 0,
"items_covered": 0,
},
)
return {
"passed": False,
"score": 0,
"checks": checks,
"standard": "TEST_QUALITY",
}
checks.append({
"name": "Test files",
"passed": True,
"message": f"Found {len(test_files)} test file(s) in tests/",
})
# Phase 2: Read all test file sources
file_sources: list[tuple[str, str]] = []
for tf in test_files:
source = _read_file_safe(tf)
if source:
file_sources.append((tf.name, source))
# Phase 3: Detect coverage across all categories
all_coverage = _detect_all_coverage(file_sources)
total_items_covered = 0
# Per-category summary checks
for category, item_coverage in all_coverage.items():
cat_total = len(item_coverage)
cat_covered = sum(1 for f in item_coverage.values() if f is not None)
total_items_covered += cat_covered
missing_items = [
item for item, f in item_coverage.items() if f is None
]
if cat_covered == cat_total:
checks.append({
"name": category,
"passed": True,
"message": f"{category}: {cat_covered}/{cat_total} covered",
})
else:
checks.append({
"name": category,
"passed": False,
"message": (
f"{category}: {cat_covered}/{cat_total} covered "
f"(missing: {', '.join(missing_items)})"
),
})
# Score = coverage percentage
score = int((total_items_covered / TOTAL_ITEMS) * 100)
# Overall pass at 75%
overall_passed = score >= 75
# Overall summary check
if overall_passed:
checks.append({
"name": "Overall coverage",
"passed": True,
"message": (
f"{total_items_covered}/{TOTAL_ITEMS} items covered "
f"across {len(STANDARD_CATEGORIES)} categories ({score}%)"
),
})
else:
checks.append({
"name": "Overall coverage",
"passed": False,
"message": (
f"{total_items_covered}/{TOTAL_ITEMS} items covered "
f"across {len(STANDARD_CATEGORIES)} categories ({score}%) "
f"-- minimum 75% required"
),
})
json_handler.log_operation(
"check_completed",
{
"branch": branch_path,
"score": score,
"standard": "test_quality",
"test_files": len(test_files),
"items_covered": total_items_covered,
"items_total": TOTAL_ITEMS,
"category_detail": {
cat: {
"covered": sum(
1 for f in items.values() if f is not None
),
"total": len(items),
}
for cat, items in all_coverage.items()
},
},
)
return {
"passed": overall_passed,
"score": score,
"checks": checks,
"standard": "TEST_QUALITY",
}
@@ -0,0 +1,132 @@
# =================== AIPass ====================
# Name: test_quality_content.py
# Description: Test Quality Standards Content Handler
# Version: 3.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
Test Quality Standards Content Handler
Provides formatted Test Quality standards content.
Module orchestrates, handler implements.
"""
from aipass.seedgo.apps.handlers.json import json_handler
def get_test_quality_standards() -> str:
"""Return formatted test_quality standards content with Rich markup
Returns:
str: Formatted standards text with Rich styling
"""
lines = [
"[bold cyan]CORE PRINCIPLE:[/bold cyan]",
" Every branch should have tests covering 10 standard categories.",
" The checker scans ALL test files in tests/ (including conftest.py) —",
" no specific filenames required. Quality = coverage breadth.",
"",
"[bold cyan]WHAT IT CHECKS:[/bold cyan]",
" Branch-level static analysis (does NOT run pytest):",
"",
" [yellow]1. Test file discovery:[/yellow]",
" Scans [dim]tests/[/dim] for ALL [dim]test_*.py[/dim] files + [dim]conftest.py[/dim]",
" No naming requirements — any test file counts",
"",
" [yellow]2. Category coverage:[/yellow]",
" For each of 10 categories, checks if test files reference",
" the expected patterns. Reports per-category: X/N covered",
"",
"[bold cyan]THE 10 CATEGORIES (48 items total):[/bold cyan]",
"",
" [bold]1. JSON Handler (8 items)[/bold]",
" [dim]default_factory, validate, get_path, ensure_exists,[/dim]",
" [dim]load, save, log_operation, ensure_module[/dim]",
" Template: seedgo/templates/test_json_handler_template.py",
"",
" [bold]2. CLI Routing (9 items)[/bold]",
" [dim]help_flag (--help), short_help (-h), help_word,[/dim]",
" [dim]no_args, unknown_command, return_bool,[/dim]",
" [dim]print_help, print_introspection, output_capture[/dim]",
" Template: seedgo/templates/test_cli_routing_template.py",
"",
" [bold]3. Conftest Fixtures (6 items)[/bold]",
" [dim]temp_dir, sample_data, mock_infrastructure,[/dim]",
" [dim]mock_logger, mock_json_handler, cleanup[/dim]",
" Template: seedgo/templates/test_conftest_template.py",
"",
" [bold]4. Error Resilience (4 items)[/bold]",
" [dim]missing_file, corrupt_json, empty_file, nonexistent_dir[/dim]",
" Template: seedgo/templates/test_error_resilience_template.py",
"",
" [bold]5. Return Type Contracts (4 items)[/bold]",
" [dim]command_returns_bool, paths_return_path,[/dim]",
" [dim]ensure_returns_bool, load_correct_type[/dim]",
" Template: seedgo/templates/test_contracts_template.py",
"",
" [bold]6. Exception Contracts (3 items)[/bold]",
" [dim]create_default_raises, save_invalid_raises,[/dim]",
" [dim]invalid_mode_raises[/dim]",
" Template: seedgo/templates/test_contracts_template.py",
"",
" [bold]7. Data Structure Contracts (3 items)[/bold]",
" [dim]config_keys, data_keys, log_entry_field[/dim]",
" Template: seedgo/templates/test_contracts_template.py",
"",
" [bold]8. Success/Failure Paths (4 items)[/bold]",
" [dim]known_routes_true, unknown_returns_false,[/dim]",
" [dim]help_preempts, no_args_triggers[/dim]",
" Template: seedgo/templates/test_cli_routing_template.py",
"",
" [bold]9. Init/Provisioning (4 items)[/bold]",
" [dim]creates_files, auto_creates_dir,[/dim]",
" [dim]no_overwrite, returns_dict[/dim]",
" Template: seedgo/templates/test_init_provisioning_template.py",
"",
" [bold]10. Infrastructure Mocking (3 items)[/bold]",
" [dim]autouse_fixtures, sys_modules_mock, reimport_after_mock[/dim]",
" Template: seedgo/templates/test_conftest_template.py",
"",
"[bold cyan]SCORING MODEL:[/bold cyan]",
"",
" Score = (items_covered / 48) * 100",
"",
" Overall pass threshold: [yellow]75%[/yellow] (36+ of 48 items)",
"",
" [dim]No test files = 0%. All 48 items covered = 100%.[/dim]",
"",
"[bold cyan]EXAMPLE OUTPUT:[/bold cyan]",
"",
" [dim]json_handler: 8/8 covered[/dim]",
" [dim]cli_routing: 7/9 covered (missing: help_word, output_capture)[/dim]",
" [dim]conftest_fixtures: 4/6 covered (missing: mock_infrastructure, mock_logger)[/dim]",
" [dim]error_resilience: 0/4 covered (...)[/dim]",
" [dim]Overall: 23/48 items covered across 10 categories (47%)[/dim]",
"",
"[bold cyan]HOW TO COMPLY:[/bold cyan]",
"",
" Write tests that cover the 10 categories above.",
" Tests can be in any test_*.py file in your tests/ directory.",
" Conftest fixtures go in tests/conftest.py.",
"",
" Reference templates are available at:",
" [dim]seedgo/templates/test_json_handler_template.py[/dim]",
" [dim]seedgo/templates/test_cli_routing_template.py[/dim]",
" [dim]seedgo/templates/test_conftest_template.py[/dim]",
" [dim]seedgo/templates/test_error_resilience_template.py[/dim]",
" [dim]seedgo/templates/test_contracts_template.py[/dim]",
" [dim]seedgo/templates/test_init_provisioning_template.py[/dim]",
"",
"[yellow]SCOPE:[/yellow]",
" AUDIT_SCOPE = [bold]branch_level[/bold]",
" Runs once per branch (not per file). Entry point: [dim]check_branch()[/dim]",
"",
"[bold cyan]BYPASS:[/bold cyan]",
" Via [dim].seedgo/bypass.json[/dim] — supports standard-level and",
" file-level bypass rules",
]
json_handler.log_operation("standard_content_queried", {"standard": "test_quality"})
return "\n".join(lines)
@@ -157,6 +157,20 @@ def _render_type_errors(audit_result: dict, console_obj) -> None:
console_obj.print(f" [green]✓[/green] No type errors")
def _render_test_map(audit_result: dict, console_obj) -> None:
"""Show custom function test coverage summary (informational, not scored)."""
test_map = audit_result.get('test_map')
if not test_map:
return
total = test_map.get('total_functions', 0)
if total == 0:
return
tested = test_map.get('tested_functions', 0)
branch_name = test_map.get('branch', '')
console_obj.print(f" [dim]Custom Test Opportunities: {total} public functions, {tested} tested."
f" Run: drone @seedgo test_map @{branch_name}[/dim]")
def _render_deprecated_patterns(audit_result: dict, console_obj) -> None:
"""Special renderer for deprecated patterns — different structure."""
deprecated_patterns = audit_result.get('deprecated_patterns', [])
@@ -228,10 +242,8 @@ def print_branch_summary(audit_result: Dict, system_averages: Dict[str, int] | N
if failed_checks:
formatted = _format_standard_name(standard_name)
console.print(f" [red]└─ {formatted} issues:[/red]")
for check in failed_checks[:5]:
for check in failed_checks:
console.print(f" [dim]• {check.get('message', '')}[/dim]")
if len(failed_checks) > 5:
console.print(f" [dim]... and {len(failed_checks) - 5} more[/dim]")
rendered_standards.add(standard_name)
# Catch any violation lists not represented in scores (defensive)
@@ -248,6 +260,9 @@ def print_branch_summary(audit_result: Dict, system_averages: Dict[str, int] | N
# Type errors (separate from standards)
_render_type_errors(audit_result, console)
# Custom function test coverage (informational)
_render_test_map(audit_result, console)
# Deprecated patterns
_render_deprecated_patterns(audit_result, console)
@@ -13,6 +13,7 @@ from typing import Any, Dict, List
from aipass.prax import logger
from aipass.seedgo.apps.handlers.bypass import ignore_handler
from aipass.seedgo.apps.handlers.json import json_handler
from aipass.seedgo.apps.handlers.test_map.function_scanner import scan_branch
def discover_checkers(pack_path: Path | None = None) -> Dict[str, Any]:
"""Auto-discover all *_check.py modules from a pack directory.
@@ -155,10 +156,17 @@ def audit_branch(branch: Dict[str, str], bypass_rules: list, pack_path: Path | N
deprecated.append({"type": "directory", "old": "DOCUMENTS/", "new": "docs/",
"path": str(branch_path / "DOCUMENTS"), "message": "Rename DOCUMENTS/ to docs/"})
# Custom function coverage scan (informational, not scored)
try:
test_map_result = scan_branch(str(branch_path))
except Exception:
test_map_result = None
diag_result = results.get("diagnostics", {})
output = {"branch": branch, "results": results, "scores": scores, "average": avg,
"deprecated_patterns": deprecated, "files_checked": len(all_files),
"type_errors": diag_result.get("total_errors", 0), "type_error_files": diag_result.get("results", [])}
"type_errors": diag_result.get("total_errors", 0), "type_error_files": diag_result.get("results", []),
"test_map": test_map_result}
for name in checkers:
output[f"{name}_violations"] = all_violations.get(name, [])
return output
@@ -0,0 +1,257 @@
# =================== AIPass ====================
# Name: function_scanner.py
# Description: AST-based public function scanner for branch test coverage mapping
# Version: 1.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
AST-based public function scanner.
Scans a branch's apps/modules/ and apps/handlers/ for public function
definitions, cross-references against tests/, and builds a coverage map.
Excludes standard infrastructure functions (json_handler, CLI routing).
"""
import ast
from pathlib import Path
from aipass.prax import logger
# -- Standard functions to exclude (already covered by test_quality checker) --
# CLI routing — every branch has these, not custom logic
CLI_ROUTING_FUNCTIONS = frozenset({
"handle_command",
"print_introspection",
"print_help",
"main",
})
# json_handler standard functions — covered by test_quality checker
JSON_HANDLER_FUNCTIONS = frozenset({
"validate_json_structure",
"get_json_path",
"ensure_json_exists",
"load_json",
"save_json",
"log_operation",
"ensure_module_jsons",
"load_template",
})
# Bypass helper present in many checkers
CHECKER_BOILERPLATE = frozenset({
"is_bypassed",
})
EXCLUDED_FUNCTIONS = CLI_ROUTING_FUNCTIONS | JSON_HANDLER_FUNCTIONS | CHECKER_BOILERPLATE
# =============================================================================
# AST SCANNING
# =============================================================================
def _read_file_safe(path: Path) -> str:
"""Read file contents, returning empty string on error."""
try:
return path.read_text(encoding="utf-8")
except (OSError, UnicodeDecodeError):
return ""
def _extract_public_functions(file_path: Path) -> list[dict]:
"""Extract public function definitions from a Python file via AST.
Returns list of dicts: {name, line, file}
Only top-level and class-level defs. Skips _private and excluded names.
"""
source = _read_file_safe(file_path)
if not source:
return []
try:
tree = ast.parse(source, filename=str(file_path))
except SyntaxError:
logger.info("Syntax error parsing %s — skipped", file_path)
return []
functions = []
for node in ast.walk(tree):
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
continue
name = node.name
# Skip private, dunder, and excluded standard functions
if name.startswith("_"):
continue
if name in EXCLUDED_FUNCTIONS:
continue
functions.append({
"name": name,
"line": node.lineno,
"file": str(file_path),
})
return functions
def _scan_source_files(branch_path: Path) -> list[dict]:
"""Scan apps/modules/ and apps/handlers/ for public functions.
Returns list of dicts: {name, line, file, relative_path}
"""
all_functions = []
apps_dir = branch_path / "apps"
scan_dirs = [
apps_dir / "modules",
apps_dir / "handlers",
]
for scan_dir in scan_dirs:
if not scan_dir.is_dir():
continue
for py_file in sorted(scan_dir.rglob("*.py")):
if py_file.name.startswith("_"):
continue
# Skip .archive, .sorting_unprocessed
if any(part.startswith(".") for part in py_file.parts):
continue
funcs = _extract_public_functions(py_file)
for func in funcs:
# Build relative path from branch root
try:
rel = py_file.relative_to(branch_path)
except ValueError:
rel = py_file
func["relative_path"] = str(rel)
all_functions.extend(funcs)
return all_functions
def _scan_test_references(branch_path: Path) -> set[str]:
"""Scan all test files for function name references.
Returns set of function names found in any test file.
Uses text matching — if the function name appears anywhere in the test
source, it counts as referenced.
"""
tests_dir = branch_path / "tests"
if not tests_dir.is_dir():
return set()
referenced = set()
for test_file in sorted(tests_dir.rglob("test_*.py")):
source = _read_file_safe(test_file)
if not source:
continue
# Collect all referenced names — simple text match
for line in source.splitlines():
stripped = line.strip()
if not stripped or stripped.startswith("#"):
continue
referenced.add(line) # Store full lines for matching
# Return just the raw source blob for matching
return referenced
def _test_files_source(branch_path: Path) -> str:
"""Concatenate all test file sources for matching."""
tests_dir = branch_path / "tests"
if not tests_dir.is_dir():
return ""
sources = []
for test_file in sorted(tests_dir.rglob("test_*.py")):
source = _read_file_safe(test_file)
if source:
sources.append(source)
return "\n".join(sources)
# =============================================================================
# PUBLIC API
# =============================================================================
def scan_branch(branch_path: str) -> dict:
"""Scan a branch and build its custom function coverage map.
Args:
branch_path: Absolute path to branch root directory.
Returns:
dict with keys:
branch: branch name
total_functions: count of public custom functions
tested_functions: count of functions referenced in tests
coverage_pct: percentage
files: list of {relative_path, functions: [{name, line, tested, test_file}]}
"""
bp = Path(branch_path)
branch_name = bp.name
# Scan source for public functions
all_functions = _scan_source_files(bp)
# Get concatenated test source for matching
test_source = _test_files_source(bp)
# Also build a set of test file names for reporting which test covers
tests_dir = bp / "tests"
test_files_map: dict[str, str] = {} # function_name -> test_file_name
if tests_dir.is_dir():
for test_file in sorted(tests_dir.rglob("test_*.py")):
source = _read_file_safe(test_file)
if not source:
continue
for func in all_functions:
if func["name"] in source and func["name"] not in test_files_map:
test_files_map[func["name"]] = test_file.name
# Build per-file coverage map
files_map: dict[str, list[dict]] = {}
tested_count = 0
for func in all_functions:
rel_path = func["relative_path"]
is_tested = func["name"] in test_source
if is_tested:
tested_count += 1
if rel_path not in files_map:
files_map[rel_path] = []
files_map[rel_path].append({
"name": func["name"],
"line": func["line"],
"tested": is_tested,
"test_file": test_files_map.get(func["name"]),
})
total = len(all_functions)
pct = int((tested_count / total) * 100) if total > 0 else 0
logger.info(
"test_map scan: %s — %d/%d custom functions tested (%d%%)",
branch_name, tested_count, total, pct,
)
return {
"branch": branch_name,
"total_functions": total,
"tested_functions": tested_count,
"coverage_pct": pct,
"files": [
{
"relative_path": rel_path,
"functions": funcs,
}
for rel_path, funcs in sorted(files_map.items())
],
}
+216
View File
@@ -0,0 +1,216 @@
# =================== AIPass ====================
# Name: test_map.py
# Description: Custom Function Test Coverage Map Module
# Version: 1.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
Custom Function Test Coverage Map Module
AST-scans a branch's apps/modules/ and apps/handlers/ for public functions,
cross-references against tests/, and outputs a coverage map showing which
custom functions have tests and which don't.
Run: drone @seedgo test_map @branch
"""
from typing import List
# =============================================================================
# INFRASTRUCTURE SETUP
# =============================================================================
# IMPORTS
# =============================================================================
# Prax logger (system-wide, always first)
from aipass.prax import logger
# CLI services (display/output formatting)
from aipass.cli import console, header
from aipass.cli.apps.modules import error
# JSON handler for tracking
from aipass.seedgo.apps.handlers.json import json_handler
# Handler (implementation)
from aipass.seedgo.apps.handlers.test_map.function_scanner import scan_branch
# Drone services for @ resolution
from aipass.drone.apps.modules import normalize_branch_arg
# Branch discovery
from aipass.seedgo.apps.handlers.audit.discovery import discover_branches
# =============================================================================
# COMMAND HANDLER
# =============================================================================
def print_introspection() -> None:
"""Display module info and connected handlers."""
console.print()
console.print("[bold cyan]test_map Module[/bold cyan]")
console.print("Custom function test coverage mapping")
console.print()
console.print("[yellow]Connected Handlers:[/yellow]")
console.print(" [dim]- test_map/function_scanner.py (AST-based public function scanner)[/dim]")
console.print()
console.print("[yellow]What It Does:[/yellow]")
console.print(" [dim]Scans apps/modules/ and apps/handlers/ for public functions,[/dim]")
console.print(" [dim]cross-references against tests/, shows what has tests and what doesn't.[/dim]")
console.print(" [dim]Excludes standard infrastructure (json_handler, CLI routing).[/dim]")
console.print()
console.print("[yellow]Next:[/yellow]")
console.print(" [green]drone @seedgo test_map @flow[/green] [dim]# Scan a branch[/dim]")
console.print(" [green]drone @seedgo test_map --help[/green] [dim]# Full usage guide[/dim]")
console.print()
def handle_command(command: str, args: List[str]) -> bool:
"""Route test_map command."""
if command not in ("test_map",):
return False
if not args:
print_introspection()
return True
if args[0] in ("--help", "-h", "help"):
print_help()
return True
# Expect @branch argument
branch_arg = args[0]
if not branch_arg.startswith("@"):
error(
f"Branch name must use @ prefix: '@{branch_arg}'",
suggestion=f"Usage: drone @seedgo test_map @{branch_arg}",
)
return True
branch_name = normalize_branch_arg(branch_arg)
# Resolve branch path
branches = discover_branches(include_private=True)
branch_entry = None
for b in branches:
if b["name"].lower() == branch_name:
branch_entry = b
break
if not branch_entry:
error(
f"Branch not found: @{branch_name}",
suggestion="Run: drone systems",
)
return True
branch_path = branch_entry["path"]
# Run the scan
result = scan_branch(branch_path)
# Display results
_display_coverage_map(result)
json_handler.log_operation(
"test_map_scan",
{
"branch": branch_name,
"total": result["total_functions"],
"tested": result["tested_functions"],
"pct": result["coverage_pct"],
},
)
return True
# =============================================================================
# DISPLAY
# =============================================================================
def _display_coverage_map(result: dict) -> None:
"""Render the coverage map with Rich formatting."""
branch = result["branch"]
total = result["total_functions"]
tested = result["tested_functions"]
pct = result["coverage_pct"]
console.print()
header(f"@{branch} CUSTOM FUNCTION COVERAGE")
console.print()
if total == 0:
console.print("[dim]No custom public functions found in apps/modules/ or apps/handlers/[/dim]")
console.print()
return
for file_entry in result["files"]:
rel_path = file_entry["relative_path"]
funcs = file_entry["functions"]
console.print(f"[cyan]{rel_path}:[/cyan]")
for func in funcs:
if func["tested"]:
test_note = f" — tested in {func['test_file']}" if func["test_file"] else " — tested"
console.print(f" [green]✓[/green] {func['name']}(){test_note}")
else:
console.print(f" [red]✗[/red] {func['name']}() — [dim]NOT TESTED[/dim]")
console.print()
# Summary
style = "green" if pct >= 50 else "yellow" if pct >= 25 else "red"
console.print(f"[bold]Summary:[/bold] [{style}]{tested}/{total}[/{style}] custom functions tested ({pct}%)")
console.print()
def print_help() -> None:
"""Full usage guide."""
console.print()
header("TEST MAP — USAGE")
console.print()
console.print("[yellow]Description:[/yellow]")
console.print(" AST-scans a branch for custom public functions and maps them")
console.print(" against existing tests. Shows test coverage opportunities.")
console.print()
console.print("[yellow]Commands:[/yellow]")
console.print(" [green]drone @seedgo test_map[/green] [dim]Module info[/dim]")
console.print(" [green]drone @seedgo test_map @branch[/green] [dim]Scan a branch[/dim]")
console.print(" [green]drone @seedgo test_map --help[/green] [dim]This help[/dim]")
console.print()
console.print("[yellow]What It Scans:[/yellow]")
console.print(" [dim]• apps/modules/*.py — module-level public functions[/dim]")
console.print(" [dim]• apps/handlers/**/*.py — handler-level public functions[/dim]")
console.print()
console.print("[yellow]What It Excludes:[/yellow]")
console.print(" [dim]• Private functions (_name)[/dim]")
console.print(" [dim]• CLI routing (handle_command, print_introspection, print_help, main)[/dim]")
console.print(" [dim]• json_handler standard functions (covered by test_quality checker)[/dim]")
console.print()
console.print("[yellow]Examples:[/yellow]")
console.print(" [green]drone @seedgo test_map @flow[/green]")
console.print(" [green]drone @seedgo test_map @api[/green]")
console.print()
# =============================================================================
# STANDALONE EXECUTION
# =============================================================================
if __name__ == "__main__":
logger.info("[TEST_MAP] Module loaded directly")
json_handler.log_operation("module_loaded", {"module": "test_map"})
print_introspection()
@@ -0,0 +1,203 @@
# =================== AIPass ====================
# Name: test_cli_routing_template.py
# Description: Universal CLI Routing Test Template
# Version: 1.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
Universal CLI Routing Test Template
Copy this file to any AIPass branch's tests/ directory.
Change BRANCH_MODULE below. Run with pytest.
Covers 9 tests across 3 groups:
- handle_command routing (6)
- print_help / print_introspection output (2)
- help preemption (1)
Every AIPass branch exposes a CLI entry point via handle_command().
These tests verify the routing contract: help flags, no-args fallback,
unknown command rejection, and boolean return types.
"""
import importlib
import sys
import types
from pathlib import Path
import pytest
# ============ BRANCH CONFIG ============
# Change these two lines when deploying to a branch:
BRANCH_MODULE = "seedgo" # e.g. "prax", "drone", "backup", "cli", etc.
# For commons: "commons" (import path is different: aipass -> just commons)
# For skills: "skills" (import path is different: aipass -> just skills)
# =======================================
# ---------------------------------------------------------------------------
# Dynamic import with cross-branch guard bypass
# ---------------------------------------------------------------------------
# Every branch has an import guard in apps/handlers/__init__.py that blocks
# cross-branch imports. When this template lives in its target branch, the
# guard passes naturally. When testing from devpulse (or any other branch),
# we pre-inject an empty handlers __init__ module to skip the guard.
if BRANCH_MODULE in ("commons", "skills"):
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_cli_mod_path = f"{BRANCH_MODULE}.apps.handlers.cli.cli_handler"
else:
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
_cli_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.cli.cli_handler"
# If the handlers package is not yet loaded, inject a stub to avoid the guard.
# The stub needs __path__ set so Python treats it as a package for sub-imports.
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
if BRANCH_MODULE in ("commons", "skills"):
_handlers_dir = (
Path(__file__).resolve().parents[3] / BRANCH_MODULE / "apps" / "handlers"
)
else:
_handlers_dir = (
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
)
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_mod = importlib.import_module(_cli_mod_path)
cli_handler = _mod
# ---------------------------------------------------------------------------
# Function discovery helpers
# ---------------------------------------------------------------------------
def _find_handle_command():
"""Locate the handle_command function on the CLI handler module."""
for name in ("handle_command", "handle_cli_command", "route_command"):
fn = getattr(_mod, name, None)
if callable(fn):
return fn
return None
def _find_print_help():
"""Locate the help printer function."""
for name in ("print_help", "show_help", "display_help"):
fn = getattr(_mod, name, None)
if callable(fn):
return fn
return None
def _find_print_introspection():
"""Locate the introspection printer function."""
for name in ("print_introspection", "show_introspection", "display_introspection"):
fn = getattr(_mod, name, None)
if callable(fn):
return fn
return None
# ============================================================================
# Group 1 -- handle_command routing (6 tests)
# ============================================================================
def test_handle_command_help_flag() -> None: # CR-001
"""handle_command with --help flag returns True."""
handle = _find_handle_command()
if handle is None:
pytest.skip("Branch does not expose handle_command")
result = handle("cmd", ["--help"])
assert result is True, "handle_command('cmd', ['--help']) must return True"
def test_handle_command_short_help() -> None: # CR-002
"""handle_command with -h flag returns True."""
handle = _find_handle_command()
if handle is None:
pytest.skip("Branch does not expose handle_command")
result = handle("cmd", ["-h"])
assert result is True, "handle_command('cmd', ['-h']) must return True"
def test_handle_command_help_word() -> None: # CR-003
"""handle_command('help', []) returns True."""
handle = _find_handle_command()
if handle is None:
pytest.skip("Branch does not expose handle_command")
result = handle("help", [])
assert result is True, "handle_command('help', []) must return True"
def test_handle_command_no_args() -> None: # CR-004
"""handle_command with no args triggers introspection fallback, returns True."""
handle = _find_handle_command()
if handle is None:
pytest.skip("Branch does not expose handle_command")
result = handle("cmd", [])
assert result is True, "handle_command('cmd', []) must trigger fallback and return True"
def test_handle_command_unknown() -> None: # CR-005
"""handle_command with unknown/bogus command returns False."""
handle = _find_handle_command()
if handle is None:
pytest.skip("Branch does not expose handle_command")
result = handle("bogus", [])
assert result is False, "handle_command('bogus', []) must return False"
def test_handle_command_return_bool() -> None: # CR-006
"""handle_command always returns a bool (True or False)."""
handle = _find_handle_command()
if handle is None:
pytest.skip("Branch does not expose handle_command")
result_true = handle("help", [])
result_false = handle("bogus", [])
assert isinstance(result_true, bool), "handle_command must return bool, not truthy"
assert isinstance(result_false, bool), "handle_command must return bool, not falsy"
assert result_true is True
assert result_false is False
# ============================================================================
# Group 2 -- print_help / print_introspection output (2 tests)
# ============================================================================
def test_print_help(capsys: pytest.CaptureFixture[str]) -> None: # CR-007
"""print_help() runs without error and produces stdout output."""
print_help = _find_print_help()
if print_help is None:
pytest.skip("Branch does not expose print_help")
print_help()
captured = capsys.readouterr()
assert len(captured.out) > 0, "print_help() must produce output"
def test_print_introspection(capsys: pytest.CaptureFixture[str]) -> None: # CR-008
"""print_introspection() runs without error and produces stdout output."""
print_intro = _find_print_introspection()
if print_intro is None:
pytest.skip("Branch does not expose print_introspection")
print_intro()
captured = capsys.readouterr()
assert len(captured.out) > 0, "print_introspection() must produce output"
# ============================================================================
# Group 3 -- help preemption (1 test)
# ============================================================================
def test_help_preempts_execution(capsys: pytest.CaptureFixture[str]) -> None: # CR-009
"""--help flag causes help text output, preempting normal execution."""
handle = _find_handle_command()
if handle is None:
pytest.skip("Branch does not expose handle_command")
result = handle("cmd", ["--help"])
captured = capsys.readouterr()
assert result is True, "--help must return True"
assert len(captured.out) > 0, "--help must produce help text on stdout"
@@ -0,0 +1,213 @@
# =================== AIPass ====================
# Name: test_conftest_template.py
# Description: Universal conftest.py Test Fixtures Template
# Version: 1.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
Universal conftest.py Template
Copy this file to any AIPass branch's tests/ directory as conftest.py.
Change BRANCH_MODULE below.
Provides standard fixtures:
- temp_test_dir: tmp_path-based isolated directory with cleanup
- sample_test_data: reusable dict of sample data
- mock_infrastructure: autouse fixture that patches logger + json_handler
- mock_logger: standalone mock logger fixture
- mock_json_handler: standalone mock json_handler fixture
These fixtures establish a consistent test environment across all branches.
"""
import importlib
import json
import logging
import sys
import types
from pathlib import Path
from unittest.mock import MagicMock
import pytest
# ============ BRANCH CONFIG ============
# Change these two lines when deploying to a branch:
BRANCH_MODULE = "seedgo" # e.g. "prax", "drone", "backup", "cli", etc.
# For commons: "commons" (import path is different: aipass -> just commons)
# For skills: "skills" (import path is different: aipass -> just skills)
# =======================================
# ---------------------------------------------------------------------------
# Dynamic import with cross-branch guard bypass
# ---------------------------------------------------------------------------
if BRANCH_MODULE in ("commons", "skills"):
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
else:
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.json.json_handler"
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
if BRANCH_MODULE in ("commons", "skills"):
_handlers_dir = (
Path(__file__).resolve().parents[3] / BRANCH_MODULE / "apps" / "handlers"
)
else:
_handlers_dir = (
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
)
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_json_mod = importlib.import_module(_json_mod_path)
# ---------------------------------------------------------------------------
# JSON_DIR variable discovery (same pattern as json_handler template)
# ---------------------------------------------------------------------------
_JSON_DIR_ATTR: str | None = None
_JSON_DIR_CANDIDATES = [
f"{BRANCH_MODULE.upper()}_JSON_DIR",
"JSON_DIR",
"BRANCH_JSON_DIR",
f"{BRANCH_MODULE}_json",
"_JSON_DIR",
]
for _candidate in _JSON_DIR_CANDIDATES:
if hasattr(_json_mod, _candidate):
_JSON_DIR_ATTR = _candidate
break
# ---------------------------------------------------------------------------
# Fixtures
# ---------------------------------------------------------------------------
@pytest.fixture()
def temp_test_dir(tmp_path: Path):
"""Provide a temporary directory for test isolation.
Yields the tmp_path directory. Cleanup is handled automatically by
pytest's tmp_path mechanism, but this fixture provides a named
semantic entry point for branch tests.
"""
test_dir = tmp_path / "test_workspace"
test_dir.mkdir(parents=True, exist_ok=True)
yield test_dir
# Cleanup: tmp_path is automatically removed by pytest after the test
# session. Explicit cleanup here handles any branch-specific teardown
# that may be needed.
for child in test_dir.iterdir():
if child.is_file():
child.unlink()
@pytest.fixture()
def sample_test_data() -> dict:
"""Provide a reusable sample data dictionary for tests.
Returns a dict with standard AIPass data structure keys
(created, last_updated) plus sample entries that tests can
use for validation, serialization, and round-trip checks.
"""
return {
"created": "2026-01-01",
"last_updated": "2026-01-15",
"entries": [
{"id": 1, "name": "alpha", "status": "active"},
{"id": 2, "name": "beta", "status": "pending"},
],
"metadata": {
"source": "test_fixture",
"version": "1.0.0",
},
}
@pytest.fixture(autouse=True)
def mock_infrastructure(
tmp_path: Path,
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""Autouse fixture that isolates JSON operations and silences logging.
This fixture:
1. Redirects the branch's JSON_DIR to tmp_path (test isolation)
2. Patches the branch logger to a NullHandler (no console noise)
Applied automatically to every test in the directory.
"""
# Isolate JSON directory
if _JSON_DIR_ATTR is not None:
original_value = getattr(_json_mod, _JSON_DIR_ATTR)
if isinstance(original_value, str):
monkeypatch.setattr(_json_mod, _JSON_DIR_ATTR, str(tmp_path))
else:
monkeypatch.setattr(_json_mod, _JSON_DIR_ATTR, tmp_path)
# Silence branch logger
logger_names = [
f"aipass.{BRANCH_MODULE}",
BRANCH_MODULE,
f"{BRANCH_MODULE}.apps.handlers.json.json_handler",
]
for logger_name in logger_names:
logger = logging.getLogger(logger_name)
monkeypatch.setattr(logger, "handlers", [logging.NullHandler()])
@pytest.fixture()
def mock_logger() -> MagicMock:
"""Provide a standalone mock logger for tests that need to verify logging calls.
Returns a MagicMock with standard logging method stubs (debug, info,
warning, error, critical). Use this when you need to assert that
specific log messages were emitted.
Example:
def test_something(mock_logger, monkeypatch):
monkeypatch.setattr(my_module, "logger", mock_logger)
my_module.do_thing()
mock_logger.info.assert_called_once()
"""
logger = MagicMock(spec=logging.Logger)
logger.debug = MagicMock()
logger.info = MagicMock()
logger.warning = MagicMock()
logger.error = MagicMock()
logger.critical = MagicMock()
return logger
@pytest.fixture()
def mock_json_handler() -> MagicMock:
"""Provide a standalone mock json_handler for tests that need to isolate
from real file I/O.
Returns a MagicMock with stubs for all standard json_handler functions.
Useful when testing modules that CALL json_handler but you want to
verify the calls without touching the filesystem.
Example:
def test_my_module(mock_json_handler, monkeypatch):
monkeypatch.setattr(my_module, "json_handler", mock_json_handler)
mock_json_handler.load_json.return_value = {"key": "val"}
result = my_module.process()
mock_json_handler.save_json.assert_called_once()
"""
handler = MagicMock()
handler.load_json = MagicMock(return_value={})
handler.save_json = MagicMock(return_value=True)
handler.ensure_json_exists = MagicMock(return_value=True)
handler.ensure_module_jsons = MagicMock(return_value=True)
handler.get_json_path = MagicMock(return_value=Path("/tmp/mock.json"))
handler.validate_json_structure = MagicMock(return_value=True)
handler.log_operation = MagicMock(return_value=True)
return handler
@@ -0,0 +1,281 @@
# =================== AIPass ====================
# Name: test_contracts_template.py
# Description: Universal Contracts Test Template (return types, exceptions, data structures)
# Version: 1.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
Universal Contracts Test Template
Copy this file to any AIPass branch's tests/ directory.
Change BRANCH_MODULE below. Run with pytest.
Covers 10 tests across 3 groups:
- Return type contracts (4): handle_command->bool, get_json_path->Path,
ensure_json_exists->bool, load_json->dict
- Exception contracts (3): _create_default unknown type raises ValueError,
save_json invalid structure raises ValueError, invalid mode raises ValueError
- Data structure contracts (3): config has module_name/version,
data has created/last_updated, log entry has operation
These tests enforce the behavioral contracts that all AIPass branches
must honor. They verify what functions RETURN, what they RAISE, and
what data shapes they PRODUCE.
"""
import importlib
import json
import sys
import types
from pathlib import Path
from typing import Any
import pytest
# ============ BRANCH CONFIG ============
# Change these two lines when deploying to a branch:
BRANCH_MODULE = "seedgo" # e.g. "prax", "drone", "backup", "cli", etc.
# For commons: "commons" (import path is different: aipass -> just commons)
# For skills: "skills" (import path is different: aipass -> just skills)
# =======================================
# ---------------------------------------------------------------------------
# Dynamic import with cross-branch guard bypass
# ---------------------------------------------------------------------------
if BRANCH_MODULE in ("commons", "skills"):
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
else:
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.json.json_handler"
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
if BRANCH_MODULE in ("commons", "skills"):
_handlers_dir = (
Path(__file__).resolve().parents[3] / BRANCH_MODULE / "apps" / "handlers"
)
else:
_handlers_dir = (
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
)
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_mod = importlib.import_module(_json_mod_path)
json_handler = _mod
# ---------------------------------------------------------------------------
# JSON_DIR variable discovery
# ---------------------------------------------------------------------------
_JSON_DIR_ATTR: str | None = None
_JSON_DIR_CANDIDATES = [
f"{BRANCH_MODULE.upper()}_JSON_DIR",
"JSON_DIR",
"BRANCH_JSON_DIR",
f"{BRANCH_MODULE}_json",
"_JSON_DIR",
]
for _candidate in _JSON_DIR_CANDIDATES:
if hasattr(_mod, _candidate):
_JSON_DIR_ATTR = _candidate
break
if _JSON_DIR_ATTR is None:
pytest.skip(
f"Cannot find JSON_DIR attribute on {BRANCH_MODULE}.json_handler -- "
f"tried: {_JSON_DIR_CANDIDATES}",
allow_module_level=True,
)
# ---------------------------------------------------------------------------
# Isolation fixture
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def isolate_json_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect JSON operations to tmp_path for test isolation."""
assert _JSON_DIR_ATTR is not None
original_value = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(original_value, str):
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, str(tmp_path))
else:
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, tmp_path)
return tmp_path
# ---------------------------------------------------------------------------
# Default factory helpers (same as json_handler template)
# ---------------------------------------------------------------------------
def _get_default_for_type(json_type: str, module_name: str = "test_mod") -> Any:
"""Call whichever default factory the branch exposes."""
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
"load_template",
):
fn = getattr(_mod, fn_name, None)
if fn is not None:
return fn(json_type, module_name)
if json_type == "config" and hasattr(_mod, "_default_config"):
return _mod._default_config(module_name)
if json_type == "data" and hasattr(_mod, "_default_data"):
return _mod._default_data(module_name)
if json_type == "log" and hasattr(_mod, "_default_log"):
return _mod._default_log(module_name)
return None
def _default_factory_raises_on_unknown() -> bool:
"""Return True if the default factory raises ValueError for unknown types."""
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
):
fn = getattr(_mod, fn_name, None)
if fn is not None:
try:
fn("__nonexistent_type__", "test_mod")
except ValueError:
return True
except Exception:
return False
return False
return False
# ============================================================================
# Group 1 -- Return type contracts (4 tests)
# ============================================================================
def test_handle_command_returns_bool() -> None: # CT-001
"""handle_command must return a bool (not int, not None, not truthy)."""
# Try to import the CLI handler
try:
if BRANCH_MODULE in ("commons", "skills"):
cli_mod_path = f"{BRANCH_MODULE}.apps.handlers.cli.cli_handler"
else:
cli_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.cli.cli_handler"
cli_mod = importlib.import_module(cli_mod_path)
except (ImportError, ModuleNotFoundError):
pytest.skip("Branch does not have a CLI handler")
handle = getattr(cli_mod, "handle_command", None)
if handle is None:
pytest.skip("Branch CLI handler does not expose handle_command")
result = handle("help", [])
assert isinstance(result, bool), f"handle_command must return bool, got {type(result)}"
def test_get_json_path_returns_path() -> None: # CT-002
"""get_json_path must return a Path or str (filesystem path type)."""
result = json_handler.get_json_path("contract_mod", "config")
assert isinstance(result, (Path, str)), (
f"get_json_path must return Path or str, got {type(result)}"
)
def test_ensure_json_exists_returns_bool(tmp_path: Path) -> None: # CT-003
"""ensure_json_exists must return a bool."""
result = json_handler.ensure_json_exists("contract_mod", "data")
assert isinstance(result, bool), (
f"ensure_json_exists must return bool, got {type(result)}"
)
assert result is True
def test_load_json_returns_dict_for_config(tmp_path: Path) -> None: # CT-004
"""load_json for config type must return a dict."""
result = json_handler.load_json("contract_mod", "config")
assert isinstance(result, dict), (
f"load_json('...', 'config') must return dict, got {type(result)}"
)
# ============================================================================
# Group 2 -- Exception contracts (3 tests)
# ============================================================================
def test_create_default_unknown_raises_value_error() -> None: # CT-005
"""_create_default (or equivalent) must raise ValueError for unknown type."""
if not _default_factory_raises_on_unknown():
pytest.skip("Branch default factory does not raise ValueError for unknown types")
with pytest.raises(ValueError, match="[Uu]nknown"):
_get_default_for_type("__nonexistent__", "test_mod")
def test_save_json_invalid_structure_raises_value_error(tmp_path: Path) -> None: # CT-006
"""save_json must raise ValueError when given an invalid structure."""
json_dir = tmp_path
json_dir.mkdir(parents=True, exist_ok=True)
with pytest.raises(ValueError, match="[Ii]nvalid"):
json_handler.save_json("bad", "config", {"missing": "keys"})
def test_validate_rejects_invalid_mode() -> None: # CT-007
"""validate_json_structure must return False for an unknown json_type.
Some branches raise ValueError instead of returning False -- both
are acceptable contracts. This test accepts either behavior.
"""
try:
result = json_handler.validate_json_structure({}, "invalid_mode_xyz")
except ValueError:
# Raising ValueError for unknown type is a valid contract
return
assert result is False, "validate_json_structure must return False for unknown type"
# ============================================================================
# Group 3 -- Data structure contracts (3 tests)
# ============================================================================
def test_config_has_required_keys(tmp_path: Path) -> None: # CT-008
"""Config data structure must contain module_name and version."""
json_handler.ensure_json_exists("struct_mod", "config")
result = json_handler.load_json("struct_mod", "config")
assert isinstance(result, dict), "Config must be a dict"
assert "module_name" in result, "Config must have 'module_name' key"
assert "version" in result, "Config must have 'version' key"
def test_data_has_date_keys(tmp_path: Path) -> None: # CT-009
"""Data structure must contain created and last_updated."""
json_handler.ensure_json_exists("struct_mod", "data")
result = json_handler.load_json("struct_mod", "data")
assert isinstance(result, dict), "Data must be a dict"
assert "created" in result, "Data must have 'created' key"
assert "last_updated" in result, "Data must have 'last_updated' key"
def test_log_entry_has_operation(tmp_path: Path) -> None: # CT-010
"""Log entries created by log_operation must contain an 'operation' field."""
json_handler.log_operation("contract_test", module_name="struct_mod")
assert _JSON_DIR_ATTR is not None
val = getattr(_mod, _JSON_DIR_ATTR)
json_dir = Path(val) if isinstance(val, str) else val
log = json.loads(
(json_dir / "struct_mod_log.json").read_text(encoding="utf-8")
)
assert len(log) >= 1, "log_operation must append at least one entry"
assert "operation" in log[-1], "Log entry must have 'operation' key"
assert log[-1]["operation"] == "contract_test"
@@ -0,0 +1,220 @@
# =================== AIPass ====================
# Name: test_error_resilience_template.py
# Description: Universal Error Resilience Test Template
# Version: 1.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
Universal Error Resilience Test Template
Copy this file to any AIPass branch's tests/ directory.
Change BRANCH_MODULE below. Run with pytest.
Covers 4 tests:
- test_missing_file: FileNotFoundError or graceful default on missing file
- test_corrupt_json: JSONDecodeError handled, file regenerated
- test_empty_file: empty content handled gracefully
- test_nonexistent_dir: missing directory handled gracefully
These tests verify that the branch's json_handler degrades gracefully
under real-world failure conditions (missing files, corrupt data,
empty files, missing directories). Every branch must survive these
scenarios without crashing.
"""
import importlib
import json
import sys
import types
from pathlib import Path
import pytest
# ============ BRANCH CONFIG ============
# Change these two lines when deploying to a branch:
BRANCH_MODULE = "seedgo" # e.g. "prax", "drone", "backup", "cli", etc.
# For commons: "commons" (import path is different: aipass -> just commons)
# For skills: "skills" (import path is different: aipass -> just skills)
# =======================================
# ---------------------------------------------------------------------------
# Dynamic import with cross-branch guard bypass
# ---------------------------------------------------------------------------
if BRANCH_MODULE in ("commons", "skills"):
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
else:
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.json.json_handler"
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
if BRANCH_MODULE in ("commons", "skills"):
_handlers_dir = (
Path(__file__).resolve().parents[3] / BRANCH_MODULE / "apps" / "handlers"
)
else:
_handlers_dir = (
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
)
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_mod = importlib.import_module(_json_mod_path)
json_handler = _mod
# ---------------------------------------------------------------------------
# JSON_DIR variable discovery
# ---------------------------------------------------------------------------
_JSON_DIR_ATTR: str | None = None
_JSON_DIR_CANDIDATES = [
f"{BRANCH_MODULE.upper()}_JSON_DIR",
"JSON_DIR",
"BRANCH_JSON_DIR",
f"{BRANCH_MODULE}_json",
"_JSON_DIR",
]
for _candidate in _JSON_DIR_CANDIDATES:
if hasattr(_mod, _candidate):
_JSON_DIR_ATTR = _candidate
break
if _JSON_DIR_ATTR is None:
pytest.skip(
f"Cannot find JSON_DIR attribute on {BRANCH_MODULE}.json_handler -- "
f"tried: {_JSON_DIR_CANDIDATES}",
allow_module_level=True,
)
# ---------------------------------------------------------------------------
# Isolation fixture
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def isolate_json_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect JSON operations to tmp_path for test isolation."""
assert _JSON_DIR_ATTR is not None
original_value = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(original_value, str):
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, str(tmp_path))
else:
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, tmp_path)
return tmp_path
def _json_dir_as_path(tmp_path: Path) -> Path:
"""Return the patched JSON dir as a Path (handles str-typed branches)."""
assert _JSON_DIR_ATTR is not None
val = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(val, str):
return Path(val)
return val
# ============================================================================
# Error Resilience Tests (4 tests)
# ============================================================================
def test_missing_file(tmp_path: Path) -> None: # ER-001
"""Loading a non-existent file returns a graceful default, not a crash.
The json_handler should either:
- Auto-create the file and return a valid default, OR
- Raise FileNotFoundError (acceptable explicit failure)
It must NOT raise an unhandled exception or return None without
creating the file.
"""
json_dir = _json_dir_as_path(tmp_path)
target = json_dir / "ghost_config.json"
assert not target.exists(), "Precondition: file must not exist"
try:
result = json_handler.load_json("ghost", "config")
except FileNotFoundError:
# Explicit FileNotFoundError is acceptable -- branch chose
# not to auto-create on load. This is a valid contract.
return
# If no exception, the handler auto-created a default
assert result is not None, "load_json must not return None for missing file"
assert isinstance(result, dict), "Auto-created config must be a dict"
def test_corrupt_json(tmp_path: Path) -> None: # ER-002
"""Corrupt JSON on disk is handled gracefully -- file is regenerated.
Writes invalid bytes to a JSON file, then calls ensure_json_exists.
The handler must detect the corruption and regenerate a valid default.
"""
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "corrupt_data.json"
target.write_bytes(b"\x00\x01NOT-JSON{{{broken")
# ensure_json_exists must heal the corrupt file
result = json_handler.ensure_json_exists("corrupt", "data")
assert result is True, "ensure_json_exists must return True after healing"
# The file on disk must now be valid JSON
raw = target.read_text(encoding="utf-8")
data = json.loads(raw) # must not raise JSONDecodeError
assert isinstance(data, dict), "Regenerated data file must be a dict"
assert "created" in data, "Regenerated data must have 'created' key"
assert "last_updated" in data, "Regenerated data must have 'last_updated' key"
def test_empty_file(tmp_path: Path) -> None: # ER-003
"""An empty file (0 bytes) is handled gracefully.
Writes an empty file, then calls ensure_json_exists. The handler
must detect that the file is empty/unparseable and regenerate it.
"""
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "empty_log.json"
target.write_text("", encoding="utf-8")
result = json_handler.ensure_json_exists("empty", "log")
assert result is True, "ensure_json_exists must return True after healing empty file"
raw = target.read_text(encoding="utf-8")
data = json.loads(raw)
assert isinstance(data, list), "Regenerated log file must be a list"
def test_nonexistent_dir(tmp_path: Path) -> None: # ER-004
"""Missing parent directory is handled gracefully.
Points JSON_DIR at a directory that does not exist. The handler
must either create the directory automatically or raise a clear
error -- not crash with an obscure traceback.
"""
json_dir = tmp_path / "does_not_exist" / "nested"
assert not json_dir.exists(), "Precondition: directory must not exist"
# Patch JSON_DIR to the non-existent directory
assert _JSON_DIR_ATTR is not None
original_value = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(original_value, str):
setattr(_mod, _JSON_DIR_ATTR, str(json_dir))
else:
setattr(_mod, _JSON_DIR_ATTR, json_dir)
try:
result = json_handler.ensure_json_exists("nodir", "config")
# If it succeeds, the directory must have been created
assert json_dir.exists(), "Handler must create missing directories"
assert result is True
except (FileNotFoundError, OSError):
# Explicit filesystem error is acceptable -- the handler chose
# not to auto-create directories. This is a valid contract.
pass
@@ -0,0 +1,228 @@
# =================== AIPass ====================
# Name: test_init_provisioning_template.py
# Description: Universal Init/Provisioning Test Template
# Version: 1.0.0
# Created: 2026-03-24
# Modified: 2026-03-24
# =============================================
"""
Universal Init/Provisioning Test Template
Copy this file to any AIPass branch's tests/ directory.
Change BRANCH_MODULE below. Run with pytest.
Covers 4 tests:
- test_creates_expected_files: ensure_json_exists creates files on disk
- test_auto_creates_directory: mkdir/makedirs runs when dir is missing
- test_no_overwrite_on_second_call: idempotent -- second call preserves data
- test_returns_dict_with_expected_keys: provisioned file has correct structure
These tests verify the "cold start" contract: when a branch is freshly
cloned or its JSON directory is empty, the provisioning functions must
bootstrap valid files without manual intervention.
"""
import importlib
import json
import sys
import types
from pathlib import Path
import pytest
# ============ BRANCH CONFIG ============
# Change these two lines when deploying to a branch:
BRANCH_MODULE = "seedgo" # e.g. "prax", "drone", "backup", "cli", etc.
# For commons: "commons" (import path is different: aipass -> just commons)
# For skills: "skills" (import path is different: aipass -> just skills)
# =======================================
# ---------------------------------------------------------------------------
# Dynamic import with cross-branch guard bypass
# ---------------------------------------------------------------------------
if BRANCH_MODULE in ("commons", "skills"):
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
else:
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
_json_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.json.json_handler"
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
if BRANCH_MODULE in ("commons", "skills"):
_handlers_dir = (
Path(__file__).resolve().parents[3] / BRANCH_MODULE / "apps" / "handlers"
)
else:
_handlers_dir = (
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
)
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_mod = importlib.import_module(_json_mod_path)
json_handler = _mod
# ---------------------------------------------------------------------------
# JSON_DIR variable discovery
# ---------------------------------------------------------------------------
_JSON_DIR_ATTR: str | None = None
_JSON_DIR_CANDIDATES = [
f"{BRANCH_MODULE.upper()}_JSON_DIR",
"JSON_DIR",
"BRANCH_JSON_DIR",
f"{BRANCH_MODULE}_json",
"_JSON_DIR",
]
for _candidate in _JSON_DIR_CANDIDATES:
if hasattr(_mod, _candidate):
_JSON_DIR_ATTR = _candidate
break
if _JSON_DIR_ATTR is None:
pytest.skip(
f"Cannot find JSON_DIR attribute on {BRANCH_MODULE}.json_handler -- "
f"tried: {_JSON_DIR_CANDIDATES}",
allow_module_level=True,
)
# ---------------------------------------------------------------------------
# Isolation fixture
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def isolate_json_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect JSON operations to tmp_path for test isolation."""
assert _JSON_DIR_ATTR is not None
original_value = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(original_value, str):
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, str(tmp_path))
else:
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, tmp_path)
return tmp_path
def _json_dir_as_path(tmp_path: Path) -> Path:
"""Return the patched JSON dir as a Path (handles str-typed branches)."""
assert _JSON_DIR_ATTR is not None
val = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(val, str):
return Path(val)
return val
# ============================================================================
# Init/Provisioning Tests (4 tests)
# ============================================================================
def test_creates_expected_files(tmp_path: Path) -> None: # IP-001
"""ensure_json_exists creates the expected file on disk.
After calling ensure_json_exists for each type (config, data, log),
the corresponding file must exist in the JSON directory.
"""
json_dir = _json_dir_as_path(tmp_path)
for json_type in ("config", "data", "log"):
result = json_handler.ensure_json_exists("prov_mod", json_type)
assert result is True, f"ensure_json_exists must return True for {json_type}"
expected = json_dir / f"prov_mod_{json_type}.json"
assert expected.exists(), (
f"ensure_json_exists must create {expected.name} on disk"
)
# Verify the file contains valid JSON
raw = expected.read_text(encoding="utf-8")
parsed = json.loads(raw) # must not raise
assert parsed is not None, f"{expected.name} must contain valid JSON"
def test_auto_creates_directory(tmp_path: Path) -> None: # IP-002
"""ensure_json_exists auto-creates the parent directory when missing.
Points JSON_DIR at a non-existent subdirectory. The handler must
create it (mkdir -p equivalent) rather than failing.
"""
nested_dir = tmp_path / "auto_created" / "subdir"
assert not nested_dir.exists(), "Precondition: directory must not exist"
# Patch JSON_DIR to the nested non-existent directory
assert _JSON_DIR_ATTR is not None
original_value = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(original_value, str):
setattr(_mod, _JSON_DIR_ATTR, str(nested_dir))
else:
setattr(_mod, _JSON_DIR_ATTR, nested_dir)
try:
result = json_handler.ensure_json_exists("autodir", "config")
assert nested_dir.exists(), (
"ensure_json_exists must auto-create missing directories"
)
assert result is True
assert (nested_dir / "autodir_config.json").exists()
except (FileNotFoundError, OSError):
# Some branches may not auto-create directories -- this is
# acceptable but should be documented in the branch's contract
pytest.skip("Branch does not auto-create missing directories")
def test_no_overwrite_on_second_call(tmp_path: Path) -> None: # IP-003
"""Second call to ensure_json_exists must not overwrite existing data.
This verifies idempotency: provisioning runs safely on every startup
without destroying previously saved data.
"""
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
# First call: provision the file
json_handler.ensure_json_exists("idem_mod", "data")
# Inject custom data into the provisioned file
target = json_dir / "idem_mod_data.json"
original = json.loads(target.read_text(encoding="utf-8"))
original["custom_field"] = "do_not_overwrite"
target.write_text(json.dumps(original, indent=2), encoding="utf-8")
# Second call: must preserve the custom field
json_handler.ensure_json_exists("idem_mod", "data")
after = json.loads(target.read_text(encoding="utf-8"))
assert after.get("custom_field") == "do_not_overwrite", (
"Second ensure_json_exists call must not overwrite existing valid data"
)
def test_returns_dict_with_expected_keys(tmp_path: Path) -> None: # IP-004
"""Provisioned files contain the correct structure keys.
After ensure_json_exists + load_json, the returned data must have
the mandatory keys for each type.
"""
# Config: must have module_name, version, config
json_handler.ensure_json_exists("key_mod", "config")
config = json_handler.load_json("key_mod", "config")
assert isinstance(config, dict), "Config must be a dict"
assert "module_name" in config, "Config must have 'module_name'"
assert "version" in config, "Config must have 'version'"
# Data: must have created, last_updated
json_handler.ensure_json_exists("key_mod", "data")
data = json_handler.load_json("key_mod", "data")
assert isinstance(data, dict), "Data must be a dict"
assert "created" in data, "Data must have 'created'"
assert "last_updated" in data, "Data must have 'last_updated'"
# Log: must be a list
json_handler.ensure_json_exists("key_mod", "log")
log = json_handler.load_json("key_mod", "log")
assert isinstance(log, list), "Log must be a list"
@@ -0,0 +1,623 @@
# =================== AIPass ====================
# Name: test_json_handler_template.py
# Description: Universal JSON Handler Test Template (DPLAN-0059)
# Version: 1.0.0
# Created: 2026-03-25
# Modified: 2026-03-25
# =============================================
"""
Universal JSON Handler Test Template
Copy this file to any AIPass branch's tests/ directory.
Change BRANCH_MODULE below. Run with pytest.
Covers 43 tests across 8 groups:
- _create_default / default templates (4)
- validate_json_structure (10)
- get_json_path (3)
- ensure_json_exists (5)
- load_json (4)
- save_json (5)
- log_operation (7)
- ensure_module_jsons (5)
"""
import importlib
import json
import sys
import types
from datetime import datetime
from pathlib import Path
from typing import Any
import pytest
# ============ BRANCH CONFIG ============
# Change these two lines when deploying to a branch:
BRANCH_MODULE = "seedgo" # e.g. "prax", "drone", "backup", "cli", etc.
# For commons: "commons" (import path is different: aipass -> just commons)
# For skills: "skills" (import path is different: aipass -> just skills)
# =======================================
# ---------------------------------------------------------------------------
# Dynamic import with cross-branch guard bypass
# ---------------------------------------------------------------------------
# Every branch has an import guard in apps/handlers/__init__.py that blocks
# cross-branch imports. When this template lives in its target branch, the
# guard passes naturally. When testing from devpulse (or any other branch),
# we pre-inject an empty handlers __init__ module to skip the guard.
if BRANCH_MODULE in ("commons", "skills"):
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_json_pkg = f"{BRANCH_MODULE}.apps.handlers.json"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
else:
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
_json_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers.json"
_json_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.json.json_handler"
# If the handlers package is not yet loaded, inject a stub to avoid the guard.
# The stub needs __path__ set so Python treats it as a package for sub-imports.
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
# Resolve the real filesystem path for the handlers package
if BRANCH_MODULE in ("commons", "skills"):
_handlers_dir = (
Path(__file__).resolve().parents[3] / BRANCH_MODULE / "apps" / "handlers"
)
else:
_handlers_dir = (
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
)
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_mod = importlib.import_module(_json_mod_path)
json_handler = _mod
# ---------------------------------------------------------------------------
# JSON_DIR variable discovery
# ---------------------------------------------------------------------------
# Branches use different names: JSON_DIR, BACKUP_JSON_DIR, PRAX_JSON_DIR,
# BRANCH_JSON_DIR, _JSON_DIR, AI_MAIL_JSON_DIR, etc.
# We find the right one at import time so the isolation fixture can patch it.
_JSON_DIR_ATTR: str | None = None
_JSON_DIR_CANDIDATES = [
f"{BRANCH_MODULE.upper()}_JSON_DIR", # SEEDGO_JSON_DIR, BACKUP_JSON_DIR, etc.
"JSON_DIR", # seedgo, daemon, memory, cli, drone
"BRANCH_JSON_DIR", # commons
f"{BRANCH_MODULE}_json", # unlikely but covered
"_JSON_DIR", # spawn
]
for _candidate in _JSON_DIR_CANDIDATES:
if hasattr(_mod, _candidate):
_JSON_DIR_ATTR = _candidate
break
if _JSON_DIR_ATTR is None:
pytest.skip(
f"Cannot find JSON_DIR attribute on {BRANCH_MODULE}.json_handler — "
f"tried: {_JSON_DIR_CANDIDATES}",
allow_module_level=True,
)
# ---------------------------------------------------------------------------
# Default factory discovery
# ---------------------------------------------------------------------------
# Branches use: _create_default, _get_default_template, _get_default,
# _default_template, load_template, or per-type _default_config/_default_data/_default_log.
def _get_default_for_type(json_type: str, module_name: str = "test_mod") -> Any:
"""Call whichever default factory the branch exposes."""
# Single-function factories (most branches)
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
"load_template",
):
fn = getattr(_mod, fn_name, None)
if fn is not None:
return fn(json_type, module_name)
# Per-type factories (drone pattern)
if json_type == "config" and hasattr(_mod, "_default_config"):
return _mod._default_config(module_name)
if json_type == "data" and hasattr(_mod, "_default_data"):
return _mod._default_data(module_name)
if json_type == "log" and hasattr(_mod, "_default_log"):
return _mod._default_log(module_name)
return None
def _has_default_factory() -> bool:
"""Return True if the branch has any callable default factory."""
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
"load_template",
"_default_config",
):
if hasattr(_mod, fn_name):
return True
return False
def _default_factory_raises_on_unknown() -> bool:
"""Return True if the default factory raises ValueError for unknown types."""
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
):
fn = getattr(_mod, fn_name, None)
if fn is not None:
try:
fn("__nonexistent_type__", "test_mod")
except ValueError:
return True
except Exception:
return False
return False
# load_template reads files — may raise FileNotFoundError, not ValueError
# Per-type factories don't have a single entry point for unknown types
return False
# ---------------------------------------------------------------------------
# Isolation fixture
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def isolate_json_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect JSON operations to tmp_path for test isolation."""
assert _JSON_DIR_ATTR is not None
original_value = getattr(_mod, _JSON_DIR_ATTR)
# Some branches store JSON_DIR as a string (commons), others as Path
if isinstance(original_value, str):
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, str(tmp_path))
else:
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, tmp_path)
return tmp_path
# ---------------------------------------------------------------------------
# Helper: resolve JSON dir as Path regardless of branch type
# ---------------------------------------------------------------------------
def _json_dir_as_path(tmp_path: Path) -> Path:
"""Return the patched JSON dir as a Path (handles str-typed branches)."""
assert _JSON_DIR_ATTR is not None
val = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(val, str):
return Path(val)
return val
# ============================================================================
# Group 1 — _create_default / default templates (4 tests)
# ============================================================================
def test_default_config_returns_dict_with_required_keys() -> None: # JH-001
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("config", "test_mod")
assert isinstance(result, dict), "Config default must be a dict"
assert "module_name" in result, "Config default must have module_name"
assert "version" in result, "Config default must have version"
assert "config" in result, "Config default must have config"
def test_default_data_returns_dict_with_date_keys() -> None: # JH-002
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("data", "test_mod")
assert isinstance(result, dict), "Data default must be a dict"
assert "created" in result, "Data default must have created"
assert "last_updated" in result, "Data default must have last_updated"
def test_default_log_returns_empty_list() -> None: # JH-003
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("log", "test_mod")
assert isinstance(result, list), "Log default must be a list"
assert len(result) == 0, "Log default must be empty"
def test_default_unknown_type_raises_value_error() -> None: # JH-004
if not _default_factory_raises_on_unknown():
pytest.skip("Branch default factory does not raise ValueError for unknown types")
with pytest.raises(ValueError, match="[Uu]nknown"):
_get_default_for_type("__nonexistent__", "test_mod")
# ============================================================================
# Group 2 — validate_json_structure (10 tests)
# ============================================================================
def test_validate_valid_config() -> None: # JH-005
data = {"module_name": "x", "version": "1.0.0", "config": {}}
assert json_handler.validate_json_structure(data, "config") is True
def test_validate_config_missing_key() -> None: # JH-006
data = {"module_name": "x", "version": "1.0.0"} # missing config
assert json_handler.validate_json_structure(data, "config") is False
def test_validate_config_not_dict() -> None: # JH-007
assert json_handler.validate_json_structure([1, 2, 3], "config") is False
def test_validate_valid_data() -> None: # JH-008
data = {"created": "2026-01-01", "last_updated": "2026-01-01"}
assert json_handler.validate_json_structure(data, "data") is True
def test_validate_data_missing_key() -> None: # JH-009
data = {"created": "2026-01-01"} # missing last_updated
assert json_handler.validate_json_structure(data, "data") is False
def test_validate_data_not_dict() -> None: # JH-010
assert json_handler.validate_json_structure("not a dict", "data") is False
def test_validate_valid_log() -> None: # JH-011
assert json_handler.validate_json_structure([], "log") is True
assert json_handler.validate_json_structure([{"entry": 1}], "log") is True
def test_validate_log_not_list() -> None: # JH-012
assert json_handler.validate_json_structure({"not": "a list"}, "log") is False
def test_validate_unknown_type_returns_false() -> None: # JH-013
assert json_handler.validate_json_structure({}, "nonexistent_type") is False
def test_validate_none_input_returns_false() -> None: # JH-014
assert json_handler.validate_json_structure(None, "config") is False
assert json_handler.validate_json_structure(None, "data") is False
assert json_handler.validate_json_structure(None, "log") is False
# ============================================================================
# Group 3 — get_json_path (3 tests)
# ============================================================================
def test_get_json_path_returns_path_type(tmp_path: Path) -> None: # JH-015
result = json_handler.get_json_path("mymod", "config")
# Some branches return str (commons), most return Path
assert isinstance(result, (Path, str)), "get_json_path must return Path or str"
def test_get_json_path_filename_pattern(tmp_path: Path) -> None: # JH-016
result = json_handler.get_json_path("mymod", "config")
name = Path(result).name if isinstance(result, str) else result.name
assert name == "mymod_config.json", f"Expected mymod_config.json, got {name}"
def test_get_json_path_different_combos_differ(tmp_path: Path) -> None: # JH-017
path_a = str(json_handler.get_json_path("alpha", "log"))
path_b = str(json_handler.get_json_path("beta", "data"))
assert path_a != path_b, "Different module/type combos must produce different paths"
# ============================================================================
# Group 4 — ensure_json_exists (5 tests)
# ============================================================================
def test_ensure_creates_file_when_missing(tmp_path: Path) -> None: # JH-018
result = json_handler.ensure_json_exists("ens_mod", "config")
assert result is True
json_dir = _json_dir_as_path(tmp_path)
created = json_dir / "ens_mod_config.json"
assert created.exists(), "ensure_json_exists must create the file"
def test_ensure_preserves_valid_existing_file(tmp_path: Path) -> None: # JH-019
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "keep_data.json"
original = {
"created": "2025-01-01",
"last_updated": "2025-06-01",
"custom_key": "preserve_me",
}
target.write_text(json.dumps(original), encoding="utf-8")
json_handler.ensure_json_exists("keep", "data")
data = json.loads(target.read_text(encoding="utf-8"))
assert data["custom_key"] == "preserve_me", "Valid existing file must not be overwritten"
def test_ensure_regenerates_corrupt_json(tmp_path: Path) -> None: # JH-020
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "bad_log.json"
target.write_bytes(b"\x00\x01NOT VALID JSON{{{")
json_handler.ensure_json_exists("bad", "log")
data = json.loads(target.read_text(encoding="utf-8"))
assert isinstance(data, list), "Corrupt JSON must be regenerated to valid log (list)"
def test_ensure_regenerates_invalid_structure(tmp_path: Path) -> None: # JH-021
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "wrong_config.json"
target.write_text(json.dumps({"wrong": "structure"}), encoding="utf-8")
json_handler.ensure_json_exists("wrong", "config")
data = json.loads(target.read_text(encoding="utf-8"))
assert "module_name" in data, "Invalid structure must be regenerated with correct keys"
assert "version" in data
assert "config" in data
def test_ensure_returns_bool(tmp_path: Path) -> None: # JH-022
result = json_handler.ensure_json_exists("bool_mod", "data")
assert isinstance(result, bool), "ensure_json_exists must return bool"
assert result is True
# ============================================================================
# Group 5 — load_json (4 tests)
# ============================================================================
def test_load_creates_default_when_missing(tmp_path: Path) -> None: # JH-023
result = json_handler.load_json("fresh_mod", "log")
assert result is not None, "load_json must auto-create and return content"
assert isinstance(result, list), "Default log must be a list"
def test_load_returns_existing_content(tmp_path: Path) -> None: # JH-024
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
payload = {"created": "2025-01-01", "last_updated": "2025-06-15", "x": 42}
target = json_dir / "exist_data.json"
target.write_text(json.dumps(payload), encoding="utf-8")
result = json_handler.load_json("exist", "data")
assert isinstance(result, dict)
assert result["x"] == 42, "load_json must return existing file content"
def test_load_returns_dict_for_config(tmp_path: Path) -> None: # JH-025
result = json_handler.load_json("cfg_mod", "config")
assert isinstance(result, dict), "load_json for config must return dict"
def test_load_returns_list_for_log(tmp_path: Path) -> None: # JH-026
result = json_handler.load_json("log_mod", "log")
assert isinstance(result, list), "load_json for log must return list"
# ============================================================================
# Group 6 — save_json (5 tests)
# ============================================================================
def test_save_roundtrip(tmp_path: Path) -> None: # JH-027
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
data = {"module_name": "rt", "version": "1.0.0", "config": {"key": "val"}}
json_handler.save_json("rt", "config", data)
loaded = json_handler.load_json("rt", "config")
assert loaded is not None
assert loaded["config"]["key"] == "val", "Saved data must be readable via load_json"
def test_save_returns_true(tmp_path: Path) -> None: # JH-028
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
data = {"module_name": "sv", "version": "1.0.0", "config": {}}
result = json_handler.save_json("sv", "config", data)
assert result is True, "save_json must return True on success"
def test_save_rejects_invalid_structure(tmp_path: Path) -> None: # JH-029
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
with pytest.raises(ValueError, match="[Ii]nvalid"):
json_handler.save_json("bad", "config", {"missing": "keys"})
def test_save_data_updates_last_updated(tmp_path: Path) -> None: # JH-030
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
today = datetime.now().date().isoformat()
data = {"created": "2025-01-01", "last_updated": "2025-01-01"}
json_handler.save_json("ts", "data", data)
on_disk = json.loads(
(json_dir / "ts_data.json").read_text(encoding="utf-8")
)
assert on_disk["last_updated"] == today, "Saving data type must auto-stamp last_updated"
def test_save_writes_valid_json_to_disk(tmp_path: Path) -> None: # JH-031
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
entries = [{"timestamp": "t1", "operation": "test"}]
json_handler.save_json("disk", "log", entries)
raw = (json_dir / "disk_log.json").read_text(encoding="utf-8")
parsed = json.loads(raw) # must not raise
assert isinstance(parsed, list), "Saved file must be valid JSON on disk"
assert len(parsed) == 1
# ============================================================================
# Group 7 — log_operation (7 tests)
# ============================================================================
def test_log_operation_appends_entry(tmp_path: Path) -> None: # JH-032
json_handler.log_operation("deploy", module_name="logmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "logmod_log.json").read_text(encoding="utf-8")
)
assert len(log) >= 1, "log_operation must append at least one entry"
assert log[-1]["operation"] == "deploy"
def test_log_operation_returns_bool(tmp_path: Path) -> None: # JH-033
result = json_handler.log_operation("test_op", module_name="boolmod")
assert isinstance(result, bool), "log_operation must return bool"
assert result is True
def test_log_operation_entry_has_timestamp(tmp_path: Path) -> None: # JH-034
json_handler.log_operation("check_ts", module_name="tsmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "tsmod_log.json").read_text(encoding="utf-8")
)
assert "timestamp" in log[-1], "Log entry must have a timestamp field"
def test_log_operation_includes_data_when_provided(tmp_path: Path) -> None: # JH-035
json_handler.log_operation(
"with_data", data={"count": 5}, module_name="datamod"
)
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "datamod_log.json").read_text(encoding="utf-8")
)
assert "data" in log[-1], "Log entry must include data dict when provided"
assert log[-1]["data"]["count"] == 5
def test_log_operation_multiple_calls_accumulate(tmp_path: Path) -> None: # JH-039
json_handler.log_operation("first", module_name="accmod")
json_handler.log_operation("second", module_name="accmod")
json_handler.log_operation("third", module_name="accmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "accmod_log.json").read_text(encoding="utf-8")
)
assert len(log) >= 3, "Multiple log_operation calls must accumulate entries"
ops = [e["operation"] for e in log[-3:]]
assert ops == ["first", "second", "third"]
def test_log_operation_fifo_rotation(tmp_path: Path) -> None: # JH-040
# Find the max log entries constant
max_entries = getattr(_mod, "MAX_LOG_ENTRIES", getattr(_mod, "max_log_entries", None))
if max_entries is None:
# Try to find it by checking common names
for attr in ("MAX_LOG_ENTRIES", "max_log_entries", "LOG_MAX_ENTRIES", "_MAX_LOG_ENTRIES"):
max_entries = getattr(_mod, attr, None)
if max_entries is not None:
break
if max_entries is None:
pytest.skip("Cannot find max_log_entries constant on module")
# Fill to max + 5
for i in range(max_entries + 5):
json_handler.log_operation(f"op_{i}", module_name="fifomod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "fifomod_log.json").read_text(encoding="utf-8")
)
assert len(log) <= max_entries, f"Log must not exceed {max_entries} entries after rotation"
# First entries should have been rotated out
assert log[-1]["operation"] == f"op_{max_entries + 4}", "Most recent entry must be last"
def test_log_operation_empty_dict_not_attached(tmp_path: Path) -> None: # JH-041
json_handler.log_operation("no_data", data={}, module_name="emptymod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "emptymod_log.json").read_text(encoding="utf-8")
)
entry = log[-1]
# Empty dict should either not be attached or be an empty dict
# The key test: the entry should not have a non-empty "data" field from an empty input
if "data" in entry:
assert entry["data"] == {} or entry["data"] is None, "Empty dict data should not create non-empty data field"
# ============================================================================
# Group 8 — ensure_module_jsons (5 tests)
# ============================================================================
def test_ensure_module_jsons_creates_all_three(tmp_path: Path) -> None: # JH-036
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("triple")
json_dir = _json_dir_as_path(tmp_path)
assert (json_dir / "triple_config.json").exists(), "Config file must exist"
assert (json_dir / "triple_data.json").exists(), "Data file must exist"
assert (json_dir / "triple_log.json").exists(), "Log file must exist"
def test_ensure_module_jsons_returns_true(tmp_path: Path) -> None: # JH-037
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
result = json_handler.ensure_module_jsons("retmod")
assert result is True, "ensure_module_jsons must return True"
def test_ensure_module_jsons_files_pass_validation(tmp_path: Path) -> None: # JH-038
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("valid_mod")
json_dir = _json_dir_as_path(tmp_path)
config = json.loads(
(json_dir / "valid_mod_config.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(config, "config") is True
data = json.loads(
(json_dir / "valid_mod_data.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(data, "data") is True
log = json.loads(
(json_dir / "valid_mod_log.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(log, "log") is True
def test_ensure_module_jsons_data_has_correct_keys(tmp_path: Path) -> None: # JH-042
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("keymod")
json_dir = _json_dir_as_path(tmp_path)
data = json.loads(
(json_dir / "keymod_data.json").read_text(encoding="utf-8")
)
assert "created" in data, "Data file must have 'created' key"
assert "last_updated" in data, "Data file must have 'last_updated' key"
def test_ensure_module_jsons_log_is_empty_list(tmp_path: Path) -> None: # JH-043
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("listmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "listmod_log.json").read_text(encoding="utf-8")
)
assert isinstance(log, list), "Log file must be a list"
assert len(log) == 0, "Initial log file must be an empty list"
@@ -0,0 +1,623 @@
# =================== AIPass ====================
# Name: test_json_handler_template.py
# Description: Universal JSON Handler Test Template (DPLAN-0059)
# Version: 1.0.0
# Created: 2026-03-25
# Modified: 2026-03-25
# =============================================
"""
Universal JSON Handler Test Template
Copy this file to any AIPass branch's tests/ directory.
Change BRANCH_MODULE below. Run with pytest.
Covers 43 tests across 8 groups:
- _create_default / default templates (4)
- validate_json_structure (10)
- get_json_path (3)
- ensure_json_exists (5)
- load_json (4)
- save_json (5)
- log_operation (7)
- ensure_module_jsons (5)
"""
import importlib
import json
import sys
import types
from datetime import datetime
from pathlib import Path
from typing import Any
import pytest
# ============ BRANCH CONFIG ============
# Change these two lines when deploying to a branch:
BRANCH_MODULE = "seedgo" # e.g. "prax", "drone", "backup", "cli", etc.
# For commons: "commons" (import path is different: aipass -> just commons)
# For skills: "skills" (import path is different: aipass -> just skills)
# =======================================
# ---------------------------------------------------------------------------
# Dynamic import with cross-branch guard bypass
# ---------------------------------------------------------------------------
# Every branch has an import guard in apps/handlers/__init__.py that blocks
# cross-branch imports. When this template lives in its target branch, the
# guard passes naturally. When testing from devpulse (or any other branch),
# we pre-inject an empty handlers __init__ module to skip the guard.
if BRANCH_MODULE in ("commons", "skills"):
_handler_pkg = f"{BRANCH_MODULE}.apps.handlers"
_json_pkg = f"{BRANCH_MODULE}.apps.handlers.json"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
else:
_handler_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers"
_json_pkg = f"aipass.{BRANCH_MODULE}.apps.handlers.json"
_json_mod_path = f"aipass.{BRANCH_MODULE}.apps.handlers.json.json_handler"
# If the handlers package is not yet loaded, inject a stub to avoid the guard.
# The stub needs __path__ set so Python treats it as a package for sub-imports.
if _handler_pkg not in sys.modules:
_stub = types.ModuleType(_handler_pkg)
# Resolve the real filesystem path for the handlers package
if BRANCH_MODULE in ("commons", "skills"):
_handlers_dir = (
Path(__file__).resolve().parents[3] / BRANCH_MODULE / "apps" / "handlers"
)
else:
_handlers_dir = (
Path(__file__).resolve().parents[3] / "aipass" / BRANCH_MODULE / "apps" / "handlers"
)
_stub.__path__ = [str(_handlers_dir)]
sys.modules[_handler_pkg] = _stub
_mod = importlib.import_module(_json_mod_path)
json_handler = _mod
# ---------------------------------------------------------------------------
# JSON_DIR variable discovery
# ---------------------------------------------------------------------------
# Branches use different names: JSON_DIR, BACKUP_JSON_DIR, PRAX_JSON_DIR,
# BRANCH_JSON_DIR, _JSON_DIR, AI_MAIL_JSON_DIR, etc.
# We find the right one at import time so the isolation fixture can patch it.
_JSON_DIR_ATTR: str | None = None
_JSON_DIR_CANDIDATES = [
f"{BRANCH_MODULE.upper()}_JSON_DIR", # SEEDGO_JSON_DIR, BACKUP_JSON_DIR, etc.
"JSON_DIR", # seedgo, daemon, memory, cli, drone
"BRANCH_JSON_DIR", # commons
f"{BRANCH_MODULE}_json", # unlikely but covered
"_JSON_DIR", # spawn
]
for _candidate in _JSON_DIR_CANDIDATES:
if hasattr(_mod, _candidate):
_JSON_DIR_ATTR = _candidate
break
if _JSON_DIR_ATTR is None:
pytest.skip(
f"Cannot find JSON_DIR attribute on {BRANCH_MODULE}.json_handler — "
f"tried: {_JSON_DIR_CANDIDATES}",
allow_module_level=True,
)
# ---------------------------------------------------------------------------
# Default factory discovery
# ---------------------------------------------------------------------------
# Branches use: _create_default, _get_default_template, _get_default,
# _default_template, load_template, or per-type _default_config/_default_data/_default_log.
def _get_default_for_type(json_type: str, module_name: str = "test_mod") -> Any:
"""Call whichever default factory the branch exposes."""
# Single-function factories (most branches)
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
"load_template",
):
fn = getattr(_mod, fn_name, None)
if fn is not None:
return fn(json_type, module_name)
# Per-type factories (drone pattern)
if json_type == "config" and hasattr(_mod, "_default_config"):
return _mod._default_config(module_name)
if json_type == "data" and hasattr(_mod, "_default_data"):
return _mod._default_data(module_name)
if json_type == "log" and hasattr(_mod, "_default_log"):
return _mod._default_log(module_name)
return None
def _has_default_factory() -> bool:
"""Return True if the branch has any callable default factory."""
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
"load_template",
"_default_config",
):
if hasattr(_mod, fn_name):
return True
return False
def _default_factory_raises_on_unknown() -> bool:
"""Return True if the default factory raises ValueError for unknown types."""
for fn_name in (
"_create_default",
"_get_default_template",
"_get_default",
"_default_template",
):
fn = getattr(_mod, fn_name, None)
if fn is not None:
try:
fn("__nonexistent_type__", "test_mod")
except ValueError:
return True
except Exception:
return False
return False
# load_template reads files — may raise FileNotFoundError, not ValueError
# Per-type factories don't have a single entry point for unknown types
return False
# ---------------------------------------------------------------------------
# Isolation fixture
# ---------------------------------------------------------------------------
@pytest.fixture(autouse=True)
def isolate_json_dir(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
"""Redirect JSON operations to tmp_path for test isolation."""
assert _JSON_DIR_ATTR is not None
original_value = getattr(_mod, _JSON_DIR_ATTR)
# Some branches store JSON_DIR as a string (commons), others as Path
if isinstance(original_value, str):
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, str(tmp_path))
else:
monkeypatch.setattr(_mod, _JSON_DIR_ATTR, tmp_path)
return tmp_path
# ---------------------------------------------------------------------------
# Helper: resolve JSON dir as Path regardless of branch type
# ---------------------------------------------------------------------------
def _json_dir_as_path(tmp_path: Path) -> Path:
"""Return the patched JSON dir as a Path (handles str-typed branches)."""
assert _JSON_DIR_ATTR is not None
val = getattr(_mod, _JSON_DIR_ATTR)
if isinstance(val, str):
return Path(val)
return val
# ============================================================================
# Group 1 — _create_default / default templates (4 tests)
# ============================================================================
def test_default_config_returns_dict_with_required_keys() -> None: # JH-001
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("config", "test_mod")
assert isinstance(result, dict), "Config default must be a dict"
assert "module_name" in result, "Config default must have module_name"
assert "version" in result, "Config default must have version"
assert "config" in result, "Config default must have config"
def test_default_data_returns_dict_with_date_keys() -> None: # JH-002
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("data", "test_mod")
assert isinstance(result, dict), "Data default must be a dict"
assert "created" in result, "Data default must have created"
assert "last_updated" in result, "Data default must have last_updated"
def test_default_log_returns_empty_list() -> None: # JH-003
if not _has_default_factory():
pytest.skip("Branch has no default factory function")
result = _get_default_for_type("log", "test_mod")
assert isinstance(result, list), "Log default must be a list"
assert len(result) == 0, "Log default must be empty"
def test_default_unknown_type_raises_value_error() -> None: # JH-004
if not _default_factory_raises_on_unknown():
pytest.skip("Branch default factory does not raise ValueError for unknown types")
with pytest.raises(ValueError, match="[Uu]nknown"):
_get_default_for_type("__nonexistent__", "test_mod")
# ============================================================================
# Group 2 — validate_json_structure (10 tests)
# ============================================================================
def test_validate_valid_config() -> None: # JH-005
data = {"module_name": "x", "version": "1.0.0", "config": {}}
assert json_handler.validate_json_structure(data, "config") is True
def test_validate_config_missing_key() -> None: # JH-006
data = {"module_name": "x", "version": "1.0.0"} # missing config
assert json_handler.validate_json_structure(data, "config") is False
def test_validate_config_not_dict() -> None: # JH-007
assert json_handler.validate_json_structure([1, 2, 3], "config") is False
def test_validate_valid_data() -> None: # JH-008
data = {"created": "2026-01-01", "last_updated": "2026-01-01"}
assert json_handler.validate_json_structure(data, "data") is True
def test_validate_data_missing_key() -> None: # JH-009
data = {"created": "2026-01-01"} # missing last_updated
assert json_handler.validate_json_structure(data, "data") is False
def test_validate_data_not_dict() -> None: # JH-010
assert json_handler.validate_json_structure("not a dict", "data") is False
def test_validate_valid_log() -> None: # JH-011
assert json_handler.validate_json_structure([], "log") is True
assert json_handler.validate_json_structure([{"entry": 1}], "log") is True
def test_validate_log_not_list() -> None: # JH-012
assert json_handler.validate_json_structure({"not": "a list"}, "log") is False
def test_validate_unknown_type_returns_false() -> None: # JH-013
assert json_handler.validate_json_structure({}, "nonexistent_type") is False
def test_validate_none_input_returns_false() -> None: # JH-014
assert json_handler.validate_json_structure(None, "config") is False
assert json_handler.validate_json_structure(None, "data") is False
assert json_handler.validate_json_structure(None, "log") is False
# ============================================================================
# Group 3 — get_json_path (3 tests)
# ============================================================================
def test_get_json_path_returns_path_type(tmp_path: Path) -> None: # JH-015
result = json_handler.get_json_path("mymod", "config")
# Some branches return str (commons), most return Path
assert isinstance(result, (Path, str)), "get_json_path must return Path or str"
def test_get_json_path_filename_pattern(tmp_path: Path) -> None: # JH-016
result = json_handler.get_json_path("mymod", "config")
name = Path(result).name if isinstance(result, str) else result.name
assert name == "mymod_config.json", f"Expected mymod_config.json, got {name}"
def test_get_json_path_different_combos_differ(tmp_path: Path) -> None: # JH-017
path_a = str(json_handler.get_json_path("alpha", "log"))
path_b = str(json_handler.get_json_path("beta", "data"))
assert path_a != path_b, "Different module/type combos must produce different paths"
# ============================================================================
# Group 4 — ensure_json_exists (5 tests)
# ============================================================================
def test_ensure_creates_file_when_missing(tmp_path: Path) -> None: # JH-018
result = json_handler.ensure_json_exists("ens_mod", "config")
assert result is True
json_dir = _json_dir_as_path(tmp_path)
created = json_dir / "ens_mod_config.json"
assert created.exists(), "ensure_json_exists must create the file"
def test_ensure_preserves_valid_existing_file(tmp_path: Path) -> None: # JH-019
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "keep_data.json"
original = {
"created": "2025-01-01",
"last_updated": "2025-06-01",
"custom_key": "preserve_me",
}
target.write_text(json.dumps(original), encoding="utf-8")
json_handler.ensure_json_exists("keep", "data")
data = json.loads(target.read_text(encoding="utf-8"))
assert data["custom_key"] == "preserve_me", "Valid existing file must not be overwritten"
def test_ensure_regenerates_corrupt_json(tmp_path: Path) -> None: # JH-020
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "bad_log.json"
target.write_bytes(b"\x00\x01NOT VALID JSON{{{")
json_handler.ensure_json_exists("bad", "log")
data = json.loads(target.read_text(encoding="utf-8"))
assert isinstance(data, list), "Corrupt JSON must be regenerated to valid log (list)"
def test_ensure_regenerates_invalid_structure(tmp_path: Path) -> None: # JH-021
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
target = json_dir / "wrong_config.json"
target.write_text(json.dumps({"wrong": "structure"}), encoding="utf-8")
json_handler.ensure_json_exists("wrong", "config")
data = json.loads(target.read_text(encoding="utf-8"))
assert "module_name" in data, "Invalid structure must be regenerated with correct keys"
assert "version" in data
assert "config" in data
def test_ensure_returns_bool(tmp_path: Path) -> None: # JH-022
result = json_handler.ensure_json_exists("bool_mod", "data")
assert isinstance(result, bool), "ensure_json_exists must return bool"
assert result is True
# ============================================================================
# Group 5 — load_json (4 tests)
# ============================================================================
def test_load_creates_default_when_missing(tmp_path: Path) -> None: # JH-023
result = json_handler.load_json("fresh_mod", "log")
assert result is not None, "load_json must auto-create and return content"
assert isinstance(result, list), "Default log must be a list"
def test_load_returns_existing_content(tmp_path: Path) -> None: # JH-024
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
payload = {"created": "2025-01-01", "last_updated": "2025-06-15", "x": 42}
target = json_dir / "exist_data.json"
target.write_text(json.dumps(payload), encoding="utf-8")
result = json_handler.load_json("exist", "data")
assert isinstance(result, dict)
assert result["x"] == 42, "load_json must return existing file content"
def test_load_returns_dict_for_config(tmp_path: Path) -> None: # JH-025
result = json_handler.load_json("cfg_mod", "config")
assert isinstance(result, dict), "load_json for config must return dict"
def test_load_returns_list_for_log(tmp_path: Path) -> None: # JH-026
result = json_handler.load_json("log_mod", "log")
assert isinstance(result, list), "load_json for log must return list"
# ============================================================================
# Group 6 — save_json (5 tests)
# ============================================================================
def test_save_roundtrip(tmp_path: Path) -> None: # JH-027
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
data = {"module_name": "rt", "version": "1.0.0", "config": {"key": "val"}}
json_handler.save_json("rt", "config", data)
loaded = json_handler.load_json("rt", "config")
assert loaded is not None
assert loaded["config"]["key"] == "val", "Saved data must be readable via load_json"
def test_save_returns_true(tmp_path: Path) -> None: # JH-028
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
data = {"module_name": "sv", "version": "1.0.0", "config": {}}
result = json_handler.save_json("sv", "config", data)
assert result is True, "save_json must return True on success"
def test_save_rejects_invalid_structure(tmp_path: Path) -> None: # JH-029
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
with pytest.raises(ValueError, match="[Ii]nvalid"):
json_handler.save_json("bad", "config", {"missing": "keys"})
def test_save_data_updates_last_updated(tmp_path: Path) -> None: # JH-030
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
today = datetime.now().date().isoformat()
data = {"created": "2025-01-01", "last_updated": "2025-01-01"}
json_handler.save_json("ts", "data", data)
on_disk = json.loads(
(json_dir / "ts_data.json").read_text(encoding="utf-8")
)
assert on_disk["last_updated"] == today, "Saving data type must auto-stamp last_updated"
def test_save_writes_valid_json_to_disk(tmp_path: Path) -> None: # JH-031
json_dir = _json_dir_as_path(tmp_path)
json_dir.mkdir(parents=True, exist_ok=True)
entries = [{"timestamp": "t1", "operation": "test"}]
json_handler.save_json("disk", "log", entries)
raw = (json_dir / "disk_log.json").read_text(encoding="utf-8")
parsed = json.loads(raw) # must not raise
assert isinstance(parsed, list), "Saved file must be valid JSON on disk"
assert len(parsed) == 1
# ============================================================================
# Group 7 — log_operation (7 tests)
# ============================================================================
def test_log_operation_appends_entry(tmp_path: Path) -> None: # JH-032
json_handler.log_operation("deploy", module_name="logmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "logmod_log.json").read_text(encoding="utf-8")
)
assert len(log) >= 1, "log_operation must append at least one entry"
assert log[-1]["operation"] == "deploy"
def test_log_operation_returns_bool(tmp_path: Path) -> None: # JH-033
result = json_handler.log_operation("test_op", module_name="boolmod")
assert isinstance(result, bool), "log_operation must return bool"
assert result is True
def test_log_operation_entry_has_timestamp(tmp_path: Path) -> None: # JH-034
json_handler.log_operation("check_ts", module_name="tsmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "tsmod_log.json").read_text(encoding="utf-8")
)
assert "timestamp" in log[-1], "Log entry must have a timestamp field"
def test_log_operation_includes_data_when_provided(tmp_path: Path) -> None: # JH-035
json_handler.log_operation(
"with_data", data={"count": 5}, module_name="datamod"
)
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "datamod_log.json").read_text(encoding="utf-8")
)
assert "data" in log[-1], "Log entry must include data dict when provided"
assert log[-1]["data"]["count"] == 5
def test_log_operation_multiple_calls_accumulate(tmp_path: Path) -> None: # JH-039
json_handler.log_operation("first", module_name="accmod")
json_handler.log_operation("second", module_name="accmod")
json_handler.log_operation("third", module_name="accmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "accmod_log.json").read_text(encoding="utf-8")
)
assert len(log) >= 3, "Multiple log_operation calls must accumulate entries"
ops = [e["operation"] for e in log[-3:]]
assert ops == ["first", "second", "third"]
def test_log_operation_fifo_rotation(tmp_path: Path) -> None: # JH-040
# Find the max log entries constant
max_entries = getattr(_mod, "MAX_LOG_ENTRIES", getattr(_mod, "max_log_entries", None))
if max_entries is None:
# Try to find it by checking common names
for attr in ("MAX_LOG_ENTRIES", "max_log_entries", "LOG_MAX_ENTRIES", "_MAX_LOG_ENTRIES"):
max_entries = getattr(_mod, attr, None)
if max_entries is not None:
break
if max_entries is None:
pytest.skip("Cannot find max_log_entries constant on module")
# Fill to max + 5
for i in range(max_entries + 5):
json_handler.log_operation(f"op_{i}", module_name="fifomod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "fifomod_log.json").read_text(encoding="utf-8")
)
assert len(log) <= max_entries, f"Log must not exceed {max_entries} entries after rotation"
# First entries should have been rotated out
assert log[-1]["operation"] == f"op_{max_entries + 4}", "Most recent entry must be last"
def test_log_operation_empty_dict_not_attached(tmp_path: Path) -> None: # JH-041
json_handler.log_operation("no_data", data={}, module_name="emptymod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "emptymod_log.json").read_text(encoding="utf-8")
)
entry = log[-1]
# Empty dict should either not be attached or be an empty dict
# The key test: the entry should not have a non-empty "data" field from an empty input
if "data" in entry:
assert entry["data"] == {} or entry["data"] is None, "Empty dict data should not create non-empty data field"
# ============================================================================
# Group 8 — ensure_module_jsons (5 tests)
# ============================================================================
def test_ensure_module_jsons_creates_all_three(tmp_path: Path) -> None: # JH-036
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("triple")
json_dir = _json_dir_as_path(tmp_path)
assert (json_dir / "triple_config.json").exists(), "Config file must exist"
assert (json_dir / "triple_data.json").exists(), "Data file must exist"
assert (json_dir / "triple_log.json").exists(), "Log file must exist"
def test_ensure_module_jsons_returns_true(tmp_path: Path) -> None: # JH-037
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
result = json_handler.ensure_module_jsons("retmod")
assert result is True, "ensure_module_jsons must return True"
def test_ensure_module_jsons_files_pass_validation(tmp_path: Path) -> None: # JH-038
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("valid_mod")
json_dir = _json_dir_as_path(tmp_path)
config = json.loads(
(json_dir / "valid_mod_config.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(config, "config") is True
data = json.loads(
(json_dir / "valid_mod_data.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(data, "data") is True
log = json.loads(
(json_dir / "valid_mod_log.json").read_text(encoding="utf-8")
)
assert json_handler.validate_json_structure(log, "log") is True
def test_ensure_module_jsons_data_has_correct_keys(tmp_path: Path) -> None: # JH-042
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("keymod")
json_dir = _json_dir_as_path(tmp_path)
data = json.loads(
(json_dir / "keymod_data.json").read_text(encoding="utf-8")
)
assert "created" in data, "Data file must have 'created' key"
assert "last_updated" in data, "Data file must have 'last_updated' key"
def test_ensure_module_jsons_log_is_empty_list(tmp_path: Path) -> None: # JH-043
if not hasattr(json_handler, "ensure_module_jsons"):
pytest.skip("Branch does not have ensure_module_jsons")
json_handler.ensure_module_jsons("listmod")
json_dir = _json_dir_as_path(tmp_path)
log = json.loads(
(json_dir / "listmod_log.json").read_text(encoding="utf-8")
)
assert isinstance(log, list), "Log file must be a list"
assert len(log) == 0, "Initial log file must be an empty list"
@@ -0,0 +1,5 @@
# Sent Mail
Sent email records for `{{BRANCHNAME}}`.
Outgoing messages are logged here by the ai_mail system.
@@ -0,0 +1,6 @@
{
"mailbox": "inbox",
"total_messages": 0,
"unread_count": 0,
"messages": []
}
@@ -0,0 +1,3 @@
# Branch Prompt
AI context for `{{BRANCHNAME}}`. The `aipass_local_prompt.md` file is injected every turn, telling the AI who you are and how to work in your branch.
@@ -0,0 +1,5 @@
# Archive
Disabled and archived files for `{{BRANCHNAME}}`.
Files tagged `(disabled)` are moved here instead of deleted.
@@ -0,0 +1,5 @@
# Claude Code Settings
Claude Code configuration for `{{BRANCHNAME}}`.
Contains `settings.local.json` with permission rules. Most branches are denied raw git commands and must use `drone @git` instead.
@@ -0,0 +1,28 @@
{
"permissions": {
"allow": [],
"deny": [
"Bash(git reset*)",
"Bash(git rebase*)",
"Bash(git merge*)",
"Bash(git config*)",
"Bash(git push --force*)",
"Bash(git push -f *)",
"Bash(git checkout -- *)",
"Bash(git checkout .*)",
"Bash(git restore --staged*)",
"Bash(git restore .*)",
"Bash(git clean*)",
"Bash(git branch -D*)",
"Bash(git stash drop*)",
"Bash(git stash clear*)",
"Bash(rm -rf*)",
"Bash(rm -r *)",
"Bash(git checkout -b*)",
"Bash(git commit*)",
"Bash(git push*)"
],
"ask": []
},
"enabledMcpjsonServers": []
}
@@ -0,0 +1,5 @@
# Standards Bypass
Seedgo audit bypass config for `{{BRANCHNAME}}`.
When an audit flags a false positive that doesn't apply to your architecture, add a bypass entry in `bypass.json` with a reason explaining why it's justified.
@@ -0,0 +1,43 @@
{
"metadata": {
"version": "1.1.0",
"description": "Files and patterns to exclude when updating branches from template",
"last_updated": "2025-11-09"
},
"ignore_files": [
".template_registry.json",
".registry_ignore.json"
],
"ignore_patterns": [
"__pycache__",
"*.pyc",
"#@comments.txt",
".backup_*"
],
"notes": {
"how_it_works": "Two types of exclusions during branch updates:",
"ignore_files": {
"description": "Exact filename matches (no wildcards)",
"purpose": "Template-internal management files that branches don't need",
"examples": [
".template_registry.json (template's file tracking list)",
".registry_ignore.json (this config file)"
]
},
"ignore_patterns": {
"description": "Glob patterns - matches filenames AND directory names",
"purpose": "Exclude generated files, caches, and user content markers",
"examples": [
"__pycache__ (Python cache directories)",
"*.pyc (Python compiled bytecode files)",
"#@comments.txt (user comment files)",
".backup_* (timestamped backup files like .backup_20251109, NOT .backup directory)"
],
"how_matching_works": [
"Step 1: Check if filename matches pattern (e.g., file.pyc matches *.pyc)",
"Step 2: Check if any parent directory matches (e.g., files inside __pycache__/ dir)"
]
},
"what_gets_ignored": "These files never copy to branches during updates - they stay template-only"
}
}
@@ -0,0 +1,238 @@
{
"metadata": {
"description": "Template registry for tracking files",
"generated": true
},
"files": {
"f001": {
"path": ".ai_mail.local/inbox.json",
"name": "inbox.json",
"content_hash": "c9702fe2cc21b748",
"has_branch_placeholder": false
},
"f002": {
"path": ".ai_mail.local/sent/README.md",
"name": "README.md",
"content_hash": "49299c242a018d73",
"has_branch_placeholder": true
},
"f003": {
"path": ".aipass/aipass_local_prompt.md",
"name": "aipass_local_prompt.md",
"content_hash": "5f8e2a3859a99906",
"has_branch_placeholder": true
},
"f004": {
"path": ".archive/README.md",
"name": "README.md",
"content_hash": "93d3fcb74f234b1d",
"has_branch_placeholder": true
},
"f005": {
"path": ".claude/settings.local.json",
"name": "settings.local.json",
"content_hash": "eacf065629cd9c03",
"has_branch_placeholder": false
},
"f006": {
"path": ".gitignore",
"name": ".gitignore",
"content_hash": "841dedb922da7ddb",
"has_branch_placeholder": false
},
"f007": {
"path": ".seedgo/bypass.json",
"name": "bypass.json",
"content_hash": "0ac90a35515b35c2",
"has_branch_placeholder": true
},
"f008": {
"path": ".trinity/local.json",
"name": "local.json",
"content_hash": "f4e62d2d12bada2e",
"has_branch_placeholder": true
},
"f009": {
"path": ".trinity/observations.json",
"name": "observations.json",
"content_hash": "25b957960a5939a3",
"has_branch_placeholder": true
},
"f010": {
"path": ".trinity/passport.json",
"name": "passport.json",
"content_hash": "04c2ead2a4284027",
"has_branch_placeholder": true
},
"f011": {
"path": "DASHBOARD.local.json",
"name": "DASHBOARD.local.json",
"content_hash": "88360d943d4bacb6",
"has_branch_placeholder": true
},
"f012": {
"path": "README.md",
"name": "README.md",
"content_hash": "ad99517a50f96ba7",
"has_branch_placeholder": true
},
"f013": {
"path": "STATUS.local.md",
"name": "STATUS.local.md",
"content_hash": "059295ad4d4e62eb",
"has_branch_placeholder": true
},
"f014": {
"path": "apps/__init__.py",
"name": "__init__.py",
"content_hash": "41b011f487af14dc",
"has_branch_placeholder": true
},
"f015": {
"path": "apps/handlers/__init__.py",
"name": "__init__.py",
"content_hash": "e3b0c44298fc1c14",
"has_branch_placeholder": false
},
"f016": {
"path": "apps/modules/__init__.py",
"name": "__init__.py",
"content_hash": "e3b0c44298fc1c14",
"has_branch_placeholder": false
},
"f017": {
"path": "apps/plugins/__init__.py",
"name": "__init__.py",
"content_hash": "e3b0c44298fc1c14",
"has_branch_placeholder": false
},
"f018": {
"path": "apps/{{BRANCH}}.py",
"name": "{{BRANCH}}.py",
"content_hash": "fc71b424c10bb172",
"has_branch_placeholder": true
},
"f019": {
"path": "artifacts/birth_certificate.json",
"name": "birth_certificate.json",
"content_hash": "1ec401f4e397eec7",
"has_branch_placeholder": true
},
"f020": {
"path": "docs/README.md",
"name": "README.md",
"content_hash": "2434da568727499d",
"has_branch_placeholder": true
},
"f021": {
"path": "dropbox/README.md",
"name": "README.md",
"content_hash": "9e1e9b71f93b4cde",
"has_branch_placeholder": true
},
"f022": {
"path": "pytest.ini",
"name": "pytest.ini",
"content_hash": "7b39ba7bca4025c5",
"has_branch_placeholder": false
},
"f023": {
"path": "tests/__init__.py",
"name": "__init__.py",
"content_hash": "881f06bb6574d2f7",
"has_branch_placeholder": true
},
"f024": {
"path": "tests/conftest.py",
"name": "conftest.py",
"content_hash": "df147b2015e4b789",
"has_branch_placeholder": false
},
"f025": {
"path": "tools/verify_branch.py",
"name": "verify_branch.py",
"content_hash": "3a7aa5d9b479fe92",
"has_branch_placeholder": false
},
"f026": {
"path": "{{BRANCH}}_json/README.md",
"name": "README.md",
"content_hash": "e64fa555e7b8e20f",
"has_branch_placeholder": true
}
},
"directories": {
"d001": {
"path": ".ai_mail.local",
"name": ".ai_mail.local"
},
"d002": {
"path": ".ai_mail.local/sent",
"name": "sent"
},
"d003": {
"path": ".aipass",
"name": ".aipass"
},
"d004": {
"path": ".archive",
"name": ".archive"
},
"d005": {
"path": ".claude",
"name": ".claude"
},
"d006": {
"path": ".seedgo",
"name": ".seedgo"
},
"d007": {
"path": ".trinity",
"name": ".trinity"
},
"d008": {
"path": "apps",
"name": "apps"
},
"d009": {
"path": "apps/handlers",
"name": "handlers"
},
"d010": {
"path": "apps/modules",
"name": "modules"
},
"d011": {
"path": "apps/plugins",
"name": "plugins"
},
"d012": {
"path": "artifacts",
"name": "artifacts"
},
"d013": {
"path": "docs",
"name": "docs"
},
"d014": {
"path": "dropbox",
"name": "dropbox"
},
"d015": {
"path": "templates",
"name": "templates"
},
"d016": {
"path": "tests",
"name": "tests"
},
"d017": {
"path": "tools",
"name": "tools"
},
"d018": {
"path": "{{BRANCH}}_json",
"name": "{{BRANCH}}_json"
}
}
}
@@ -0,0 +1,5 @@
# Spawn Metadata
Branch lifecycle metadata for `{{BRANCHNAME}}`, managed by spawn.
Contains migration tracking and branch registry information.
@@ -0,0 +1,9 @@
# Identity & Memory
Identity and memory files for `{{BRANCHNAME}}`.
- `passport.json` — Who you are. Role, purpose, principles.
- `local.json` — Session history and key learnings.
- `observations.json` — Collaboration patterns and insights.
These files are your presence in the system. Without them, you're just a directory with code.
@@ -0,0 +1,37 @@
{
"document_metadata": {
"document_type": "session_history",
"document_name": "{{BRANCHNAME}}.LOCAL",
"version": "2.0.0",
"schema_version": "2.0.0",
"created": "{{DATE}}",
"last_updated": "{{DATE}}",
"managed_by": "{{BRANCHNAME}}",
"tags": [
"session_tracking",
"work_log",
"{{BRANCHNAME}}"
],
"limits": {
"max_sessions": 20,
"max_key_learnings": 25,
"session_summary_max_chars": 150,
"learning_value_max_chars": 200,
"note": "Auto-rollover to @memory when limits exceeded. Oldest entries trimmed first."
},
"status": {
"health": "healthy",
"last_health_check": "{{DATE}}",
"current_lines": 0
}
},
"key_learnings": {},
"sessions": [
{
"session_number": 1,
"date": "{{DATE}}",
"summary": "Branch initialized - {{BRANCHNAME}} created by aipass init.",
"status": "completed"
}
]
}
@@ -0,0 +1,36 @@
{
"document_metadata": {
"document_type": "collaboration_patterns",
"document_name": "{{BRANCHNAME}}.OBSERVATIONS",
"version": "1.0.0",
"schema_version": "1.0.0",
"created": "{{DATE}}",
"last_updated": "{{DATE}}",
"managed_by": "{{BRANCHNAME}}",
"tags": [
"collaboration",
"patterns",
"{{BRANCHNAME}}"
],
"limits": {
"max_lines": 600,
"note": "Auto-rollover when max_lines exceeded"
},
"status": {
"health": "healthy",
"current_lines": 0,
"last_health_check": "{{DATE}}"
}
},
"guidelines": {
"purpose": "Capture collaboration patterns and experiential insights over time",
"chronological_order": "Newest entries at TOP, oldest at BOTTOM - NEVER reorder"
},
"observations": [
{
"date": "{{DATE}}",
"pattern": "Branch initialized. Ready to begin capturing collaboration patterns.",
"source": "initialization"
}
]
}
@@ -0,0 +1,62 @@
{
"_warning": "AUTO-GENERATED FILE - DO NOT MANUALLY EDIT. This file is 100% automated and will be overwritten. Services update their own sections.",
"branch": "{{BRANCHNAME}}",
"last_updated": "{{DATE}}",
"quick_status": {
"new_mail": 0,
"opened_mail": 0,
"active_plans": 0,
"commons_mentions": 0,
"action_required": false,
"summary": ""
},
"sections": {
"ai_mail": {
"managed_by": "ai_mail",
"new": 0,
"opened": 0,
"total": 0,
"oldest_unread_age": null,
"last_dispatch_received": null,
"last_updated": "{{DATE}}"
},
"flow": {
"managed_by": "flow",
"active_plans": 0,
"recently_closed": [],
"last_updated": "{{DATE}}"
},
"memory_bank": {
"managed_by": "memory_bank",
"vectors_stored": 0,
"notes": {},
"last_updated": "{{DATE}}"
},
"devpulse": {
"managed_by": "devpulse",
"summary": {
"issues": 0,
"todos": 0
},
"dplan_counts": {},
"recent_activity": "",
"last_updated": "{{DATE}}"
},
"commons_activity": {
"managed_by": "the_commons",
"new_posts_since_last_visit": 0,
"new_comments_since_last_visit": 0,
"mentions": 0,
"trending": "None",
"last_checked": null,
"last_updated": "{{DATE}}"
},
"agent_status": {
"managed_by": "prax",
"active_agents": [],
"agent_count": 0,
"stale_agents": [],
"last_updated": "{{DATE}}"
}
}
}
@@ -0,0 +1,15 @@
# @{{BRANCHNAME}}
> {{BRANCHNAME}} branch
**State:** Not Started
**Last update:** None
## Milestones
-
## Current Work
-
## Known Issues
-
@@ -0,0 +1,8 @@
# Apps
Application layer for `{{BRANCHNAME}}`.
- `{{BRANCH}}.py` — Entry point. Auto-discovers and routes commands to modules.
- `modules/` — Business logic and orchestration. One module per command.
- `handlers/` — Implementation details. Called by modules, never by CLI directly.
- `plugins/` — Scheduled tasks and extensions.
@@ -0,0 +1,5 @@
# Handlers
Implementation details for `{{BRANCHNAME}}`.
Handlers do the actual work. They are called by modules, never directly by the CLI. Keep business logic in modules, implementation in handlers.
@@ -0,0 +1,5 @@
# Modules
Business logic for `{{BRANCHNAME}}`. One module per command.
Modules orchestrate work by calling handlers. They are the public API of the branch — drone routes commands here.
@@ -0,0 +1,5 @@
# Plugins
Scheduled tasks and extensions for `{{BRANCHNAME}}`.
Plugins are standalone units of work that can be scheduled via the daemon. Each plugin handles one specific recurring task.
@@ -0,0 +1,5 @@
# Artifacts
Ownership items for `{{BRANCHNAME}}`.
Contains the birth certificate and any other unique artifacts belonging to this branch. Artifacts represent milestones, achievements, and identity within the AIPass ecosystem.
@@ -0,0 +1,15 @@
{
"id": "{{CITIZEN_NUMBER}}",
"name": "{{BRANCHNAME}} Birth Certificate",
"type": "birth_certificate",
"creator": "SYSTEM",
"owner": "{{BRANCHNAME}}",
"rarity": "unique",
"description": "Official birth certificate for {{BRANCHNAME}}. Citizen #{{CITIZEN_NUMBER}}, registered using '{{PROFILE}}' template. Purpose: {{PURPOSE_BRIEF}}",
"metadata": {
"citizen_number": "{{CITIZEN_NUMBER}}",
"template": "{{PROFILE}}",
"purpose": "{{PURPOSE_BRIEF}}"
},
"created_at": "{{DATE}}"
}
@@ -0,0 +1,5 @@
# Local Docs
Untracked working documents for `{{BRANCHNAME}}`.
This directory is git-ignored. Use it for agent research dumps, reports, working files, and any documents that don't need to be committed. Subagents should organize their output here instead of scattering files across the branch.
@@ -0,0 +1,5 @@
# Sub-Agent Drops
Output directory for subagent research and investigations.
When agents are deployed to gather information, analyze code, or run diagnostics, their output goes here instead of being scattered across the branch. Keeps the workspace organized and makes it easy to find or clean up agent-generated content.
@@ -0,0 +1,5 @@
# Dropbox
Incoming file drop zone for `{{BRANCHNAME}}`.
Other branches deliver files here for `{{BRANCHNAME}}` to pick up.
@@ -0,0 +1,5 @@
# Logs
Prax log output for `{{BRANCHNAME}}`.
Operational logs written by the prax logger. Check here first when debugging unexpected behavior.
@@ -0,0 +1,5 @@
# Templates
Branch-specific templates for `{{BRANCHNAME}}`.
Any templates this branch provides to the system or uses internally. Examples: plan templates (flow), trinity templates (memory), test templates (seedgo).
@@ -0,0 +1,6 @@
# Tests
Pytest unit tests for `{{BRANCHNAME}}`.
- `conftest.py` — Shared fixtures (temp dirs, mocks, sample data).
- `test_*.py` — Test files. Standard tests cover JSON handler, CLI routing, and error resilience. Custom tests cover branch-specific domain logic.
@@ -0,0 +1,5 @@
# Tools
Standalone scripts for `{{BRANCHNAME}}`.
Diagnostic tools, prototypes, and one-off utilities. Use this directory for investigations and experiments that don't belong in the main application layer.
@@ -0,0 +1,5 @@
# {{BRANCHNAME}} JSON Logs
JSON operation logs for the `{{BRANCHNAME}}` branch.
Files here are written by `json_handler.log_operation()` during branch activity.
@@ -0,0 +1,5 @@
# Custom Config
Branch-specific configuration files for `{{BRANCHNAME}}`.
Settings, preferences, and any custom JSON configs that don't fit the standard config/data/log pattern go here.