- Fix broken API_KEY_REGEX in linter_v2.py (was invalid regex causing runtime crash) - Fix syntax error in architecture_linter.py (malformed character class) - Add --repo flag and --json output to linter_v2 - Add LinterResult class for structured programmatic access - Port v1 sovereignty rules (cloud API endpoint/provider checks) into v2 - Skip .git, node_modules, __pycache__ dirs; skip .env.example files - Add tests/test_linter.py (19 tests covering all checks) - Add .gitea/workflows/architecture-lint.yml for CI enforcement - All files pass python3 -m py_compile Refs: #437
234 lines
8.7 KiB
Python
234 lines
8.7 KiB
Python
"""Tests for Architecture Linter v2.
|
|
|
|
Validates that the linter correctly detects violations and passes clean repos.
|
|
Refs: #437 — test-backed linter.
|
|
"""
|
|
|
|
import json
|
|
import sys
|
|
import tempfile
|
|
from pathlib import Path
|
|
|
|
# Add scripts/ to path
|
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
|
|
|
from architecture_linter_v2 import Linter, LinterResult
|
|
|
|
|
|
# ── helpers ───────────────────────────────────────────────────────────
|
|
|
|
def _make_repo(tmpdir: str, files: dict[str, str], name: str = "test-repo") -> Path:
|
|
"""Create a fake repo with given files and return its path."""
|
|
repo = Path(tmpdir) / name
|
|
repo.mkdir()
|
|
for relpath, content in files.items():
|
|
p = repo / relpath
|
|
p.parent.mkdir(parents=True, exist_ok=True)
|
|
p.write_text(content)
|
|
return repo
|
|
|
|
|
|
def _run(tmpdir, files, name="test-repo"):
|
|
repo = _make_repo(tmpdir, files, name)
|
|
return Linter(str(repo)).run()
|
|
|
|
|
|
# ── clean repo passes ─────────────────────────────────────────────────
|
|
|
|
def test_clean_repo_passes():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# Test Repo\n\nThis is a clean test repo with sufficient content to pass.",
|
|
"main.py": "print('hello world')\n",
|
|
})
|
|
assert result.passed, f"Expected pass but got: {result.errors}"
|
|
assert result.violation_count == 0
|
|
|
|
|
|
# ── missing README ────────────────────────────────────────────────────
|
|
|
|
def test_missing_readme_fails():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {"main.py": "x = 1\n"})
|
|
assert not result.passed
|
|
assert any("README" in e for e in result.errors)
|
|
|
|
|
|
def test_short_readme_warns():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {"README.md": "hi\n"})
|
|
# Warnings don't fail the build
|
|
assert result.passed
|
|
assert any("short" in w.lower() for w in result.warnings)
|
|
|
|
|
|
# ── hardcoded IPs ─────────────────────────────────────────────────────
|
|
|
|
def test_hardcoded_public_ip_detected():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"server.py": "HOST = '203.0.113.42'\n",
|
|
})
|
|
assert not result.passed
|
|
assert any("203.0.113.42" in e for e in result.errors)
|
|
|
|
|
|
def test_localhost_ip_ignored():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"server.py": "HOST = '127.0.0.1'\n",
|
|
})
|
|
ip_errors = [e for e in result.errors if "IP" in e]
|
|
assert len(ip_errors) == 0
|
|
|
|
|
|
# ── API keys ──────────────────────────────────────────────────────────
|
|
|
|
def test_openai_key_detected():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"config.py": 'key = "sk-abcdefghijklmnopqrstuvwx"\n',
|
|
})
|
|
assert not result.passed
|
|
assert any("secret" in e.lower() or "key" in e.lower() for e in result.errors)
|
|
|
|
|
|
def test_aws_key_detected():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"deploy.yaml": 'aws_key: AKIAIOSFODNN7EXAMPLE\n',
|
|
})
|
|
assert not result.passed
|
|
assert any("secret" in e.lower() for e in result.errors)
|
|
|
|
|
|
def test_env_example_skipped():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
".env.example": 'OPENAI_KEY=sk-placeholder\n',
|
|
})
|
|
secret_errors = [e for e in result.errors if "secret" in e.lower()]
|
|
assert len(secret_errors) == 0
|
|
|
|
|
|
# ── sovereignty rules (v1 cloud API checks) ───────────────────────────
|
|
|
|
def test_openai_url_detected():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"app.py": 'url = "https://api.openai.com/v1/chat"\n',
|
|
})
|
|
assert not result.passed
|
|
assert any("openai" in e.lower() for e in result.errors)
|
|
|
|
|
|
def test_cloud_provider_detected():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"config.yaml": "provider: openai\n",
|
|
})
|
|
assert not result.passed
|
|
assert any("provider" in e.lower() for e in result.errors)
|
|
|
|
|
|
# ── sidecar boundary ──────────────────────────────────────────────────
|
|
|
|
def test_sovereign_keyword_in_hermes_agent_fails():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"index.py": "import mempalace\n",
|
|
}, name="hermes-agent")
|
|
assert not result.passed
|
|
assert any("sidecar" in e.lower() or "mempalace" in e.lower() for e in result.errors)
|
|
|
|
|
|
def test_sovereign_keyword_in_other_repo_ok():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"index.py": "import mempalace\n",
|
|
}, name="some-other-repo")
|
|
sidecar_errors = [e for e in result.errors if "sidecar" in e.lower()]
|
|
assert len(sidecar_errors) == 0
|
|
|
|
|
|
# ── SOUL.md canonical location ────────────────────────────────────────
|
|
|
|
def test_soul_md_required_in_timmy_config():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# timmy-config\n\nConfig repo.",
|
|
}, name="timmy-config")
|
|
assert not result.passed
|
|
assert any("SOUL.md" in e for e in result.errors)
|
|
|
|
|
|
def test_soul_md_present_in_timmy_config_ok():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# timmy-config\n\nConfig repo.",
|
|
"SOUL.md": "# Soul\n\nCanonical identity document.",
|
|
}, name="timmy-config")
|
|
soul_errors = [e for e in result.errors if "SOUL" in e]
|
|
assert len(soul_errors) == 0
|
|
|
|
|
|
def test_soul_md_in_wrong_repo_fails():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"SOUL.md": "# Soul\n\nShould not be here.",
|
|
}, name="other-repo")
|
|
assert any("canonical" in e.lower() for e in result.errors)
|
|
|
|
|
|
# ── LinterResult structure ────────────────────────────────────────────
|
|
|
|
def test_result_summary_is_string():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {"README.md": "# OK repo with enough text here\n"})
|
|
assert isinstance(result.summary(), str)
|
|
assert "PASSED" in result.summary() or "FAILED" in result.summary()
|
|
|
|
|
|
def test_result_repo_name():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
result = _run(tmp, {"README.md": "# OK\n"}, name="my-repo")
|
|
assert result.repo_name == "my-repo"
|
|
|
|
|
|
# ── invalid path ──────────────────────────────────────────────────────
|
|
|
|
def test_invalid_path_raises():
|
|
try:
|
|
Linter("/nonexistent/path/xyz")
|
|
assert False, "Should have raised FileNotFoundError"
|
|
except FileNotFoundError:
|
|
pass
|
|
|
|
|
|
# ── skip dirs ──────────────────────────────────────────────────────────
|
|
|
|
def test_git_dir_skipped():
|
|
with tempfile.TemporaryDirectory() as tmp:
|
|
repo = _make_repo(tmp, {
|
|
"README.md": "# R\n\nGood repo.",
|
|
"main.py": "x = 1\n",
|
|
})
|
|
# Create a .git/ dir with a bad file
|
|
git_dir = repo / ".git"
|
|
git_dir.mkdir()
|
|
(git_dir / "bad.py").write_text("HOST = '203.0.113.1'\n")
|
|
|
|
result = Linter(str(repo)).run()
|
|
git_errors = [e for e in result.errors if ".git" in e]
|
|
assert len(git_errors) == 0
|