import json import pytest from furtka import dockerops, reconciler VALID_MANIFEST = { "name": "fileshare", "display_name": "Network Files", "version": "0.1.0", "description": "SMB share", "volumes": ["files", "config"], "ports": [445], "icon": "icon.svg", } @pytest.fixture def fake_docker(monkeypatch): """Replace dockerops with no-op recorders so reconcile() doesn't shell out.""" calls: dict[str, list] = {"ensure_volume": [], "compose_up": [], "compose_down": []} existing_volumes: set[str] = set() def fake_ensure(name): calls["ensure_volume"].append(name) if name in existing_volumes: return False existing_volumes.add(name) return True def fake_compose_up(app_dir, project): calls["compose_up"].append((str(app_dir), project)) def fake_compose_down(app_dir, project): calls["compose_down"].append((str(app_dir), project)) monkeypatch.setattr(dockerops, "ensure_volume", fake_ensure) monkeypatch.setattr(dockerops, "compose_up", fake_compose_up) monkeypatch.setattr(dockerops, "compose_down", fake_compose_down) return calls def _make_app(root, name, manifest=None): app = root / name app.mkdir(parents=True) if manifest is not None: (app / "manifest.json").write_text(json.dumps(manifest)) (app / "docker-compose.yaml").write_text("services: {}\n") return app def test_reconcile_empty_root(tmp_path, fake_docker): actions = reconciler.reconcile(tmp_path) assert actions == [] assert fake_docker["ensure_volume"] == [] assert fake_docker["compose_up"] == [] def test_reconcile_one_app(tmp_path, fake_docker): _make_app(tmp_path, "fileshare", VALID_MANIFEST) actions = reconciler.reconcile(tmp_path) assert [(a.kind, a.target) for a in actions] == [ ("ensure_volume", "furtka_fileshare_files"), ("ensure_volume", "furtka_fileshare_config"), ("compose_up", "fileshare"), ] assert fake_docker["ensure_volume"] == [ "furtka_fileshare_files", "furtka_fileshare_config", ] assert len(fake_docker["compose_up"]) == 1 assert fake_docker["compose_up"][0][1] == "fileshare" def test_reconcile_dry_run_does_not_act(tmp_path, fake_docker): _make_app(tmp_path, "fileshare", VALID_MANIFEST) actions = reconciler.reconcile(tmp_path, dry_run=True) assert len(actions) == 3 assert fake_docker["ensure_volume"] == [] assert fake_docker["compose_up"] == [] def test_reconcile_skips_broken_manifest(tmp_path, fake_docker): _make_app(tmp_path, "fileshare", VALID_MANIFEST) _make_app(tmp_path, "broken") # no manifest actions = reconciler.reconcile(tmp_path) skip_actions = [a for a in actions if a.kind == "skip"] assert len(skip_actions) == 1 assert skip_actions[0].target == "broken" # Healthy app still got reconciled. assert fake_docker["compose_up"] == [(str(tmp_path / "fileshare"), "fileshare")]