Initial curated RepoPulse source from 2524ccc3c1c27739996dccb2b7992989f88cd26d

This commit is contained in:
Public Source Publisher committed 2026-10-01 01:34:52 +02:00
commit 0adf8ef5d9
90 files changed
+14315

No files matched your search

+122
View File
@@ -0,0 +1,122 @@
from pathlib import Path
from threading import BoundedSemaphore
import pytest
from fastapi.testclient import TestClient
from app import main as main_module
from app.store import ScanStore
def _repository(root: Path) -> Path:
repository = root / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
(repository / "LICENSE").write_text("MIT\n", encoding="utf-8")
(repository / ".gitignore").write_text(".env\n", encoding="utf-8")
(repository / "app.py").write_text("print('hello')\n", encoding="utf-8")
return repository
def test_scan_api_respects_root_and_exports_reports(tmp_path: Path) -> None:
repository = _repository(tmp_path)
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
client = TestClient(main_module.app)
first_response = client.post("/api/v1/scans", json={"path": "repo"})
assert first_response.status_code == 201
first = first_response.json()
assert first["repository_path"] == str(repository.resolve())
html = client.get(f"/api/v1/scans/{first['id']}/export?format=html")
assert html.status_code == 200
assert "RepoPulse report" in html.text
assert "repo" in html.text
blocked = client.post("/api/v1/scans", json={"path": str(tmp_path.parent)})
assert blocked.status_code == 422
def test_scan_api_comparison_delta_and_trend(tmp_path: Path) -> None:
repository = _repository(tmp_path)
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
client = TestClient(main_module.app)
first = client.post("/api/v1/scans", json={"path": "repo"}).json()
(repository / "tests").mkdir()
(repository / "tests" / "test_app.py").write_text("def test_ok():\n assert True\n", encoding="utf-8")
second = client.post("/api/v1/scans", json={"path": "repo"}).json()
comparison = client.get(f"/api/v1/scans/{second['id']}/comparison")
assert comparison.status_code == 200
assert comparison.json()["previous_scan_id"] == first["id"]
delta = client.get(f"/api/v1/scans/{second['id']}/delta")
assert delta.status_code == 200
assert delta.json()["previous_health_score"] == first["health_score"]
assert delta.json()["previous_finding_count"] == len(first["findings"])
trend = client.get(f"/api/v1/trend?path={repository}")
assert trend.status_code == 200
assert [item["scan_id"] for item in trend.json()] == [first["id"], second["id"]]
def test_scan_api_rejects_when_scan_capacity_is_busy(tmp_path: Path) -> None:
_repository(tmp_path)
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
client = TestClient(main_module.app)
assert main_module.SCAN_SLOTS.acquire(blocking=False)
try:
response = client.post("/api/v1/scans", json={"path": "repo"})
finally:
main_module.SCAN_SLOTS.release()
assert response.status_code == 429
assert response.headers["retry-after"] == "2"
def test_scan_api_rejects_symlinked_repository_outside_root(tmp_path: Path) -> None:
outside = tmp_path.parent / f"{tmp_path.name}-outside-repository"
outside.mkdir()
(outside / "app.py").write_text("print('outside')\n", encoding="utf-8")
linked = tmp_path / "linked-repository"
try:
linked.symlink_to(outside, target_is_directory=True)
except (NotImplementedError, OSError):
pytest.skip("directory symlinks are unavailable in this environment")
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
client = TestClient(main_module.app)
response = client.post("/api/v1/scans", json={"path": "linked-repository"})
assert response.status_code == 422
def test_mutating_api_requires_operator_token_only_when_configured(tmp_path: Path, monkeypatch) -> None:
_repository(tmp_path)
main_module.SCAN_ROOT = str(tmp_path)
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.SCAN_SLOTS = BoundedSemaphore(1)
monkeypatch.setenv("REPOPULSE_OPERATOR_TOKEN", "test-operator-token")
client = TestClient(main_module.app)
assert client.get("/api/v1/meta").status_code == 200
denied = client.post("/api/v1/scans", json={"path": "repo"})
assert denied.status_code == 401
allowed = client.post(
"/api/v1/scans",
json={"path": "repo"},
headers={"X-RepoPulse-Token": "test-operator-token"},
)
assert allowed.status_code == 201
+130
View File
@@ -0,0 +1,130 @@
from __future__ import annotations
import hashlib
import hmac
import json
from fastapi.testclient import TestClient
from app import main as main_module
from app.gitea import GiteaClient
from app.models import Repository
from app.store import ScanStore
def test_repository_catalog_and_durable_scan_job_contract(tmp_path) -> None:
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.job_runner = None
main_module.gitea_client = GiteaClient("http://gitea.local", "test-token", transport=lambda *_: (500, b""))
repository = Repository(
id="gitea:64",
provider="gitea",
provider_repository_id="64",
owner="Jens",
name="RepoPulse",
full_name="Jens/RepoPulse",
default_branch="main",
latest_revision="0123456789abcdef0123456789abcdef01234567",
)
main_module.store.upsert_repository(repository)
client = TestClient(main_module.app)
catalog = client.get("/api/v1/repositories")
assert catalog.status_code == 200
assert catalog.json()[0]["id"] == "gitea:64"
assert "latest_scan" not in catalog.json()[0]
queued = client.post("/api/v1/repositories/gitea%3A64/scans", json={"force": False})
assert queued.status_code == 202
job = queued.json()
assert job["repository_id"] == "gitea:64"
assert job["status"] == "queued"
duplicate = client.post("/api/v1/repositories/gitea%3A64/scans", json={"force": False})
assert duplicate.status_code == 202
assert duplicate.json()["id"] == job["id"]
assert duplicate.json()["deduplicated"] is True
detail = client.get("/api/v1/repositories/gitea%3A64")
assert detail.status_code == 200
assert detail.json()["latest_scan"] is None
assert client.get(f"/api/v1/scan-jobs/{job['id']}").json()["status"] == "queued"
def test_signed_gitea_webhook_only_schedules_default_branch_push(
tmp_path, monkeypatch
) -> None:
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.job_runner = None
main_module.gitea_client = GiteaClient("http://gitea.local", "test-token", transport=lambda *_: (500, b""))
repository = Repository(
id="gitea:64",
provider="gitea",
provider_repository_id="64",
owner="Jens",
name="RepoPulse",
full_name="Jens/RepoPulse",
default_branch="main",
latest_revision="0123456789abcdef0123456789abcdef01234567",
)
main_module.store.upsert_repository(repository)
monkeypatch.setenv("REPOPULSE_GITEA_WEBHOOK_SECRET", "webhook-secret")
payload = {
"repository": {"id": 64},
"ref": "refs/heads/main",
"after": "abcdef0123456789abcdef0123456789abcdef01",
}
body = json.dumps(payload).encode()
signature = hmac.new(b"webhook-secret", body, hashlib.sha256).hexdigest()
client = TestClient(main_module.app)
response = client.post(
"/api/v1/webhooks/gitea",
content=body,
headers={"X-Gitea-Signature": signature, "X-Gitea-Event": "push"},
)
assert response.status_code == 200
assert response.json()["job_id"]
assert main_module.store.get_repository("gitea:64").latest_revision == payload["after"]
wrong_signature = client.post(
"/api/v1/webhooks/gitea",
content=body,
headers={"X-Gitea-Signature": "0" * 64, "X-Gitea-Event": "push"},
)
assert wrong_signature.status_code == 401
feature_payload = payload | {"ref": "refs/heads/feature"}
feature_body = json.dumps(feature_payload).encode()
feature_signature = hmac.new(b"webhook-secret", feature_body, hashlib.sha256).hexdigest()
feature = client.post(
"/api/v1/webhooks/gitea",
content=feature_body,
headers={"X-Gitea-Signature": feature_signature, "X-Gitea-Event": "push"},
)
assert feature.status_code == 200
assert feature.json()["job_id"] is None
def test_meta_exposes_operational_counts_without_connector_token(tmp_path) -> None:
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.job_runner = None
main_module.gitea_client = GiteaClient("http://gitea.local", "never-return-this-token")
main_module.store.upsert_repository(
Repository(
id="gitea:64",
provider="gitea",
provider_repository_id="64",
owner="Jens",
name="RepoPulse",
full_name="Jens/RepoPulse",
latest_revision="0123456789abcdef0123456789abcdef01234567",
)
)
response = TestClient(main_module.app).get("/api/v1/meta")
assert response.status_code == 200
assert response.json()["repository_count"] == 1
assert response.json()["repository_states"]["never_scanned"] == 1
assert "never-return-this-token" not in response.text
+98
View File
@@ -0,0 +1,98 @@
from pathlib import Path
from app.architecture import build_architecture
def test_architecture_resolves_python_and_relative_typescript_imports(tmp_path: Path) -> None:
(tmp_path / "src").mkdir()
(tmp_path / "web").mkdir()
(tmp_path / "src" / "core.py").write_text("VALUE = 1\n", encoding="utf-8")
(tmp_path / "src" / "app.py").write_text("import src.core\n", encoding="utf-8")
(tmp_path / "web" / "util.ts").write_text("export const value = 1\n", encoding="utf-8")
(tmp_path / "web" / "app.ts").write_text("import { value } from './util'\n", encoding="utf-8")
(tmp_path / "web" / "side-effect.ts").write_text("import './util.js'\n", encoding="utf-8")
source_texts = {
"src/core.py": "VALUE = 1\n",
"src/app.py": "import src.core\n",
"web/util.ts": "export const value = 1\n",
"web/app.ts": "import { value } from './util'\n",
"web/side-effect.ts": "import './util.js'\n",
}
languages = {
"src/core.py": "Python",
"src/app.py": "Python",
"web/util.ts": "TypeScript",
"web/app.ts": "TypeScript",
"web/side-effect.ts": "TypeScript",
}
loc = {path: 1 for path in source_texts}
graph, hotspots = build_architecture(
tmp_path,
source_texts,
languages,
loc,
{path: 0 for path in source_texts},
{"src/core.py": 100, "src/app.py": 200},
)
pairs = {(edge.source, edge.target) for edge in graph.edges}
assert ("src/app.py", "src/core.py") in pairs
assert ("web/app.ts", "web/util.ts") in pairs
assert ("web/side-effect.ts", "web/util.ts") in pairs
assert {node.id for node in graph.nodes} == set(source_texts)
assert hotspots[0].path in {"src/app.py", "src/core.py"}
def test_architecture_edge_bound_is_explicit(tmp_path: Path) -> None:
source_texts = {
"src/a.py": "import src.b\n",
"src/b.py": "import src.c\n",
"src/c.py": "VALUE = 1\n",
}
languages = {path: "Python" for path in source_texts}
graph, _ = build_architecture(
tmp_path,
source_texts,
languages,
{path: 1 for path in source_texts},
{path: 0 for path in source_texts},
{},
max_edges=1,
)
assert len(graph.edges) == 1
assert graph.truncated is True
def test_architecture_resolves_jsconfig_aliases_and_local_workspace_packages(tmp_path: Path) -> None:
(tmp_path / "src" / "components").mkdir(parents=True)
(tmp_path / "packages" / "shared" / "src").mkdir(parents=True)
(tmp_path / "jsconfig.json").write_text(
'{"compilerOptions":{"baseUrl":".","paths":{"@/*":["src/*"]}}}',
encoding="utf-8",
)
(tmp_path / "package.json").write_text('{"name":"workspace-root"}', encoding="utf-8")
(tmp_path / "packages" / "shared" / "package.json").write_text(
'{"name":"@workspace/shared"}', encoding="utf-8"
)
source_texts = {
"src/app.ts": "import { Button } from '@/components/Button'\nimport { shared } from '@workspace/shared'\n",
"src/components/Button.ts": "export const Button = () => null\n",
"packages/shared/src/index.ts": "export const shared = true\n",
}
graph, _ = build_architecture(
tmp_path,
source_texts,
{path: "TypeScript" for path in source_texts},
{path: 1 for path in source_texts},
{path: 0 for path in source_texts},
{},
)
pairs = {(edge.source, edge.target) for edge in graph.edges}
assert ("src/app.ts", "src/components/Button.ts") in pairs
assert ("src/app.ts", "packages/shared/src/index.ts") in pairs
+203
View File
@@ -0,0 +1,203 @@
from __future__ import annotations
import json
from pathlib import Path
import pytest
from app.gitea import GiteaClient, GiteaError
from app.store import ScanStore
def _transport(url: str, headers: dict[str, str]) -> tuple[int, bytes]:
assert headers["Authorization"] == "token test-token"
if "/user/repos" in url:
return 200, json.dumps(
[
{
"id": 64,
"name": "RepoPulse",
"full_name": "Jens/RepoPulse",
"owner": {"login": "Jens"},
"description": "Repository intelligence",
"default_branch": "main",
"clone_url": "https://evil.example/Jens/RepoPulse.git",
"private": True,
}
]
).encode()
if "/branches/main" in url:
return 200, b'{"commit":{"id":"0123456789abcdef0123456789abcdef01234567"}}'
raise AssertionError(url)
def test_gitea_discovery_is_bounded_and_preserves_stable_identity() -> None:
client = GiteaClient(
"http://gitea.local",
"test-token",
transport=_transport,
)
discovered = client.discover()
assert len(discovered) == 1
repository, revision = discovered[0]
assert repository.id == "gitea:64"
assert repository.provider_repository_id == "64"
assert repository.full_name == "Jens/RepoPulse"
assert repository.clone_url == "http://gitea.local/Jens/RepoPulse.git"
assert repository.latest_revision == revision
def test_gitea_auth_errors_are_sanitized() -> None:
def rejected(_url: str, _headers: dict[str, str]) -> tuple[int, bytes]:
return 401, b"secret-bearing upstream response"
client = GiteaClient("http://gitea.local", "test-token", transport=rejected)
try:
client.list_repositories()
except GiteaError as exc:
assert exc.code == "authentication"
assert "test-token" not in str(exc)
assert "secret-bearing" not in str(exc)
else:
raise AssertionError("authentication error was not raised")
@pytest.mark.parametrize("status_code, error_code", [(401, "authentication"), (403, "authentication"), (404, "not_found"), (429, "rate_limited"), (500, "upstream")])
def test_gitea_http_statuses_are_classified_without_upstream_body(status_code: int, error_code: str) -> None:
client = GiteaClient(
"http://gitea.local",
"test-token",
transport=lambda *_: (status_code, b"token=test-token upstream body"),
)
with pytest.raises(GiteaError) as raised:
client.list_repositories()
assert raised.value.code == error_code
assert "test-token" not in str(raised.value)
assert "upstream body" not in str(raised.value)
def test_gitea_discovery_paginates_until_a_short_page() -> None:
calls: list[str] = []
def paginated(url: str, headers: dict[str, str]) -> tuple[int, bytes]:
calls.append(url)
assert headers["Authorization"] == "token test-token"
if "/user/repos" in url:
if "page=1" in url:
payload = [{"id": 64, "owner": {"login": "Jens"}, "name": "One", "default_branch": "main"}]
elif "page=2" in url:
payload = [{"id": 65, "owner": {"login": "Jens"}, "name": "Two", "default_branch": "trunk"}]
else:
payload = []
return 200, json.dumps(payload).encode()
if "/branches/main" in url:
return 200, b'{"commit":{"id":"0123456789abcdef0123456789abcdef01234567"}}'
if "/branches/trunk" in url:
return 200, b'{"commit":{"sha":"abcdef0123456789abcdef0123456789abcdef01"}}'
raise AssertionError(url)
client = GiteaClient("http://gitea.local", "test-token", page_size=1, max_pages=3, transport=paginated)
discovered = client.discover()
assert [repository.id for repository, _ in discovered] == ["gitea:64", "gitea:65"]
assert len([call for call in calls if "/user/repos" in call]) == 3
@pytest.mark.parametrize(
"transport, error_code",
[
(lambda *_: (200, b"not-json"), "invalid_response"),
(lambda *_: (_ for _ in ()).throw(TimeoutError()), "unreachable"),
],
)
def test_gitea_malformed_and_timeout_responses_are_safe(transport, error_code: str) -> None:
client = GiteaClient("http://gitea.local", "test-token", transport=transport)
with pytest.raises(GiteaError) as raised:
client.list_repositories()
assert raised.value.code == error_code
def test_gitea_sync_creates_catalog_without_full_reports(tmp_path: Path) -> None:
store = ScanStore(str(tmp_path / "history.db"))
client = GiteaClient("http://gitea.local", "test-token", transport=_transport)
result = client.sync(store)
assert result.discovered == 1
assert result.created == 1
repository = store.get_repository("gitea:64")
assert repository is not None
assert repository.latest_revision == "0123456789abcdef0123456789abcdef01234567"
assert repository.description == "Repository intelligence"
assert store.repository_scans(repository.id) == []
def test_gitea_outage_preserves_catalog_and_marks_sync_error(tmp_path: Path) -> None:
store = ScanStore(str(tmp_path / "history.db"))
healthy = GiteaClient("http://gitea.local", "test-token", transport=_transport)
healthy.sync(store)
unavailable = GiteaClient(
"http://gitea.local",
"test-token",
transport=lambda *_: (503, b"upstream details must not escape"),
)
result = unavailable.sync(store)
assert result.errors == ["upstream: Gitea returned a server error"]
repository = store.get_repository("gitea:64")
assert repository is not None
assert repository.available is True
assert repository.latest_revision == "0123456789abcdef0123456789abcdef01234567"
assert repository.sync_error == "upstream: Gitea returned a server error"
def test_complete_sync_marks_missing_repository_unavailable_without_deleting_history(tmp_path: Path) -> None:
store = ScanStore(str(tmp_path / "history.db"))
GiteaClient("http://gitea.local", "test-token", transport=_transport).sync(store)
missing = GiteaClient("http://gitea.local", "test-token", transport=lambda *_: (200, b"[]"))
result = missing.sync(store)
assert result.unavailable == 1
repository = store.get_repository("gitea:64")
assert repository is not None
assert repository.available is False
assert store.repository_scans(repository.id) == []
def test_branch_resolution_failure_is_partial_and_preserves_other_catalog_entries(tmp_path: Path) -> None:
def partial_transport(url: str, _headers: dict[str, str]) -> tuple[int, bytes]:
if "/user/repos" in url:
return 200, json.dumps(
[
{"id": 1, "owner": {"login": "Jens"}, "name": "Healthy", "default_branch": "main"},
{"id": 2, "owner": {"login": "Jens"}, "name": "Broken", "default_branch": "main"},
]
).encode()
if "/repos/Jens/Healthy/branches/main" in url:
return 200, b'{"commit":{"id":"0123456789abcdef0123456789abcdef01234567"}}'
if "/repos/Jens/Broken/branches/main" in url:
return 500, b"upstream details must remain private"
raise AssertionError(url)
store = ScanStore(str(tmp_path / "history.db"))
result = GiteaClient("http://gitea.local", "test-token", transport=partial_transport).sync(store)
assert result.discovered == 1
assert result.partial is True
assert result.branch_failures == 1
assert result.listing_complete is True
assert "Jens/Broken: upstream: Gitea returned a server error" in result.errors
assert store.get_repository("gitea:1").latest_revision
broken = store.get_repository("gitea:2")
assert broken is not None and broken.available is True
assert broken.sync_error == "Jens/Broken: upstream: Gitea returned a server error"
+232
View File
@@ -0,0 +1,232 @@
from __future__ import annotations
import subprocess
from concurrent.futures import ThreadPoolExecutor
from datetime import UTC, datetime, timedelta
from pathlib import Path
import pytest
from app.acquisition import AcquisitionError, GitAcquirer
from app.gitea import GiteaClient
from app.jobs import ScanJobRunner
from app.models import Repository, ScanJobStatus, ScanSource, ScanTrigger, SourceOrigin
from app.scanner import scan_repository
from app.store import ScanStore
def _git_repository(root: Path) -> tuple[Path, str]:
root.mkdir()
(root / "README.md").write_text("# Demo\n", encoding="utf-8")
(root / "app.py").write_text("print('hello')\n", encoding="utf-8")
subprocess.run(["git", "init", "--quiet", str(root)], check=True)
subprocess.run(["git", "-C", str(root), "config", "user.email", "test@example.com"], check=True)
subprocess.run(["git", "-C", str(root), "config", "user.name", "RepoPulse Test"], check=True)
subprocess.run(["git", "-C", str(root), "add", "."], check=True)
subprocess.run(["git", "-C", str(root), "commit", "--quiet", "-m", "initial"], check=True)
revision = subprocess.run(
["git", "-C", str(root), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
return root, revision
def _catalog_repository(source: Path, revision: str) -> Repository:
return Repository(
id="gitea:64",
provider="gitea",
provider_repository_id="64",
owner="Jens",
name="Demo",
full_name="Jens/Demo",
clone_url=source.as_uri(),
default_branch="master",
latest_revision=revision,
)
def test_acquirer_scans_detached_exact_revision_and_cleans_up(tmp_path: Path) -> None:
source, revision = _git_repository(tmp_path / "source")
repository = _catalog_repository(source, revision)
acquirer = GitAcquirer(tmp_path / "cache", timeout=30)
acquired = acquirer.acquire(repository, "job-1", revision)
checked_out = subprocess.run(
["git", "-C", str(acquired.path), "rev-parse", "HEAD"],
check=True,
capture_output=True,
text=True,
).stdout.strip()
assert checked_out == revision
assert (acquired.path / "app.py").read_text(encoding="utf-8") == "print('hello')\n"
acquirer.cleanup(acquired, repository)
assert not acquired.path.exists()
def test_acquirer_recovers_a_corrupt_mirror_and_rejects_missing_sha(tmp_path: Path) -> None:
source, revision = _git_repository(tmp_path / "source")
repository = _catalog_repository(source, revision)
acquirer = GitAcquirer(tmp_path / "cache", timeout=30)
mirror = acquirer._mirror_path(repository)
mirror.parent.mkdir(parents=True, exist_ok=True)
mirror.write_text("corrupt mirror", encoding="utf-8")
with pytest.raises(AcquisitionError, match="Requested commit was not found"):
acquirer.acquire(repository, "job-missing", "abcdef0123456789abcdef0123456789abcdef01")
acquired = acquirer.acquire(repository, "job-recovered", revision)
assert acquired.actual_revision == revision
acquirer.cleanup(acquired, repository)
def test_durable_runner_completes_and_deduplicates_revision(tmp_path: Path) -> None:
source, revision = _git_repository(tmp_path / "source")
store = ScanStore(str(tmp_path / "history.db"))
repository = _catalog_repository(source, revision)
store.upsert_repository(repository)
runner = ScanJobRunner(
store,
gitea=GiteaClient("http://gitea.local", "test-token", transport=lambda *_: (500, b"")),
acquirer=GitAcquirer(tmp_path / "cache", timeout=30),
)
first = runner.enqueue(repository)
completed = runner.process_one()
reused = runner.enqueue(repository)
assert completed is not None
assert completed.status == ScanJobStatus.COMPLETED
assert completed.scan_id is not None
assert reused.id == first.id
assert reused.deduplicated is True
report = store.get(completed.scan_id)
assert report is not None
assert report.source.actual_revision == revision
assert report.source.trigger == ScanTrigger.MANUAL
assert completed.duration_ms is not None
def test_expired_job_lease_is_recovered_for_another_claim(tmp_path: Path) -> None:
store = ScanStore(str(tmp_path / "history.db"))
repository = _catalog_repository(tmp_path, "0123456789abcdef0123456789abcdef01234567")
store.upsert_repository(repository)
queued = store.enqueue_job(repository.id, repository.latest_revision, ScanTrigger.SCHEDULED)
store.update_job(
queued.id,
status=ScanJobStatus.SCANNING,
phase="scanning",
lease_until=datetime.now(UTC) - timedelta(seconds=5),
)
claimed = store.claim_next_job()
assert claimed is not None
assert claimed.id == queued.id
assert claimed.status == ScanJobStatus.ACQUIRING_SOURCE
assert claimed.attempt == 1
assert claimed.message == "Acquiring exact repository revision"
def test_active_job_lease_renews_but_terminal_jobs_are_not_resurrected(tmp_path: Path) -> None:
store = ScanStore(str(tmp_path / "history.db"))
repository = _catalog_repository(tmp_path, "0123456789abcdef0123456789abcdef01234567")
store.upsert_repository(repository)
queued = store.enqueue_job(repository.id, repository.latest_revision, ScanTrigger.SCHEDULED)
claimed = store.claim_next_job()
assert claimed is not None
with store._connection() as db:
initial_lease = db.execute("SELECT lease_until FROM scan_jobs WHERE id = ?", (queued.id,)).fetchone()[0]
assert initial_lease is not None
renewed = store.renew_job_lease(queued.id)
assert renewed is not None
with store._connection() as db:
renewed_lease = db.execute("SELECT lease_until FROM scan_jobs WHERE id = ?", (queued.id,)).fetchone()[0]
assert renewed_lease is not None and renewed_lease > initial_lease
completed = store.update_job(queued.id, status=ScanJobStatus.COMPLETED, phase="completed", progress=100)
assert completed is not None
after_completion = store.renew_job_lease(queued.id)
assert after_completion is not None
with store._connection() as db:
terminal_lease = db.execute("SELECT lease_until FROM scan_jobs WHERE id = ?", (queued.id,)).fetchone()[0]
assert terminal_lease is None
def test_atomic_claim_allows_only_one_worker_to_own_a_queued_job(tmp_path: Path) -> None:
store = ScanStore(str(tmp_path / "history.db"))
repository = _catalog_repository(tmp_path, "0123456789abcdef0123456789abcdef01234567")
store.upsert_repository(repository)
store.enqueue_job(repository.id, repository.latest_revision, ScanTrigger.SCHEDULED)
with ThreadPoolExecutor(max_workers=2) as workers:
claims = list(workers.map(lambda _unused: store.claim_next_job(), range(2)))
successful = [claim for claim in claims if claim is not None]
assert len(successful) == 1
assert successful[0].status == ScanJobStatus.ACQUIRING_SOURCE
def test_scheduler_queues_unscanned_and_changed_repositories_only(tmp_path: Path) -> None:
source, revision = _git_repository(tmp_path / "source")
store = ScanStore(str(tmp_path / "history.db"))
repository = _catalog_repository(source, revision)
store.upsert_repository(repository)
runner = ScanJobRunner(store, gitea=GiteaClient())
runner._schedule_changed_repositories()
first = store.claim_next_job()
assert first is not None
assert first.requested_revision == revision
store.update_job(first.id, status=ScanJobStatus.SUPERSEDED)
report = scan_repository(
str(source),
source=ScanSource(
origin=SourceOrigin.GITEA,
provider="gitea",
repository_id=repository.id,
repository_full_name=repository.full_name,
branch=repository.default_branch,
requested_revision=revision,
actual_revision=revision,
trigger=ScanTrigger.MANUAL,
),
)
store.save(report)
runner._schedule_changed_repositories()
assert store.queue_stats()["queued"] == 0
changed = "abcdef0123456789abcdef0123456789abcdef01"
store.upsert_repository(repository.model_copy(update={"latest_revision": changed}))
runner._schedule_changed_repositories()
assert store.queue_stats()["queued"] == 1
disabled = repository.model_copy(
update={
"id": "gitea:65",
"provider_repository_id": "65",
"latest_revision": "1234567890abcdef1234567890abcdef12345678",
"policy": repository.policy.model_copy(update={"auto_scan": False}),
}
)
store.upsert_repository(disabled)
runner._schedule_changed_repositories()
assert store.queue_stats()["queued"] == 1
def test_acquisition_rejects_token_bearing_clone_outside_gitea_origin(tmp_path: Path) -> None:
repository = _catalog_repository(tmp_path, "0123456789abcdef0123456789abcdef01234567").model_copy(
update={"clone_url": "https://evil.example/Jens/Demo.git"}
)
acquirer = GitAcquirer(
tmp_path / "cache",
token="test-token",
allowed_clone_origin="https://gitea.local",
)
with pytest.raises(AcquisitionError, match="outside the configured Gitea origin"):
acquirer.acquire(repository, "job-1", repository.latest_revision)
+175
View File
@@ -0,0 +1,175 @@
from __future__ import annotations
from pathlib import Path
import pytest
from fastapi.testclient import TestClient
from app import main as main_module
from app.gitea import GiteaClient
from app.jobs import ScanJobRunner
from app.models import Repository, ScanJobStatus, ScanTrigger
from app.store import MaintenanceModeError, ScanStore
def _gitea_repository() -> Repository:
return Repository(
id="gitea:64",
provider="gitea",
provider_repository_id="64",
owner="Jens",
name="RepoPulse",
full_name="Jens/RepoPulse",
default_branch="main",
latest_revision="0123456789abcdef0123456789abcdef01234567",
)
def test_maintenance_mode_is_durable_authenticated_and_exposes_drain_state(tmp_path: Path, monkeypatch) -> None:
database = tmp_path / "history.db"
main_module.store = ScanStore(str(database))
main_module.job_runner = None
main_module.store.upsert_repository(_gitea_repository())
queued = main_module.store.enqueue_job(
"gitea:64",
_gitea_repository().latest_revision,
ScanTrigger.MANUAL,
)
monkeypatch.delenv("REPOPULSE_OPERATOR_TOKEN", raising=False)
monkeypatch.delenv("REPOPULSE_MAINTENANCE_TOKEN", raising=False)
client = TestClient(main_module.app)
assert client.post("/api/v1/operator/maintenance/enter").status_code == 503
monkeypatch.setenv("REPOPULSE_MAINTENANCE_TOKEN", "operator-secret")
assert client.get("/api/v1/meta").json()["mutation_authentication"] == "perimeter_required"
assert client.post("/api/v1/operator/maintenance/enter").status_code == 401
assert client.get("/api/v1/operator/maintenance").status_code == 401
headers = {"X-RepoPulse-Token": "operator-secret"}
entered = client.post("/api/v1/operator/maintenance/enter", headers=headers)
assert entered.status_code == 200
assert entered.json()["enabled"] is True
assert entered.json()["queue_depth"] == 1
assert entered.json()["active_jobs"] == 0
assert entered.json()["active_drained"] is True
assert entered.json()["drained"] is False
changed_at = entered.json()["changed_at"]
assert client.post("/api/v1/operator/maintenance/enter", headers=headers).status_code == 423
assert main_module.store.maintenance_status()["changed_at"] == changed_at
assert client.get("/api/v1/operator/maintenance", headers=headers).json()["enabled"] is True
assert client.get("/api/v1/meta").json()["maintenance"]["enabled"] is True
restarted = ScanStore(str(database))
assert restarted.maintenance_status()["enabled"] is True
assert restarted.get_job(queued.id).status == ScanJobStatus.QUEUED
assert client.post("/api/v1/operator/maintenance/exit").status_code == 401
exited = client.post("/api/v1/operator/maintenance/exit", headers=headers)
assert exited.status_code == 200
assert exited.json()["enabled"] is False
assert ScanStore(str(database)).maintenance_status()["enabled"] is False
def test_paused_mode_rejects_api_mutations_but_keeps_read_endpoints_available(tmp_path: Path, monkeypatch) -> None:
main_module.store = ScanStore(str(tmp_path / "history.db"))
main_module.job_runner = None
repository = _gitea_repository()
main_module.store.upsert_repository(repository)
queued = main_module.store.enqueue_job(
repository.id,
repository.latest_revision,
ScanTrigger.MANUAL,
)
monkeypatch.delenv("REPOPULSE_OPERATOR_TOKEN", raising=False)
monkeypatch.setenv("REPOPULSE_MAINTENANCE_TOKEN", "operator-secret")
client = TestClient(main_module.app)
headers = {"X-RepoPulse-Token": "operator-secret"}
assert client.post("/api/v1/operator/maintenance/enter", headers=headers).status_code == 200
mutation_requests = [
("/api/v1/scans", {}),
("/api/v1/repositories/sync", {}),
("/api/v1/repositories/gitea%3A64/scans", {"json": {"force": True}}),
("/api/v1/webhooks/gitea", {"content": b"not validated while paused"}),
("/api/v1/operator/database-recovery/acknowledge", {}),
]
for path, kwargs in mutation_requests:
response = client.post(path, **kwargs)
assert response.status_code == 423, (path, response.status_code, response.text)
assert client.get("/api/v1/repositories").status_code == 200
assert client.get("/api/v1/scan-jobs/" + queued.id).status_code == 200
assert main_module.store.queue_stats()["queued"] == 1
assert main_module.store.get_job(queued.id).status == ScanJobStatus.QUEUED
def test_paused_worker_makes_no_scheduler_writes_or_job_claims(tmp_path: Path) -> None:
store = ScanStore(str(tmp_path / "history.db"))
repository = _gitea_repository()
store.upsert_repository(repository)
queued = store.enqueue_job(repository.id, repository.latest_revision, ScanTrigger.SCHEDULED)
store.set_state("worker_heartbeat", "before-maintenance")
store.set_state("last_gitea_sync", "prior-sync")
store.set_maintenance_mode(True)
class CountingGitea:
configured = True
def __init__(self) -> None:
self.sync_calls = 0
def sync(self, _store):
self.sync_calls += 1
raise AssertionError("scheduler must not sync in maintenance mode")
gitea = CountingGitea()
runner = ScanJobRunner(store, gitea=gitea) # type: ignore[arg-type]
assert runner._run_iteration() is None
assert gitea.sync_calls == 0
assert store.get_state("worker_heartbeat") == "before-maintenance"
assert store.get_state("last_gitea_sync") == "prior-sync"
assert store.claim_next_job() is None
assert store.queue_stats()["queued"] == 1
assert store.get_job(queued.id).status == ScanJobStatus.QUEUED
def test_already_claimed_job_can_finish_during_maintenance(tmp_path: Path) -> None:
source = tmp_path / "source"
source.mkdir()
(source / "README.md").write_text("# Existing work\n", encoding="utf-8")
(source / "app.py").write_text("print('ok')\n", encoding="utf-8")
store = ScanStore(str(tmp_path / "history.db"))
repository = Repository(
id="local:existing-job",
provider="local",
provider_repository_id="existing-job",
owner="local",
name="Existing job",
full_name="local/Existing job",
default_branch="main",
)
store.upsert_repository(repository, source_path=str(source))
store.enqueue_job(repository.id, None, ScanTrigger.MANUAL)
claimed = store.claim_next_job()
assert claimed is not None
runner = ScanJobRunner(store, gitea=GiteaClient())
store.set_maintenance_mode(True)
assert store.maintenance_status()["enabled"] is True
assert store.queue_stats()["active"] == 1
runner._process(claimed)
assert store.get_job(claimed.id).status == ScanJobStatus.COMPLETED
assert store.queue_stats()["active"] == 0
assert store.maintenance_status()["enabled"] is True
def test_invalid_persisted_maintenance_state_fails_closed(tmp_path: Path) -> None:
store = ScanStore(str(tmp_path / "history.db"))
store.set_state("maintenance_mode", "not-json")
assert store.maintenance_status()["enabled"] is True
with pytest.raises(MaintenanceModeError):
store.enqueue_job("missing", "revision", ScanTrigger.MANUAL)
+86
View File
@@ -0,0 +1,86 @@
from __future__ import annotations
import sqlite3
from pathlib import Path
from app.models import (
DependencySignals,
RepositoryMetrics,
ScanReport,
)
from app.models import (
TestSignals as SignalsModel,
)
from app.scanner import scan_repository
from app.store import SCHEMA_VERSION, ScanStore
def test_v1_scan_schema_is_migrated_and_legacy_history_is_cataloged(tmp_path: Path) -> None:
database = tmp_path / "history.db"
report = ScanReport(
id="legacy-scan",
repository_path=str((tmp_path / "repo").resolve()),
repository_name="repo",
scanned_at="2026-09-12T00:00:00+00:00",
duration_ms=1,
metrics=RepositoryMetrics(),
languages={},
category_scores={},
health_score=100,
release_readiness="READY",
findings=[],
tests=SignalsModel(),
dependencies=DependencySignals(),
)
connection = sqlite3.connect(database)
connection.execute(
"""
CREATE TABLE scans (
id TEXT PRIMARY KEY,
repository_path TEXT NOT NULL,
repository_name TEXT NOT NULL,
scanned_at TEXT NOT NULL,
health_score INTEGER NOT NULL,
report_json TEXT NOT NULL
)
"""
)
connection.execute(
"INSERT INTO scans VALUES (?, ?, ?, ?, ?, ?)",
(report.id, report.repository_path, report.repository_name, report.scanned_at.isoformat(), 100, report.model_dump_json()),
)
connection.commit()
connection.close()
store = ScanStore(str(database))
assert store.get_state("missing") is None
assert store.repository_count() == 1
migrated = store.get("legacy-scan")
assert migrated is not None
assert migrated.source.provider == "local"
assert migrated.source.repository_id is not None
assert store.list_repositories()[0].last_scan_id == "legacy-scan"
connection = sqlite3.connect(database)
assert connection.execute("PRAGMA user_version").fetchone()[0] == SCHEMA_VERSION
connection.close()
def test_schema_upgrade_creates_a_timestamped_pre_migration_backup(tmp_path: Path) -> None:
database = tmp_path / "history.db"
repository = tmp_path / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
(repository / ".gitignore").write_text("*.db\n", encoding="utf-8")
original = ScanStore(str(database))
saved = original.save(scan_repository(str(repository)))
connection = sqlite3.connect(database)
connection.execute("PRAGMA user_version = 3")
connection.commit()
connection.close()
migrated = ScanStore(str(database))
assert list(tmp_path.glob("history.db.pre-v3-*.bak"))
assert migrated.get(saved.id) is not None
+12
View File
@@ -0,0 +1,12 @@
from app.reporting import content_disposition, safe_report_filename
def test_report_filenames_are_safe_and_support_unicode() -> None:
filename = safe_report_filename('repo/evil\r\n"<é>', "html")
disposition = content_disposition(filename)
assert filename.startswith("repopulse-repo_evil___")
assert "\r" not in disposition
assert "\n" not in disposition
assert 'filename="' in disposition
assert "filename*=UTF-8''" in disposition
+223
View File
@@ -0,0 +1,223 @@
import json
import subprocess
from pathlib import Path
import pytest
from app.models import Severity
from app.scanner import scan_repository
def _write_good_repo(root: Path) -> None:
(root / "README.md").write_text("# Demo\n", encoding="utf-8")
(root / "LICENSE").write_text("MIT\n", encoding="utf-8")
(root / ".gitignore").write_text(".env\n", encoding="utf-8")
(root / "src").mkdir()
(root / "tests").mkdir()
(root / ".gitea" / "workflows").mkdir(parents=True)
(root / ".gitea" / "workflows" / "check.yml").write_text("name: check\n", encoding="utf-8")
(root / "src" / "core.py").write_text("def answer():\n return 42\n", encoding="utf-8")
(root / "src" / "app.py").write_text("import src.core\n\ndef run():\n return src.core.answer()\n", encoding="utf-8")
(root / "tests" / "test_app.py").write_text("def test_app():\n assert True\n", encoding="utf-8")
def _realistic_fixture_secret() -> str:
parts = ["production", "secret", "value", "123456"]
return "-".join(parts)
def test_scan_collects_v1_signals_and_architecture(tmp_path: Path) -> None:
_write_good_repo(tmp_path)
report = scan_repository(str(tmp_path))
assert report.repository_name == tmp_path.name
assert report.metrics.source_files == 3
assert report.metrics.test_files == 1
assert report.languages["Python"] > 0
assert report.tests.ci_workflows == [".gitea/workflows/check.yml"]
assert report.release_gate is not None
assert report.release_gate.status in {"READY", "READY WITH WARNINGS"}
assert any(edge.source == "src/app.py" and edge.target == "src/core.py" for edge in report.architecture.edges)
assert any(item.path == "src/app.py" for item in report.hotspots)
assert 0 <= report.health_score <= 100
def test_scan_collects_debt_and_dependency_signals(tmp_path: Path) -> None:
_write_good_repo(tmp_path)
(tmp_path / "package.json").write_text(
'{"dependencies":{"react":"latest"},"devDependencies":{"vitest":"^3.0.0"}}',
encoding="utf-8",
)
(tmp_path / "src" / "app.py").write_text(
"# TODO improve this\n# FIXME remove workaround\nprint('hello')\n",
encoding="utf-8",
)
report = scan_repository(str(tmp_path))
assert report.metrics.todo == 1
assert report.metrics.fixme == 1
assert report.dependencies.declared_dependencies == 2
assert report.dependencies.unbounded_dependencies == 1
assert any(item.code == "dependency.unbounded" for item in report.findings)
def test_potential_secret_is_redacted_and_blocks_release(tmp_path: Path) -> None:
(tmp_path / "README.md").write_text("# Demo\n", encoding="utf-8")
(tmp_path / ".gitignore").write_text("*.db\n", encoding="utf-8")
secret_name = "API_" + "KEY"
(tmp_path / "app.py").write_text(
f"{secret_name} = 'super-secret-value-123456'\n",
encoding="utf-8",
)
report = scan_repository(str(tmp_path))
secret_findings = [item for item in report.findings if item.code == "security.secret-assignment"]
assert report.release_readiness == "BLOCKED"
assert report.release_gate is not None
assert report.release_gate.status == "BLOCKED"
assert secret_findings
assert secret_findings[0].severity == Severity.BLOCKER
assert "super-secret-example-value" not in secret_findings[0].message
def test_secret_detection_scans_real_literals_in_tests_but_ignores_obvious_templates(tmp_path: Path) -> None:
_write_good_repo(tmp_path)
realistic_secret = _realistic_fixture_secret()
token_key = "to" + "ken"
(tmp_path / "src" / "settings.py").write_text(
"secret = os.getenv('APP_SECRET')\n" + f"api_key = '{realistic_secret}'\n",
encoding="utf-8",
)
(tmp_path / "tests" / "test_transport.py").write_text(
token_key + ' = "' + realistic_secret + '"\nresponse = b"token=test-token upstream body"\n',
encoding="utf-8",
)
(tmp_path / "README.md").write_text("# Demo\n\n`token=example-documentation-value`\n", encoding="utf-8")
(tmp_path / ".env.development.example").write_text("APP_SECRET=example-development-secret\n", encoding="utf-8")
report = scan_repository(str(tmp_path))
secret_findings = [item for item in report.findings if item.code == "security.secret-assignment"]
assert len(secret_findings) == 2
assert {item.path for item in secret_findings} == {"src/settings.py", "tests/test_transport.py"}
assert all(item.guidance and item.guidance.remediation_class == "security remediation" for item in secret_findings)
assert secret_findings[0].line == 2
assert not any(item.code == "security.env-committed" for item in report.findings)
def test_scan_budget_reports_coverage_without_turning_truncation_into_a_blocker(tmp_path: Path) -> None:
_write_good_repo(tmp_path)
for index in range(4):
(tmp_path / f"module_{index}.py").write_text("print('ok')\n", encoding="utf-8")
from app.models import ScanLimits
report = scan_repository(str(tmp_path), limits=ScanLimits(max_files=2, max_text_bytes_per_file=2_000_000, max_findings=20, max_secret_findings=5))
assert report.coverage is not None and report.coverage.truncated is True
assert report.coverage.skipped_files_by_budget > 0
assert any(check.key == "analysis_coverage" and check.state.value == "warn" for check in report.release_gate.checks)
assert any(item.code == "analysis.findings-truncated" for item in report.findings) is False
def test_configuration_literal_secret_remains_a_blocker(tmp_path: Path) -> None:
_write_good_repo(tmp_path)
realistic_secret = _realistic_fixture_secret()
token_key = "to" + "ken"
(tmp_path / "settings.json").write_text(
json.dumps({token_key: realistic_secret}) + "\n",
encoding="utf-8",
)
report = scan_repository(str(tmp_path))
secret_findings = [item for item in report.findings if item.code == "security.secret-assignment"]
assert report.release_readiness == "BLOCKED"
assert [(item.path, item.line) for item in secret_findings] == [("settings.json", 1)]
def test_member_literal_secret_remains_a_blocker(tmp_path: Path) -> None:
_write_good_repo(tmp_path)
(tmp_path / "src" / "client.py").write_text(
"class Client:\n def __init__(self):\n self.api_key = 'production-secret-value-123456'\n",
encoding="utf-8",
)
report = scan_repository(str(tmp_path))
secret_findings = [item for item in report.findings if item.code == "security.secret-assignment"]
assert report.release_readiness == "BLOCKED"
assert [(item.path, item.line) for item in secret_findings] == [("src/client.py", 3)]
def test_documentation_only_repository_does_not_require_test_files(tmp_path: Path) -> None:
(tmp_path / "README.md").write_text("# Catalog entry\n", encoding="utf-8")
(tmp_path / ".gitignore").write_text(".DS_Store\n", encoding="utf-8")
report = scan_repository(str(tmp_path))
assert report.metrics.source_files == 0
assert not any(item.code == "testing.none-detected" for item in report.findings)
def test_scan_skips_symlinked_files_outside_repository(tmp_path: Path) -> None:
_write_good_repo(tmp_path)
external = tmp_path.parent / f"{tmp_path.name}-outside.py"
external.write_text("API_KEY = 'outside-secret-example-value'\n", encoding="utf-8")
linked = tmp_path / "linked.py"
try:
linked.symlink_to(external)
except (NotImplementedError, OSError):
pytest.skip("symlinks are unavailable in this environment")
report = scan_repository(str(tmp_path))
assert report.metrics.source_files == 3
assert all(finding.path != "linked.py" for finding in report.findings)
def test_scan_skips_symlinked_directories_outside_repository(tmp_path: Path) -> None:
_write_good_repo(tmp_path)
external = tmp_path.parent / f"{tmp_path.name}-outside-dir"
external.mkdir()
(external / "outside.py").write_text("API_KEY = 'outside-secret-example-value'\n", encoding="utf-8")
linked = tmp_path / "linked"
try:
linked.symlink_to(external, target_is_directory=True)
except (NotImplementedError, OSError):
pytest.skip("directory symlinks are unavailable in this environment")
report = scan_repository(str(tmp_path))
assert report.metrics.source_files == 3
assert all(finding.path != "linked/outside.py" for finding in report.findings)
def test_malformed_dependency_manifests_are_reported_without_crashing(tmp_path: Path) -> None:
(tmp_path / "README.md").write_text("# Demo\n", encoding="utf-8")
(tmp_path / ".gitignore").write_text("\n", encoding="utf-8")
(tmp_path / "package.json").write_text("[]", encoding="utf-8")
(tmp_path / "pyproject.toml").write_text("[project]\ndependencies = \"invalid\"\n", encoding="utf-8")
(tmp_path / "pom.xml").write_text("<project>", encoding="utf-8")
report = scan_repository(str(tmp_path))
codes = {finding.code for finding in report.findings}
assert "dependency.invalid-package-json" in codes
assert "dependency.invalid-pyproject" in codes
assert "dependency.invalid-pom" in codes
def test_empty_git_repository_does_not_fallback_to_untracked_files(tmp_path: Path) -> None:
(tmp_path / "stray.py").write_text("print('not tracked')\n", encoding="utf-8")
subprocess.run(["git", "init", "--quiet", str(tmp_path)], check=True)
report = scan_repository(str(tmp_path))
assert report.git.available is True
assert report.git.tracked_files == 0
assert report.metrics.files == 0
assert report.metrics.source_files == 0
+113
View File
@@ -0,0 +1,113 @@
from pathlib import Path
from app.signals import analyze_dependencies, detect_test_signals
def test_dependency_analysis_handles_multiple_ecosystems(tmp_path: Path) -> None:
package = tmp_path / "package.json"
package.write_text('{"dependencies":{"react":"latest"}}', encoding="utf-8")
requirements = tmp_path / "requirements.txt"
requirements.write_text("fastapi>=0.100\n", encoding="utf-8")
lock = tmp_path / "package-lock.json"
lock.write_text("{}", encoding="utf-8")
signals, findings = analyze_dependencies(
tmp_path,
{
"package.json": [package],
"requirements.txt": [requirements],
"package-lock.json": [lock],
},
)
assert signals.declared_dependencies == 2
assert signals.unbounded_dependencies == 2
assert signals.unpinned_dependencies == 0
assert "package-lock.json" in signals.lockfiles
assert any(item.code == "dependency.unbounded" for item in findings)
def test_python_dependency_ranges_only_flag_truly_unbounded_declarations(tmp_path: Path) -> None:
pyproject = tmp_path / "pyproject.toml"
pyproject.write_text(
"""
[project]
dependencies = [
"exact==1.2.3",
"compatible~=2.0",
"bounded>=3.0,<4.0",
"open>=5.0",
]
optional-dependencies.dev = ["pytest~=8.0"]
""".strip(),
encoding="utf-8",
)
signals, findings = analyze_dependencies(tmp_path, {"pyproject.toml": [pyproject]})
assert signals.declared_dependencies == 5
assert signals.unbounded_dependencies == 1
assert [finding.code for finding in findings] == ["dependency.python-unbounded"]
def test_test_signals_detect_ci_coverage_and_frameworks() -> None:
text_by_path = {
"package.json": '{"devDependencies":{"vitest":"^3.0.0","@playwright/test":"^1.0.0"}}',
}
signals = detect_test_signals(
["src/app.ts", "src/app.test.ts", ".gitea/workflows/ci.yml", "coverage/lcov.info"],
source_files=2,
test_files=1,
text_by_path=text_by_path,
)
assert "Vitest" in signals.frameworks
assert "Playwright" in signals.frameworks
assert signals.ci_workflows == [".gitea/workflows/ci.yml"]
assert signals.coverage_artifacts == ["coverage/lcov.info"]
assert signals.test_source_ratio == 50.0
def test_test_signals_distinguish_validation_from_deploy_only_workflows() -> None:
signals = detect_test_signals(
[".gitea/workflows/validate.yml", ".gitea/workflows/deploy.yml"],
source_files=1,
test_files=1,
text_by_path={
".gitea/workflows/validate.yml": "steps:\n - run: pytest --cov\n - run: npm run build\n",
".gitea/workflows/deploy.yml": "steps:\n - run: deploy production\n",
},
)
assert signals.ci_validation_workflows == [".gitea/workflows/validate.yml"]
assert signals.ci_build_workflows == [".gitea/workflows/validate.yml"]
assert signals.ci_deploy_only_workflows == [".gitea/workflows/deploy.yml"]
assert signals.coverage_evidence == [".gitea/workflows/validate.yml"]
def test_dependency_analysis_follows_local_requirements_includes_and_workspace_locks(tmp_path: Path) -> None:
packages = tmp_path / "packages" / "web"
packages.mkdir(parents=True)
package = packages / "package.json"
package.write_text('{"dependencies":{"shared":"workspace:*"}}', encoding="utf-8")
lock = tmp_path / "pnpm-lock.yaml"
lock.write_text("lockfileVersion: '9.0'\n", encoding="utf-8")
requirements = tmp_path / "requirements.txt"
included = tmp_path / "requirements-prod.txt"
requirements.write_text("-r requirements-prod.txt\n", encoding="utf-8")
included.write_text("fastapi==0.116.0\n", encoding="utf-8")
signals, findings = analyze_dependencies(
tmp_path,
{
"package.json": [package],
"pnpm-lock.yaml": [lock],
"requirements.txt": [requirements],
"requirements-prod.txt": [included],
},
)
assert signals.unbounded_dependencies == 0
assert signals.included_requirements == ["requirements-prod.txt"]
assert signals.declared_dependencies == 2
assert not any(item.code == "dependency.no-lockfile" for item in findings)
+59
View File
@@ -0,0 +1,59 @@
from __future__ import annotations
import subprocess
from pathlib import Path
from app.git_analysis import analyze_git
from app.scanner import _walk_files, scan_repository
def test_non_git_fallback_applies_root_gitignore_rules(tmp_path: Path) -> None:
(tmp_path / ".gitignore").write_text("ignored.py\nignored-dir/\n*.log\n!keep.log\n", encoding="utf-8")
(tmp_path / "kept.py").write_text("print('kept')\n", encoding="utf-8")
(tmp_path / "ignored.py").write_text("print('ignored')\n", encoding="utf-8")
(tmp_path / "keep.log").write_text("kept\n", encoding="utf-8")
(tmp_path / "drop.log").write_text("dropped\n", encoding="utf-8")
(tmp_path / "ignored-dir").mkdir()
(tmp_path / "ignored-dir" / "nested.py").write_text("print('nested')\n", encoding="utf-8")
files = {path.relative_to(tmp_path).as_posix() for path in _walk_files(tmp_path)}
assert "kept.py" in files
assert "keep.log" in files
assert "ignored.py" not in files
assert "drop.log" not in files
assert "ignored-dir/nested.py" not in files
def test_git_churn_attributes_renames_to_the_new_path(tmp_path: Path) -> None:
subprocess.run(["git", "init", "--quiet", str(tmp_path)], check=True)
subprocess.run(["git", "-C", str(tmp_path), "config", "user.email", "test@example.com"], check=True)
subprocess.run(["git", "-C", str(tmp_path), "config", "user.name", "RepoPulse Test"], check=True)
(tmp_path / "old.py").write_text("print('hello')\n", encoding="utf-8")
subprocess.run(["git", "-C", str(tmp_path), "add", "."], check=True)
subprocess.run(["git", "-C", str(tmp_path), "commit", "--quiet", "-m", "initial"], check=True)
subprocess.run(["git", "-C", str(tmp_path), "mv", "old.py", "new.py"], check=True)
subprocess.run(["git", "-C", str(tmp_path), "commit", "--quiet", "-m", "rename"], check=True)
(tmp_path / "new.py").write_text("print('hello world')\n", encoding="utf-8")
subprocess.run(["git", "-C", str(tmp_path), "commit", "--quiet", "-am", "edit after rename"], check=True)
signals, churn = analyze_git(tmp_path)
assert signals.renamed_files == ["new.py"]
assert churn.get("new.py", 0) > 0
assert "old.py" not in churn
def test_policy_suppresses_accepted_risk_without_erasing_the_observation(tmp_path: Path) -> None:
(tmp_path / "README.md").write_text("# Demo\n", encoding="utf-8")
(tmp_path / ".gitignore").write_text("\n", encoding="utf-8")
(tmp_path / ".repopulse.yml").write_text("suppressions:\n - security.secret-assignment\n", encoding="utf-8")
(tmp_path / "app.py").write_text("api_key = 'abcdefghijk1234'\n", encoding="utf-8")
report = scan_repository(str(tmp_path))
finding = next(item for item in report.findings if item.code == "security.secret-assignment")
assert finding.suppressed is True
assert report.category_scores["Security basics"] == 100
assert report.release_gate is not None
assert report.release_gate.checks[0].state.value == "pass"
+161
View File
@@ -0,0 +1,161 @@
import json
from datetime import UTC, datetime, timedelta
from pathlib import Path
import app.store as store_module
from app.scanner import scan_repository
from app.store import ScanStore
def test_scan_history_comparison_and_trend(tmp_path: Path) -> None:
repository = tmp_path / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
(repository / ".gitignore").write_text("*.db\n", encoding="utf-8")
store = ScanStore(str(tmp_path / "repopulse.db"))
first = store.save(scan_repository(str(repository)))
(repository / "app.py").write_text("print('hello')\n", encoding="utf-8")
(repository / "tests").mkdir()
(repository / "tests" / "test_app.py").write_text("def test_ok():\n assert True\n", encoding="utf-8")
second = store.save(scan_repository(str(repository)))
assert store.get(second.id) == second
assert store.recent(1) == [second]
assert store.previous_for_repository(second.repository_path, second.id) == first
comparison = store.compare(second)
assert comparison.previous_scan_id == first.id
assert comparison.health_score_delta == second.health_score - first.health_score
assert comparison.metric_deltas["source_files"] == 2
trend = store.trend(second.repository_path)
assert [item.scan_id for item in trend] == [first.id, second.id]
assert trend[-1].health_score == second.health_score
def test_comparison_uses_the_scan_before_the_selected_scan(tmp_path: Path) -> None:
repository = tmp_path / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
(repository / ".gitignore").write_text("*.db\n", encoding="utf-8")
store = ScanStore(str(tmp_path / "repopulse.db"))
first = store.save(scan_repository(str(repository)))
second = store.save(scan_repository(str(repository)))
third = store.save(scan_repository(str(repository)))
assert store.previous_for_repository(second.repository_path, second.id) == first
assert store.previous_for_repository(third.repository_path, third.id) == second
def test_repository_summary_exposes_compact_remediation_context(tmp_path: Path) -> None:
repository = tmp_path / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
(repository / ".gitignore").write_text(".env\n", encoding="utf-8")
(repository / ".env").write_text("TOKEN=actual-secret-value-123456\n", encoding="utf-8")
store = ScanStore(str(tmp_path / "repopulse.db"))
store.save(scan_repository(str(repository)))
(repository / ".env").unlink()
(repository / "app.py").write_text("api_key = 'actual-secret-value-123456'\n", encoding="utf-8")
store.save(scan_repository(str(repository)))
summary = store.list_repositories()[0]
assert summary.last_scan_blockers == 1
assert summary.last_scan_primary_signal is not None
assert summary.last_scan_primary_signal.code == "security.secret-assignment"
assert summary.last_scan_primary_signal.count == 1
assert summary.last_scan_added_findings >= 1
assert summary.last_scan_resolved_findings == 1
def test_corrupt_database_is_quarantined_and_recreated(tmp_path: Path) -> None:
database = tmp_path / "repopulse.db"
database.write_bytes(b"not a sqlite database")
store = ScanStore(str(database))
assert store.recent() == []
assert list(tmp_path.glob("repopulse.db.corrupt-*"))
assert store.database_recovery_status()["status"] == "degraded_recovered"
def test_schema_migration_records_the_durable_backup_artifact(tmp_path: Path) -> None:
database = tmp_path / "repopulse.db"
initial_store = ScanStore(str(database))
with initial_store._connection() as db:
db.execute("PRAGMA user_version = 3")
migrated_store = ScanStore(str(database))
recorded = migrated_store.get_state("last_schema_migration_backup")
assert recorded is not None
metadata = json.loads(recorded)
assert metadata["from_version"] == 3
assert metadata["to_version"] == store_module.SCHEMA_VERSION
assert (tmp_path / metadata["backup_file"]).is_file()
assert migrated_store.storage_stats()["last_schema_migration_backup"] == recorded
def test_history_is_bounded_per_repository(tmp_path: Path, monkeypatch) -> None:
repository = tmp_path / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
monkeypatch.setattr(store_module, "MAX_HISTORY_PER_REPOSITORY", 2)
store = ScanStore(str(tmp_path / "repopulse.db"))
first = store.save(scan_repository(str(repository)))
second = store.save(scan_repository(str(repository)))
third = store.save(scan_repository(str(repository)))
assert [item.id for item in store.recent_for_repository(third.repository_path)] == [third.id, second.id]
assert store.get(first.id) is None
def test_retention_compacts_old_detailed_payloads_and_expires_old_snapshots(tmp_path: Path, monkeypatch) -> None:
repository = tmp_path / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
(repository / ".gitignore").write_text("*.db\n", encoding="utf-8")
monkeypatch.setenv("REPOPULSE_FULL_REPORTS_PER_REPOSITORY", "1")
monkeypatch.setenv("REPOPULSE_REPORT_RETENTION_DAYS", "30")
store = ScanStore(str(tmp_path / "repopulse.db"))
first = store.save(scan_repository(str(repository)))
second = store.save(scan_repository(str(repository)))
compacted = store.get(first.id)
assert compacted is not None and compacted.compacted is True
assert compacted.findings == []
assert store.get(second.id) is not None
expired = second.model_copy(
update={"id": "expired-scan", "scanned_at": datetime.now(UTC) - timedelta(days=31)}
)
store.save(expired)
assert store.get("expired-scan") is None
def test_catalog_summary_uses_denormalized_fields_without_report_json_parsing(tmp_path: Path, monkeypatch) -> None:
repository = tmp_path / "repo"
repository.mkdir()
(repository / "README.md").write_text("# Demo\n", encoding="utf-8")
(repository / ".gitignore").write_text("*.db\n", encoding="utf-8")
store = ScanStore(str(tmp_path / "repopulse.db"))
store.save(scan_repository(str(repository)))
def fail_if_called(_row):
raise AssertionError("catalog list must not parse report_json")
monkeypatch.setattr(store, "_report_from_row", fail_if_called)
summary = store.list_repositories()[0]
assert summary.last_scan_id
storage = store.storage_stats()
assert storage["scan_count"] == 1
assert storage["mirror_count"] >= 0
assert storage["worktree_count"] >= 0