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:
co-authored by
Claude Opus 4.6
@devpulse
parent
f19f7ac12d
commit
f49f9e2a06
+26
@@ -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"
|
||||
@@ -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())
|
||||
],
|
||||
}
|
||||
@@ -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.
|
||||
Reference in New Issue
Block a user