fix: #692 builder->aipass_framework legacy migration + birth-cert template; #694 test-order pollution (prax fixture scope + skills hermetic tests). Verified: repro pair passes, 1576 tests green, Vera dry-run plans exactly 2 migrations zero writes.

This commit is contained in:
AIOSAI
2026-07-12 00:02:32 -07:00
parent 002d83da8c
commit e0811fcf93
11 changed files with 202 additions and 81 deletions
+35
View File
@@ -9,6 +9,41 @@ PyPI version — not the changelog header.
---
## [2026-07-12]
### Fixed
- **Legacy `builder` citizen_class migration + birth-certificate template
(fixes #692).** `builder` was renamed to `aipass_framework` on 2026-07-01
(13463c0c) as a pure rename, but passports minted pre-rename kept the retired
name, and the seedgo Architecture checker requires
`spawn/templates/<citizen_class>/` — hard-capping those citizens below 100%
(Vera Studio's @vera/@writer stuck at 99%; same legacy class found in 6
external projects). @spawn completed the rename instead of resurrecting a
`builder` template: `sync-registry --fix` now migrates the exact value
`builder` → `aipass_framework` in passports (idempotent, dry-run safe, 3 new
tests), so external projects self-heal via `aipass doctor --fix`. Also fixed
the template leftover that kept minting the retired name:
`birth_certificate.json` now renders `{{CITIZEN_CLASS}}` like the passport
does. Verified: dry-run against Vera Studio's live registry plans exactly the
two migrations with zero writes; spawn 347 tests green. #695 closed won't-fix
(armed Monitor-tool watchdog is the dispatch indicator; always-arm is the
rule).
- **Order-dependent `test_missing_file` + skills test litter (fixes #694).**
Root cause was @prax's `json_handler_module` fixture popping EVERY branch's
json_handler from `sys.modules` (never restored), orphaning the module object
@skills' conftest had patched — `test_missing_file` then re-imported a fresh
module pointed at the real `skills_json/`, planted `ghost_config.json`, and
failed on it every later full-repo run (the only failure in an 11k-test
sweep). @prax scoped the eviction to `aipass.prax.*` via
`monkeypatch.delitem` (auto-restore). @skills made all 4 resilience tests
hermetic (patch `SKILLS_JSON_DIR` → `tmp_path` inside the test body, immune
to sys.modules state), fully-qualified the legacy bare `skills.`
`BRANCH_MODULE` in 3 test files (the source of the remaining litter), and
fixed a latent wrong-variable assert. Verified: original failing pair now
passes both orders, prax+skills+spawn 1576 tests green, `skills_json/` stays
clean after a full run.
## [2026-07-11]
### Added
+3 -3
View File
@@ -51,10 +51,10 @@ def cleanup_temp(tmp_path):
@pytest.fixture
def json_handler_module(mock_prax_infrastructure, tmp_path, monkeypatch):
"""Import json_handler with mocked dependencies and temp directories."""
# Remove cached module to get fresh import
# Remove cached prax json_handler modules to get fresh import (scoped to prax only)
for key in list(sys.modules.keys()):
if "json_handler" in key and "aipass" in key:
sys.modules.pop(key, None)
if "json_handler" in key and key.startswith("aipass.prax."):
monkeypatch.delitem(sys.modules, key)
mod = MagicMock()
mod.PRAX_JSON_DIR = tmp_path / "prax_json"
+1 -1
View File
@@ -22,7 +22,7 @@ import json
from pathlib import Path
BRANCH_MODULE = "skills"
BRANCH_MODULE = "aipass.skills"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
@@ -3,7 +3,7 @@
# Description: Error Resilience Tests for skills branch
# Version: 1.0.0
# Created: 2026-03-28
# Modified: 2026-03-28
# Modified: 2026-07-11
# =============================================
"""
@@ -16,6 +16,7 @@ Covers 4 tests:
import importlib
import json
from pathlib import Path
from unittest.mock import patch
BRANCH_MODULE = "aipass.skills"
@@ -23,7 +24,7 @@ _json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
def _import_handler():
"""Import json_handler."""
"""Import json_handler, re-resolving from sys.modules."""
return importlib.import_module(_json_mod_path)
@@ -32,59 +33,57 @@ def _import_handler():
# ============================================================================
def test_missing_file() -> None:
def test_missing_file(tmp_path: Path) -> None:
"""Loading a non-existent file returns a graceful default, not a crash."""
handler = _import_handler()
target = handler.get_json_path("ghost", "config")
assert not target.exists()
with patch.object(handler, "SKILLS_JSON_DIR", tmp_path):
target = handler.get_json_path("ghost", "config")
assert not target.exists()
try:
result = handler.load_json("ghost", "config")
except FileNotFoundError:
return
try:
result = handler.load_json("ghost", "config")
except FileNotFoundError:
return
assert result is not None
assert isinstance(result, dict)
assert result is not None
assert isinstance(result, dict)
def test_corrupt_json() -> None:
def test_corrupt_json(tmp_path: Path) -> None:
"""Corrupt JSON on disk is handled gracefully -- file is regenerated."""
handler = _import_handler()
json_dir = handler.SKILLS_JSON_DIR
json_dir.mkdir(parents=True, exist_ok=True)
target = handler.get_json_path("corrupt", "data")
target.write_bytes(b"\x00\x01NOT-JSON{{{broken")
with patch.object(handler, "SKILLS_JSON_DIR", tmp_path):
target = handler.get_json_path("corrupt", "data")
target.write_bytes(b"\x00\x01NOT-JSON{{{broken")
result = handler.ensure_json_exists("corrupt", "data")
assert result is True
result = handler.ensure_json_exists("corrupt", "data")
assert result is True
raw = target.read_text(encoding="utf-8")
data = json.loads(raw)
assert isinstance(data, dict)
assert "created" in data
assert "last_updated" in data
raw = target.read_text(encoding="utf-8")
data = json.loads(raw)
assert isinstance(data, dict)
assert "created" in data
assert "last_updated" in data
def test_empty_file() -> None:
def test_empty_file(tmp_path: Path) -> None:
"""An empty file (0 bytes) is handled gracefully."""
handler = _import_handler()
json_dir = handler.SKILLS_JSON_DIR
json_dir.mkdir(parents=True, exist_ok=True)
target = handler.get_json_path("empty", "log")
target.write_text("", encoding="utf-8")
with patch.object(handler, "SKILLS_JSON_DIR", tmp_path):
target = handler.get_json_path("empty", "log")
target.write_text("", encoding="utf-8")
result = handler.ensure_json_exists("empty", "log")
assert result is True
result = handler.ensure_json_exists("empty", "log")
assert result is True
raw = target.read_text(encoding="utf-8")
data = json.loads(raw)
assert isinstance(data, list)
raw = target.read_text(encoding="utf-8")
data = json.loads(raw)
assert isinstance(data, list)
def test_nonexistent_dir(tmp_path: Path) -> None:
"""Missing parent directory is handled gracefully."""
handler = _import_handler()
from unittest.mock import patch
nested_dir = tmp_path / "does_not_exist" / "nested"
assert not nested_dir.exists()
@@ -3,7 +3,7 @@
# Description: Init/Provisioning Tests for skills branch
# Version: 1.0.0
# Created: 2026-03-28
# Modified: 2026-03-28
# Modified: 2026-07-11
# =============================================
"""
@@ -21,7 +21,7 @@ from unittest.mock import patch
import pytest
BRANCH_MODULE = "skills"
BRANCH_MODULE = "aipass.skills"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
@@ -35,21 +35,21 @@ def _import_handler():
# ============================================================================
def test_creates_expected_files() -> None:
def test_creates_expected_files(tmp_path: Path) -> None:
"""ensure_json_exists creates expected files on disk."""
handler = _import_handler()
json_dir = handler.SKILLS_JSON_DIR
for json_type in ("config", "data", "log"):
result = handler.ensure_json_exists("prov_mod", json_type)
assert result is True
with patch.object(handler, "SKILLS_JSON_DIR", tmp_path):
for json_type in ("config", "data", "log"):
result = handler.ensure_json_exists("prov_mod", json_type)
assert result is True
expected = json_dir / f"prov_mod_{json_type}.json"
assert expected.exists()
expected = tmp_path / f"prov_mod_{json_type}.json"
assert expected.exists()
raw = expected.read_text(encoding="utf-8")
parsed = json.loads(raw)
assert parsed is not None
raw = expected.read_text(encoding="utf-8")
parsed = json.loads(raw)
assert parsed is not None
def test_auto_creates_directory(tmp_path: Path) -> None:
@@ -68,41 +68,41 @@ def test_auto_creates_directory(tmp_path: Path) -> None:
pytest.skip("Branch does not auto-create missing directories")
def test_no_overwrite_on_second_call() -> None:
def test_no_overwrite_on_second_call(tmp_path: Path) -> None:
"""Second call must not overwrite existing data (no_overwrite idempotency)."""
handler = _import_handler()
json_dir = handler.SKILLS_JSON_DIR
json_dir.mkdir(parents=True, exist_ok=True)
handler.ensure_json_exists("idem_mod", "data")
with patch.object(handler, "SKILLS_JSON_DIR", tmp_path):
handler.ensure_json_exists("idem_mod", "data")
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")
target = tmp_path / "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")
handler.ensure_json_exists("idem_mod", "data")
handler.ensure_json_exists("idem_mod", "data")
after = json.loads(target.read_text(encoding="utf-8"))
assert after.get("custom_field") == "do_not_overwrite"
after = json.loads(target.read_text(encoding="utf-8"))
assert after.get("custom_field") == "do_not_overwrite"
def test_returns_dict_with_expected_keys() -> None:
def test_returns_dict_with_expected_keys(tmp_path: Path) -> None:
"""Provisioned files contain the correct structure keys."""
handler = _import_handler()
handler.ensure_json_exists("key_mod", "config")
config = handler.load_json("key_mod", "config")
assert isinstance(config, dict)
assert "module_name" in config
assert "version" in config
with patch.object(handler, "SKILLS_JSON_DIR", tmp_path):
handler.ensure_json_exists("key_mod", "config")
config = handler.load_json("key_mod", "config")
assert isinstance(config, dict)
assert "module_name" in config
assert "version" in config
handler.ensure_json_exists("key_mod", "data")
data = handler.load_json("key_mod", "data")
assert isinstance(data, dict)
assert "created" in data
assert "last_updated" in data
handler.ensure_json_exists("key_mod", "data")
data = handler.load_json("key_mod", "data")
assert isinstance(data, dict)
assert "created" in data
assert "last_updated" in data
handler.ensure_json_exists("key_mod", "log")
log = handler.load_json("key_mod", "log")
assert isinstance(log, list)
handler.ensure_json_exists("key_mod", "log")
log = handler.load_json("key_mod", "log")
assert isinstance(log, list)
+1 -1
View File
@@ -26,7 +26,7 @@ import pytest
# Import helper
# ---------------------------------------------------------------------------
BRANCH_MODULE = "skills"
BRANCH_MODULE = "aipass.skills"
_json_mod_path = f"{BRANCH_MODULE}.apps.handlers.json.json_handler"
+1 -1
View File
@@ -177,7 +177,7 @@ spawn/
## Tests
**344 tests | 0 skipped | 0 failed** across 14 test files:
**347 tests | 0 skipped | 0 failed** across 14 test files:
| File | Focus |
|------|-------|
@@ -608,6 +608,29 @@ def fix_owner_identity(registry_path=None, dry_run=False):
actions.extend(passport_actions)
# --- migrate legacy citizen_class "builder" → "aipass_framework" ---
for branch in branches:
branch_dir = project_root / branch.get("path", "")
passport_path = branch_dir / ".trinity" / "passport.json"
if not passport_path.exists():
continue
try:
passport = json.loads(passport_path.read_text(encoding="utf-8"))
except (json.JSONDecodeError, IOError) as e:
logger.warning("[fix-identity] Cannot read passport for migration: %s", e)
continue
current_class = passport.get("identity", {}).get("citizen_class", "")
if current_class != "builder":
continue
passport.setdefault("identity", {})["citizen_class"] = "aipass_framework"
if not dry_run:
try:
passport_path.write_text(json.dumps(passport, indent=2, ensure_ascii=False), encoding="utf-8")
except IOError as e:
logger.warning("[fix-identity] Failed to migrate citizen_class for %s: %s", branch.get("name", "?"), e)
continue
actions.append(f"Migrate citizen_class for {branch.get('name', '?')}: builder → aipass_framework")
applied = False
if registry_changed and not dry_run:
applied = save_registry(registry_path, reg_data)
@@ -616,6 +639,9 @@ def fix_owner_identity(registry_path=None, dry_run=False):
else:
logger.error("[fix-identity] Failed to save registry")
if not registry_changed and actions and not dry_run:
applied = True
if dry_run and actions:
logger.info("[fix-identity] Dry-run: %d action(s) planned", len(actions))
@@ -301,7 +301,7 @@
"path": "artifacts/README.md"
},
"f029": {
"content_hash": "0b6e4319781e",
"content_hash": "b089aef46e8c",
"has_branch_placeholder": false,
"name": "birth_certificate.json",
"path": "artifacts/birth_certificate.json"
@@ -429,7 +429,7 @@
},
"metadata": {
"description": "Template file tracking registry for ID-based updates",
"last_updated": "2026-07-11",
"last_updated": "2026-07-12",
"version": "1.0.0"
}
}
@@ -5,10 +5,10 @@
"creator": "SYSTEM",
"owner": "{{BRANCHNAME}}",
"rarity": "unique",
"description": "Official birth certificate for {{BRANCHNAME}}. Citizen #{{CITIZEN_NUMBER}}, builder class. Purpose: {{PURPOSE_BRIEF}}",
"description": "Official birth certificate for {{BRANCHNAME}}. Citizen #{{CITIZEN_NUMBER}}, {{CITIZEN_CLASS}} class. Purpose: {{PURPOSE_BRIEF}}",
"metadata": {
"citizen_number": "{{CITIZEN_NUMBER}}",
"citizen_class": "builder",
"citizen_class": "{{CITIZEN_CLASS}}",
"purpose": "{{PURPOSE_BRIEF}}"
},
"created_at": "{{DATE}}"
@@ -454,6 +454,67 @@ class TestFixOwnerIdentity:
assert result["applied"] is False
class TestLegacyCitizenClassMigration:
"""Tests for builder → aipass_framework passport migration."""
def test_migrates_builder_to_aipass_framework(self, tmp_path):
from aipass.spawn.apps.handlers.sync_registry_ops import fix_owner_identity
_make_branch(tmp_path, "vera", "src/vera", citizen_class="builder", passport_rid="proj-id")
_make_branch(tmp_path, "writer", "src/writer", citizen_class="builder", passport_rid="proj-id")
_make_branch(tmp_path, "modern", "src/modern", citizen_class="aipass_framework", passport_rid="proj-id")
reg = _write_registry(
tmp_path,
metadata={"version": "1.0.0", "last_updated": "2026-07-11", "id": "proj-id"},
branches=[
_entry("vera", "src/vera", owner=True, registry_id="uid-v"),
_entry("writer", "src/writer", registry_id="uid-w"),
_entry("modern", "src/modern", registry_id="uid-m"),
],
)
result = fix_owner_identity(registry_path=reg)
assert any("Migrate" in a and "vera" in a for a in result["actions"])
assert any("Migrate" in a and "writer" in a for a in result["actions"])
assert not any("modern" in a and "Migrate" in a for a in result["actions"])
vera_passport = json.loads((tmp_path / "src/vera/.trinity/passport.json").read_text(encoding="utf-8"))
assert vera_passport["identity"]["citizen_class"] == "aipass_framework"
modern_passport = json.loads((tmp_path / "src/modern/.trinity/passport.json").read_text(encoding="utf-8"))
assert modern_passport["identity"]["citizen_class"] == "aipass_framework"
def test_migration_idempotent(self, tmp_path):
from aipass.spawn.apps.handlers.sync_registry_ops import fix_owner_identity
_make_branch(tmp_path, "alpha", "src/alpha", citizen_class="builder", passport_rid="proj-id")
reg = _write_registry(
tmp_path,
metadata={"version": "1.0.0", "last_updated": "2026-07-11", "id": "proj-id"},
branches=[_entry("alpha", "src/alpha", owner=True, registry_id="uid-a")],
)
fix_owner_identity(registry_path=reg)
result2 = fix_owner_identity(registry_path=reg)
assert not any("Migrate" in a for a in result2["actions"])
def test_migration_dry_run_no_write(self, tmp_path):
from aipass.spawn.apps.handlers.sync_registry_ops import fix_owner_identity
_make_branch(tmp_path, "alpha", "src/alpha", citizen_class="builder", passport_rid="proj-id")
reg = _write_registry(
tmp_path,
metadata={"version": "1.0.0", "last_updated": "2026-07-11", "id": "proj-id"},
branches=[_entry("alpha", "src/alpha", owner=True, registry_id="uid-a")],
)
result = fix_owner_identity(registry_path=reg, dry_run=True)
assert any("Migrate" in a for a in result["actions"])
passport = json.loads((tmp_path / "src/alpha/.trinity/passport.json").read_text(encoding="utf-8"))
assert passport["identity"]["citizen_class"] == "builder"
class TestAdoptCallsEnsureOwner:
"""Test that _adopt_existing calls ensure_project_has_owner."""