From af350fe07e0e8d52833dcfe2dc025ec17f0d57f8 Mon Sep 17 00:00:00 2001 From: AIOSAI Date: Mon, 15 Jun 2026 17:45:08 -0700 Subject: [PATCH] fix(commons): canonical lowercase identity + fast test suite (green CI push) Identity: normalize branch names to lowercase at both write paths so one branch = one identity regardless of registry casing (registry has historically mixed BACKUP vs devpulse, splitting the roster into DEVPULSE/devpulse rows). - identity_ops.get_caller_branch(): _normalize_branch_name() at the single caller choke point (post/comment author writes + agent registration). - db._register_branches(): lowercase on the bulk registry seed. Verified live: post author lands lowercase, no duplicate rows; uppercase- registry branches (backup) normalize through the caller path too. Test suite: session-scoped template DB cloned per test (shutil.copy) + fast PRAGMAs (journal_mode=MEMORY, synchronous=OFF) instead of re-running schema.sql+FTS5+registry per test. 449 tests now 86s (was >120s gate timeout); full per-test isolation preserved, initialized_db interface unchanged. test_identity: assertions updated for the lowercase caller path; monkeypatch targets retargeted from the commons_identity facade to identity_ops (where get_caller_branch resolves them). .daemon/schedule.json: disabled wake-test seed (decentralized daemon contract example). seedgo 100% (37/37), 449 tests pass. Co-Authored-By: Claude Opus 4.8 (1M context) --- src/aipass/commons/.daemon/schedule.json | 18 +++ .../commons/apps/handlers/database/db.py | 5 +- .../apps/handlers/identity/identity_ops.py | 21 +++ src/aipass/commons/tests/conftest.py | 60 +++++--- src/aipass/commons/tests/test_identity.py | 138 +++++++++--------- 5 files changed, 151 insertions(+), 91 deletions(-) create mode 100644 src/aipass/commons/.daemon/schedule.json diff --git a/src/aipass/commons/.daemon/schedule.json b/src/aipass/commons/.daemon/schedule.json new file mode 100644 index 00000000..431c2f02 --- /dev/null +++ b/src/aipass/commons/.daemon/schedule.json @@ -0,0 +1,18 @@ +{ + "version": 1, + "branch": "@commons", + "jobs": [ + { + "id": "wake-test", + "enabled": false, + "schedule": { + "type": "interval", + "interval_minutes": 1 + }, + "wake": { + "fresh": true + }, + "prompt": "AUTOMATED DAEMON TEST. Do ONLY this: run drone @ai_mail email @devpulse \"DAEMON TEST FIRED\" \"Wake received from @daemon scheduler. No memory updated.\" then STOP. Do NOT run startup, do NOT update memory, do NOT do anything else." + } + ] +} diff --git a/src/aipass/commons/apps/handlers/database/db.py b/src/aipass/commons/apps/handlers/database/db.py index 1f1f0198..d7a75c39 100644 --- a/src/aipass/commons/apps/handlers/database/db.py +++ b/src/aipass/commons/apps/handlers/database/db.py @@ -354,7 +354,10 @@ def _register_branches(conn: sqlite3.Connection) -> None: branches = registry.get("branches", []) for branch in branches: - name = branch.get("name", "") + # Lowercase to keep one branch = one identity regardless of registry + # casing (registry has historically mixed BACKUP vs devpulse). Matches + # the normalization in identity_ops.get_caller_branch(). + name = branch.get("name", "").lower() if not name: continue diff --git a/src/aipass/commons/apps/handlers/identity/identity_ops.py b/src/aipass/commons/apps/handlers/identity/identity_ops.py index 2767aad3..0a7a2dea 100644 --- a/src/aipass/commons/apps/handlers/identity/identity_ops.py +++ b/src/aipass/commons/apps/handlers/identity/identity_ops.py @@ -190,6 +190,7 @@ def get_caller_branch() -> Optional[Dict[str, Any]]: if branch_root: branch_info = get_branch_info_from_registry(branch_root) if branch_info: + _normalize_branch_name(branch_info) _ensure_agent_registered(branch_info) json_handler.log_operation("caller_detected", {"branch": branch_info.get("name", "unknown")}) return branch_info @@ -199,6 +200,7 @@ def get_caller_branch() -> Optional[Dict[str, Any]]: if caller_branch_name: branch_info = get_branch_info_by_name(caller_branch_name) if branch_info: + _normalize_branch_name(branch_info) _ensure_agent_registered(branch_info) json_handler.log_operation( "caller_detected", {"branch": branch_info.get("name", "unknown"), "via": "AIPASS_CALLER_BRANCH"} @@ -215,6 +217,25 @@ def get_caller_branch() -> Optional[Dict[str, Any]]: return None +def _normalize_branch_name(branch_info: Dict[str, Any]) -> None: + """ + Lowercase the branch name so Commons identity is case-canonical. + + The AIPASS registry has historically used mixed casing for branch names + (e.g. BACKUP vs devpulse), and both the agents roster and post authorship + mirror whatever casing the registry supplies. get_caller_branch() is the + single choke point every caller identity flows through (post/comment author + writes, agent registration), so normalizing here keeps one branch = one + identity regardless of registry casing — no DEVPULSE/devpulse splits. + + Args: + branch_info: Branch dict from the registry (mutated in place). + """ + name = branch_info.get("name") + if isinstance(name, str) and name: + branch_info["name"] = name.lower() + + def _ensure_agent_registered(branch_info: Dict[str, Any]) -> None: """ Ensure the branch is registered as an agent in The Commons database. diff --git a/src/aipass/commons/tests/conftest.py b/src/aipass/commons/tests/conftest.py index 33aed05f..fd8f0b9d 100644 --- a/src/aipass/commons/tests/conftest.py +++ b/src/aipass/commons/tests/conftest.py @@ -1,16 +1,9 @@ -# ===================AIPASS==================== -# META DATA HEADER -# Name: conftest.py - The Commons test configuration -# Date: 2026-03-07 -# Version: 1.0.0 -# Category: commons/tests -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-03-07): Initial creation (FPLAN-0411) -# -# CODE STANDARDS: -# - Pytest fixtures for The Commons test suite -# - Uses temporary database for test isolation +# =================== AIPass ==================== +# Name: conftest.py +# Description: The Commons test configuration +# Version: 1.1.0 +# Created: 2026-03-07 +# Modified: 2026-06-15 # ============================================= """ @@ -21,6 +14,8 @@ and test isolation using temporary databases. """ import os +import shutil +import sqlite3 import tempfile # Redirect prax logs to temp directory during tests @@ -29,14 +24,28 @@ if "AIPASS_TEST_LOG_DIR" not in os.environ: os.environ["AIPASS_TEST_LOG_DIR"] = tempfile.mkdtemp(prefix="aipass_test_logs_") -import pytest +import logging # noqa: E402 + +import pytest # noqa: E402 + +logger = logging.getLogger(__name__) try: - from aipass.prax.apps.modules.logger import system_logger as logger + from aipass.prax.apps.modules.logger import system_logger as logger # noqa: E402, F811 except ImportError: - import logging + logger.warning("[conftest] prax logger unavailable — using stdlib logging") - logger = logging.getLogger("commons.tests") + +@pytest.fixture(scope="session") +def _template_db_path(tmp_path_factory): + """Build the initialized schema+seed DB once per session.""" + from aipass.commons.apps.modules.database import close_db, init_db + + template = tmp_path_factory.mktemp("template") / "template_commons.db" + conn = init_db(db_path=template) + conn.execute("PRAGMA wal_checkpoint(TRUNCATE)") + close_db(conn) + return template @pytest.fixture @@ -55,21 +64,26 @@ def tmp_db_path(tmp_path): @pytest.fixture -def initialized_db(tmp_db_path): +def initialized_db(_template_db_path, tmp_path): """ Provide an initialized temporary database with schema and seed data. - Creates a fresh database with all tables, default rooms, - and room personalities. Closes the connection after the test. + Copies from a session-scoped template instead of re-running init_db, + keeping the interface stable (yields sqlite3.Connection). Yields: sqlite3.Connection to the initialized test database. """ - from aipass.commons.apps.handlers.database.db import init_db, close_db + db_file = tmp_path / "test_commons.db" + shutil.copy2(str(_template_db_path), str(db_file)) - conn = init_db(db_path=tmp_db_path) + conn = sqlite3.connect(str(db_file), timeout=30) + conn.row_factory = sqlite3.Row + conn.execute("PRAGMA foreign_keys = ON") + conn.execute("PRAGMA journal_mode = MEMORY") + conn.execute("PRAGMA synchronous = OFF") yield conn - close_db(conn) + conn.close() @pytest.fixture diff --git a/src/aipass/commons/tests/test_identity.py b/src/aipass/commons/tests/test_identity.py index e99696c0..33335f08 100644 --- a/src/aipass/commons/tests/test_identity.py +++ b/src/aipass/commons/tests/test_identity.py @@ -1,17 +1,9 @@ -# ===================AIPASS==================== -# META DATA HEADER -# Name: test_identity.py - Identity Module Unit Tests -# Date: 2026-03-24 -# Version: 1.0.0 -# Category: commons/tests -# -# CHANGELOG (Max 5 entries): -# - v1.0.0 (2026-03-24): Initial creation — 14 unit tests -# -# CODE STANDARDS: -# - Pytest function style (no unittest classes) -# - Mock heavy deps (prax logger, database) -# - Tests extract_mentions, find_branch_root, resolve_display_name +# =================== AIPass ==================== +# Name: test_identity.py +# Description: Unit tests for identity module and identity_ops handler +# Version: 1.1.0 +# Created: 2026-03-24 +# Modified: 2026-06-15 # ============================================= """ @@ -21,14 +13,15 @@ Tests extract_mentions (pure regex), find_branch_root (filesystem walk), resolve_display_name, and DB-backed mention validation. """ +import logging import sqlite3 +import sys from pathlib import Path from unittest.mock import MagicMock, patch import pytest -# Mock the prax logger before importing the modules under test -import sys +logger = logging.getLogger(__name__) _mock_logger = MagicMock() _mock_logger_module = MagicMock() @@ -37,28 +30,22 @@ _mock_logger_module.system_logger = _mock_logger try: from aipass.prax.apps.modules.logger import system_logger # noqa: F401 except ImportError: + logger.warning("[test_identity] prax unavailable — injecting mock logger") sys.modules.setdefault("aipass.prax", MagicMock()) sys.modules.setdefault("aipass.prax.apps", MagicMock()) sys.modules.setdefault("aipass.prax.apps.modules", MagicMock()) sys.modules.setdefault("aipass.prax.apps.modules.logger", _mock_logger_module) -# Mock CLI console too — commons_identity imports it try: from aipass.cli.apps.modules import console # noqa: F401 except ImportError: + logger.warning("[test_identity] cli unavailable — injecting mock console") _mock_cli = MagicMock() sys.modules.setdefault("aipass.cli", _mock_cli) sys.modules.setdefault("aipass.cli.apps", MagicMock()) sys.modules.setdefault("aipass.cli.apps.modules", MagicMock()) -from aipass.commons.apps.modules.commons_identity import extract_mentions -from aipass.commons.apps.handlers.identity.identity_ops import ( - find_branch_root, - get_branch_info_by_name, - get_caller_branch, - resolve_display_name, -) -import aipass.commons.apps.handlers.identity.identity_ops as identity_ops_mod +from aipass.commons.apps.modules import commons_identity as _id_mod # noqa: E402 # --------------------------------------------------------------------------- @@ -91,13 +78,13 @@ def _patch_db_for_mentions(initialized_db: sqlite3.Connection): def test_extract_mentions_empty_string(initialized_db: sqlite3.Connection): """Empty string returns empty list.""" - result = extract_mentions("") + result = _id_mod.extract_mentions("") assert result == [] def test_extract_mentions_no_mentions(initialized_db: sqlite3.Connection): """Text without @mentions returns empty list.""" - result = extract_mentions("Hello world, no mentions here") + result = _id_mod.extract_mentions("Hello world, no mentions here") assert result == [] @@ -109,7 +96,7 @@ def test_extract_mentions_single(initialized_db: sqlite3.Connection): ) initialized_db.commit() - result = extract_mentions("Hey @drone check this out") + result = _id_mod.extract_mentions("Hey @drone check this out") assert result == ["drone"] @@ -122,13 +109,13 @@ def test_extract_mentions_multiple(initialized_db: sqlite3.Connection): ) initialized_db.commit() - result = extract_mentions("@flow and @seed please review") + result = _id_mod.extract_mentions("@flow and @seed please review") assert result == ["flow", "seed"] def test_extract_mentions_unregistered_filtered(initialized_db: sqlite3.Connection): """Mentions of agents not in the DB are filtered out.""" - result = extract_mentions("@nonexistent_branch please help") + result = _id_mod.extract_mentions("@nonexistent_branch please help") assert result == [] @@ -140,7 +127,7 @@ def test_extract_mentions_case_insensitive(initialized_db: sqlite3.Connection): ) initialized_db.commit() - result = extract_mentions("Hey @PRAX look at this") + result = _id_mod.extract_mentions("Hey @PRAX look at this") assert result == ["prax"] @@ -152,7 +139,7 @@ def test_extract_mentions_with_underscores(initialized_db: sqlite3.Connection): ) initialized_db.commit() - result = extract_mentions("Asking @ai_mail for analysis") + result = _id_mod.extract_mentions("Asking @ai_mail for analysis") assert result == ["ai_mail"] @@ -170,7 +157,7 @@ def test_find_branch_root_with_trinity(tmp_path: Path): sub = tmp_path / "apps" / "handlers" sub.mkdir(parents=True) - result = find_branch_root(sub) + result = _id_mod.find_branch_root(sub) assert result is not None assert result == tmp_path.resolve() @@ -180,7 +167,7 @@ def test_find_branch_root_no_trinity(tmp_path: Path): sub = tmp_path / "deep" / "nested" / "dir" sub.mkdir(parents=True) - result = find_branch_root(sub) + result = _id_mod.find_branch_root(sub) assert result is None @@ -190,7 +177,7 @@ def test_find_branch_root_at_start(tmp_path: Path): trinity_dir.mkdir() (trinity_dir / "passport.json").write_text("{}", encoding="utf-8") - result = find_branch_root(tmp_path) + result = _id_mod.find_branch_root(tmp_path) assert result is not None assert result == tmp_path.resolve() @@ -202,30 +189,29 @@ def test_find_branch_root_at_start(tmp_path: Path): def test_resolve_display_name_no_alias(monkeypatch: pytest.MonkeyPatch): """Falls back to branch_name when no alias is cached.""" - # Reset the alias cache to a known state - monkeypatch.setattr(identity_ops_mod, "_alias_cache", {}) - result = resolve_display_name("UNKNOWN_BRANCH") + monkeypatch.setattr("aipass.commons.apps.handlers.identity.identity_ops._alias_cache", {}) + result = _id_mod.resolve_display_name("UNKNOWN_BRANCH") assert result == "UNKNOWN_BRANCH" def test_resolve_display_name_with_alias(monkeypatch: pytest.MonkeyPatch): """Returns 'Alias (SYSTEM_NAME)' format when alias exists.""" - monkeypatch.setattr(identity_ops_mod, "_alias_cache", {"TEAM_1": "Alpha Team"}) - result = resolve_display_name("TEAM_1") + monkeypatch.setattr("aipass.commons.apps.handlers.identity.identity_ops._alias_cache", {"TEAM_1": "Alpha Team"}) + result = _id_mod.resolve_display_name("TEAM_1") assert result == "Alpha Team (TEAM_1)" def test_resolve_display_name_compact(monkeypatch: pytest.MonkeyPatch): """Compact mode returns alias only, no parenthesized system name.""" - monkeypatch.setattr(identity_ops_mod, "_alias_cache", {"TEAM_1": "Alpha Team"}) - result = resolve_display_name("TEAM_1", compact=True) + monkeypatch.setattr("aipass.commons.apps.handlers.identity.identity_ops._alias_cache", {"TEAM_1": "Alpha Team"}) + result = _id_mod.resolve_display_name("TEAM_1", compact=True) assert result == "Alpha Team" def test_resolve_display_name_compact_no_alias(monkeypatch: pytest.MonkeyPatch): """Compact mode without alias still falls back to branch_name.""" - monkeypatch.setattr(identity_ops_mod, "_alias_cache", {}) - result = resolve_display_name("RAW_NAME", compact=True) + monkeypatch.setattr("aipass.commons.apps.handlers.identity.identity_ops._alias_cache", {}) + result = _id_mod.resolve_display_name("RAW_NAME", compact=True) assert result == "RAW_NAME" @@ -246,9 +232,12 @@ def test_get_branch_info_by_name_found(tmp_path: Path, monkeypatch: pytest.Monke } reg_file = tmp_path / "AIPASS_REGISTRY.json" reg_file.write_text(json_mod.dumps(registry), encoding="utf-8") - monkeypatch.setattr(identity_ops_mod, "BRANCH_REGISTRY_PATH", reg_file) + monkeypatch.setattr( + "aipass.commons.apps.handlers.identity.identity_ops.BRANCH_REGISTRY_PATH", + reg_file, + ) - result = get_branch_info_by_name("drone") + result = _id_mod.get_branch_info_by_name("drone") assert result is not None assert result["name"] == "DRONE" assert result["email"] == "@drone" @@ -261,9 +250,12 @@ def test_get_branch_info_by_name_case_insensitive(tmp_path: Path, monkeypatch: p registry = {"branches": [{"name": "FLOW", "path": "src/aipass/flow"}]} reg_file = tmp_path / "AIPASS_REGISTRY.json" reg_file.write_text(json_mod.dumps(registry), encoding="utf-8") - monkeypatch.setattr(identity_ops_mod, "BRANCH_REGISTRY_PATH", reg_file) + monkeypatch.setattr( + "aipass.commons.apps.handlers.identity.identity_ops.BRANCH_REGISTRY_PATH", + reg_file, + ) - result = get_branch_info_by_name("Flow") + result = _id_mod.get_branch_info_by_name("Flow") assert result is not None assert result["name"] == "FLOW" @@ -275,16 +267,22 @@ def test_get_branch_info_by_name_not_found(tmp_path: Path, monkeypatch: pytest.M registry = {"branches": [{"name": "DRONE", "path": "src/aipass/drone"}]} reg_file = tmp_path / "AIPASS_REGISTRY.json" reg_file.write_text(json_mod.dumps(registry), encoding="utf-8") - monkeypatch.setattr(identity_ops_mod, "BRANCH_REGISTRY_PATH", reg_file) + monkeypatch.setattr( + "aipass.commons.apps.handlers.identity.identity_ops.BRANCH_REGISTRY_PATH", + reg_file, + ) - result = get_branch_info_by_name("nonexistent") + result = _id_mod.get_branch_info_by_name("nonexistent") assert result is None def test_get_branch_info_by_name_missing_file(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): """Returns None when registry file doesn't exist.""" - monkeypatch.setattr(identity_ops_mod, "BRANCH_REGISTRY_PATH", tmp_path / "nope.json") - result = get_branch_info_by_name("DRONE") + monkeypatch.setattr( + "aipass.commons.apps.handlers.identity.identity_ops.BRANCH_REGISTRY_PATH", + tmp_path / "nope.json", + ) + result = _id_mod.get_branch_info_by_name("DRONE") assert result is None @@ -307,17 +305,19 @@ def test_get_caller_branch_uses_caller_branch_env( registry = {"branches": [{"name": "DRONE", "path": "src/aipass/drone", "email": "@drone"}]} reg_file = tmp_path / "AIPASS_REGISTRY.json" reg_file.write_text(json_mod.dumps(registry), encoding="utf-8") - monkeypatch.setattr(identity_ops_mod, "BRANCH_REGISTRY_PATH", reg_file) + monkeypatch.setattr( + "aipass.commons.apps.handlers.identity.identity_ops.BRANCH_REGISTRY_PATH", + reg_file, + ) - # CWD with no .trinity/ — simulates running from project root no_branch_dir = tmp_path / "somewhere" no_branch_dir.mkdir() monkeypatch.setenv("AIPASS_CALLER_CWD", str(no_branch_dir)) monkeypatch.setenv("AIPASS_CALLER_BRANCH", "drone") - result = get_caller_branch() + result = _id_mod.get_caller_branch() assert result is not None - assert result["name"] == "DRONE" + assert result["name"] == "drone" mock_register.assert_called_once() @@ -332,33 +332,37 @@ def test_get_caller_branch_prefers_cwd_over_env( """CWD-based detection takes priority over AIPASS_CALLER_BRANCH.""" import json as json_mod - # Set up a branch directory with .trinity/ trinity = tmp_path / ".trinity" trinity.mkdir() (trinity / "passport.json").write_text("{}", encoding="utf-8") registry = { "branches": [ - {"name": "FLOW", "path": str(tmp_path.relative_to(tmp_path.parent.parent)), "email": "@flow"}, + { + "name": "FLOW", + "path": str(tmp_path.relative_to(tmp_path.parent.parent)), + "email": "@flow", + }, ] } reg_file = tmp_path / "AIPASS_REGISTRY.json" reg_file.write_text(json_mod.dumps(registry), encoding="utf-8") - monkeypatch.setattr(identity_ops_mod, "BRANCH_REGISTRY_PATH", reg_file) + monkeypatch.setattr( + "aipass.commons.apps.handlers.identity.identity_ops.BRANCH_REGISTRY_PATH", + reg_file, + ) - # CWD is inside the branch monkeypatch.setenv("AIPASS_CALLER_CWD", str(tmp_path)) - # Also set CALLER_BRANCH to something different — should NOT be used monkeypatch.setenv("AIPASS_CALLER_BRANCH", "DRONE") - # Need to patch get_branch_info_from_registry to return for our tmp_path - with patch.object( - identity_ops_mod, "get_branch_info_from_registry", return_value={"name": "FLOW", "email": "@flow"} + with patch( + "aipass.commons.apps.handlers.identity.identity_ops.get_branch_info_from_registry", + return_value={"name": "FLOW", "email": "@flow"}, ): - result = get_caller_branch() + result = _id_mod.get_caller_branch() assert result is not None - assert result["name"] == "FLOW" # CWD-based, not DRONE from env + assert result["name"] == "flow" @patch("aipass.commons.apps.handlers.identity.identity_ops.json_handler") @@ -373,5 +377,5 @@ def test_get_caller_branch_returns_none_when_no_detection( monkeypatch.setenv("AIPASS_CALLER_CWD", str(no_branch_dir)) monkeypatch.delenv("AIPASS_CALLER_BRANCH", raising=False) - result = get_caller_branch() + result = _id_mod.get_caller_branch() assert result is None