generated from coulomb/repo-seed
Implement operational discovery rescan loops
This commit is contained in:
@@ -7,6 +7,7 @@ from pathlib import Path
|
||||
|
||||
from railiance_fabric.cli import main as cli_main
|
||||
from railiance_fabric.registry import RegistryStore
|
||||
from railiance_fabric.schema_validation import draft202012_validator
|
||||
from railiance_fabric.server import RegistryHandler
|
||||
|
||||
|
||||
@@ -43,6 +44,89 @@ def test_registry_scan_manifest_dry_run_keeps_repo_failures_isolated(tmp_path: P
|
||||
assert (output_dir / "fixture-repo-deterministic.discovery.json").is_file()
|
||||
|
||||
|
||||
def test_registry_scan_manifest_writes_default_cache_and_report(tmp_path: Path, capsys) -> None:
|
||||
repo = _minimal_repo(tmp_path, "fixture-repo")
|
||||
manifest = _manifest(tmp_path, [{"slug": "fixture-repo", "name": "Fixture Repo", "path": str(repo)}])
|
||||
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--dry-run",
|
||||
"--json",
|
||||
]
|
||||
) == 0
|
||||
|
||||
summary = json.loads(capsys.readouterr().out)
|
||||
cache_dir = tmp_path / ".fabric-discovery"
|
||||
snapshot_path = cache_dir / "snapshots" / "fixture-repo-deterministic.discovery.json"
|
||||
assert summary["cache"]["cache_dir"] == str(cache_dir)
|
||||
assert summary["repositories"][0]["change_state"] == "baseline"
|
||||
assert summary["repositories"][0]["previous"]["source"] == "dir"
|
||||
assert summary["repositories"][0]["previous"]["found"] is False
|
||||
assert snapshot_path.is_file()
|
||||
assert Path(summary["report_path"]).is_file()
|
||||
report = json.loads(Path(summary["report_path"]).read_text(encoding="utf-8"))
|
||||
assert report["kind"] == "FabricDiscoveryRescanReport"
|
||||
assert report["counts"]["baseline"] == 1
|
||||
draft202012_validator(Path("schemas") / "rescan-run-report.schema.yaml").validate(report)
|
||||
assert not (cache_dir / "rescan.lock").exists()
|
||||
|
||||
|
||||
def test_registry_scan_manifest_operational_exit_codes_and_lock(tmp_path: Path, capsys) -> None:
|
||||
repo = _minimal_repo(tmp_path, "fixture-repo")
|
||||
manifest = _manifest(tmp_path, [{"slug": "fixture-repo", "name": "Fixture Repo", "path": str(repo)}])
|
||||
lock_file = tmp_path / "busy.lock"
|
||||
lock_file.write_text("busy\n", encoding="utf-8")
|
||||
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--dry-run",
|
||||
"--lock-file",
|
||||
str(lock_file),
|
||||
"--exit-code-mode",
|
||||
"operational",
|
||||
]
|
||||
) == 5
|
||||
assert "rescan lock already exists" in capsys.readouterr().err
|
||||
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--dry-run",
|
||||
"--exit-code-mode",
|
||||
"operational",
|
||||
"--json",
|
||||
]
|
||||
) == 2
|
||||
summary = json.loads(capsys.readouterr().out)
|
||||
assert summary["counts"]["baseline"] == 1
|
||||
|
||||
|
||||
def test_registry_scan_manifest_reconciles_from_default_cache(tmp_path: Path, capsys) -> None:
|
||||
repo = _minimal_repo(tmp_path, "fixture-repo")
|
||||
manifest = _manifest(tmp_path, [{"slug": "fixture-repo", "name": "Fixture Repo", "path": str(repo)}])
|
||||
|
||||
assert cli_main(["registry", "scan-manifest", str(manifest), "--dry-run", "--json"]) == 0
|
||||
capsys.readouterr()
|
||||
_write_pyproject(repo, ["PyYAML>=6.0"])
|
||||
|
||||
assert cli_main(["registry", "scan-manifest", str(manifest), "--dry-run", "--json"]) == 0
|
||||
|
||||
summary = json.loads(capsys.readouterr().out)
|
||||
repo_result = summary["repositories"][0]
|
||||
assert repo_result["previous"]["source"] == "dir"
|
||||
assert repo_result["previous"]["found"] is True
|
||||
assert repo_result["change_state"] == "changed"
|
||||
assert repo_result["diff_counts"]["added"] > 0
|
||||
|
||||
|
||||
def test_registry_scan_manifest_ingests_and_accepts_snapshots(tmp_path: Path, capsys) -> None:
|
||||
repo = _minimal_repo(tmp_path, "fixture-repo")
|
||||
manifest = _manifest(
|
||||
@@ -91,6 +175,187 @@ def test_registry_scan_manifest_ingests_and_accepts_snapshots(tmp_path: Path, ca
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
def test_registry_scan_manifest_reconciles_from_registry_and_skips_unchanged_ingest(tmp_path: Path, capsys) -> None:
|
||||
repo = _minimal_repo(tmp_path, "fixture-repo")
|
||||
manifest = _manifest(
|
||||
tmp_path,
|
||||
[{"slug": "fixture-repo", "name": "Fixture Repo", "domain": "testing", "path": str(repo)}],
|
||||
)
|
||||
store = RegistryStore(tmp_path / "registry.sqlite3")
|
||||
store.init_schema()
|
||||
|
||||
class Handler(RegistryHandler):
|
||||
pass
|
||||
|
||||
Handler.store = store
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
base_url = f"http://127.0.0.1:{server.server_port}"
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--registry-url",
|
||||
base_url,
|
||||
"--ingest",
|
||||
"--json",
|
||||
]
|
||||
) == 0
|
||||
first = json.loads(capsys.readouterr().out)
|
||||
assert first["repositories"][0]["change_state"] == "baseline"
|
||||
assert first["counts"]["ingested"] == 1
|
||||
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--registry-url",
|
||||
base_url,
|
||||
"--previous-from-registry",
|
||||
"--ingest",
|
||||
"--json",
|
||||
]
|
||||
) == 0
|
||||
second = json.loads(capsys.readouterr().out)
|
||||
repo_result = second["repositories"][0]
|
||||
assert repo_result["previous"]["source"] == "registry"
|
||||
assert repo_result["previous"]["found"] is True
|
||||
assert repo_result["change_state"] == "unchanged"
|
||||
assert repo_result["registry_action"] == "skipped_unchanged"
|
||||
assert second["counts"]["ingested"] == 0
|
||||
assert len(store.list_discovery_snapshots("fixture-repo")) == 1
|
||||
|
||||
_write_pyproject(repo, ["PyYAML>=6.0"])
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--registry-url",
|
||||
base_url,
|
||||
"--previous-from-registry",
|
||||
"--dry-run",
|
||||
"--json",
|
||||
]
|
||||
) == 0
|
||||
changed = json.loads(capsys.readouterr().out)
|
||||
assert changed["repositories"][0]["change_state"] == "changed"
|
||||
assert changed["repositories"][0]["previous"]["discovery_snapshot_id"] == 1
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
def test_registry_scan_manifest_registry_previous_unavailable_is_per_repo_error(tmp_path: Path, capsys) -> None:
|
||||
repo = _minimal_repo(tmp_path, "fixture-repo")
|
||||
manifest = _manifest(tmp_path, [{"slug": "fixture-repo", "name": "Fixture Repo", "path": str(repo)}])
|
||||
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--registry-url",
|
||||
"http://127.0.0.1:1",
|
||||
"--previous-from-registry",
|
||||
"--dry-run",
|
||||
"--json",
|
||||
]
|
||||
) == 0
|
||||
|
||||
summary = json.loads(capsys.readouterr().out)
|
||||
assert summary["counts"]["errors"] == 1
|
||||
assert summary["repositories"][0]["status"] == "error"
|
||||
assert "cannot reach registry" in summary["repositories"][0]["error"]
|
||||
|
||||
|
||||
def test_registry_scan_manifest_safe_accept_blocks_tombstone_projection(tmp_path: Path, capsys) -> None:
|
||||
repo = _minimal_repo(tmp_path, "fixture-repo")
|
||||
_write_pyproject(repo, ["PyYAML>=6.0", "requests>=2.31"])
|
||||
manifest = _manifest(
|
||||
tmp_path,
|
||||
[{"slug": "fixture-repo", "name": "Fixture Repo", "domain": "testing", "path": str(repo)}],
|
||||
)
|
||||
store = RegistryStore(tmp_path / "registry.sqlite3")
|
||||
store.init_schema()
|
||||
|
||||
class Handler(RegistryHandler):
|
||||
pass
|
||||
|
||||
Handler.store = store
|
||||
server = ThreadingHTTPServer(("127.0.0.1", 0), Handler)
|
||||
thread = threading.Thread(target=server.serve_forever, daemon=True)
|
||||
thread.start()
|
||||
try:
|
||||
base_url = f"http://127.0.0.1:{server.server_port}"
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--registry-url",
|
||||
base_url,
|
||||
"--ingest",
|
||||
"--json",
|
||||
]
|
||||
) == 0
|
||||
capsys.readouterr()
|
||||
|
||||
_write_pyproject(repo, ["PyYAML>=6.0"])
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"scan-manifest",
|
||||
str(manifest),
|
||||
"--registry-url",
|
||||
base_url,
|
||||
"--previous-from-registry",
|
||||
"--ingest",
|
||||
"--accept",
|
||||
"--json",
|
||||
]
|
||||
) == 0
|
||||
|
||||
summary = json.loads(capsys.readouterr().out)
|
||||
repo_result = summary["repositories"][0]
|
||||
assert repo_result["status"] == "review_required"
|
||||
assert repo_result["registry_action"] == "accept_blocked"
|
||||
assert repo_result["accepted"] is False
|
||||
assert repo_result["tombstone_count"] > 0
|
||||
assert summary["counts"]["review_required"] == 1
|
||||
assert len(store.list_discovery_snapshots("fixture-repo")) == 2
|
||||
assert store.latest_snapshots() == []
|
||||
|
||||
health = store.status()["latest_discovery_snapshots"][0]
|
||||
assert health["health"] == "needs_review"
|
||||
assert health["review_required"] is True
|
||||
assert health["tombstone_count"] > 0
|
||||
|
||||
assert cli_main(
|
||||
[
|
||||
"registry",
|
||||
"rescan-status",
|
||||
"--registry-url",
|
||||
base_url,
|
||||
"--review-only",
|
||||
"--json",
|
||||
]
|
||||
) == 0
|
||||
rescan_status = json.loads(capsys.readouterr().out)
|
||||
assert rescan_status["kind"] == "FabricDiscoveryRescanStatus"
|
||||
assert rescan_status["counts"]["review_required"] == 1
|
||||
assert rescan_status["repositories"][0]["repo_slug"] == "fixture-repo"
|
||||
finally:
|
||||
server.shutdown()
|
||||
server.server_close()
|
||||
thread.join(timeout=5)
|
||||
|
||||
|
||||
def _minimal_repo(tmp_path: Path, slug: str) -> Path:
|
||||
repo = tmp_path / slug
|
||||
repo.mkdir()
|
||||
@@ -112,3 +377,18 @@ def _manifest(tmp_path: Path, repositories: list[dict[str, object]]) -> Path:
|
||||
encoding="utf-8",
|
||||
)
|
||||
return manifest
|
||||
|
||||
|
||||
def _write_pyproject(repo: Path, dependencies: list[str]) -> None:
|
||||
dependency_lines = "\n".join(f' "{dependency}",' for dependency in dependencies)
|
||||
(repo / "pyproject.toml").write_text(
|
||||
f"""
|
||||
[project]
|
||||
name = "fixture-service"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
{dependency_lines}
|
||||
]
|
||||
""".lstrip(),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user