Bot & Automation

AI Assistants

/root/hermes-projects/AI Assistants

tests/test_project_library.py text
from pathlib import Path
from zipfile import ZipFile

import pytest

from apps.whatsapp_control_api.app.api.main import (
    _build_project_action_token,
    _build_project_archive,
    _encode_project_ref,
    _inspect_project,
    _project_is_protected,
    _render_project_browser_page,
    _render_project_card,
    _render_delete_confirmation_page,
    _scan_project_library,
    _validate_project_action,
)
from apps.whatsapp_control_api.app.core.config import get_settings


@pytest.fixture(autouse=True)
def clear_settings_cache():
    get_settings.cache_clear()
    yield
    get_settings.cache_clear()


def test_project_library_detects_web_project(tmp_path: Path, monkeypatch) -> None:
    project = tmp_path / "Coffee Website"
    project.mkdir()
    (project / "package.json").write_text('{"dependencies":{"next":"15.0.0"}}', encoding="utf-8")
    (project / "page.tsx").write_text("export default function Page() {}", encoding="utf-8")
    monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
    get_settings.cache_clear()

    projects = _scan_project_library()

    assert len(projects) == 1
    assert projects[0]["name"] == "Coffee Website"
    assert projects[0]["category"] == "web"
    assert projects[0]["file_count"] == 2
    assert "/projects/" in projects[0]["browse_url"]


def test_project_archive_excludes_secrets_and_dependencies(tmp_path: Path, monkeypatch) -> None:
    project = tmp_path / "Safe Project"
    project.mkdir()
    (project / "README.md").write_text("safe", encoding="utf-8")
    (project / ".env").write_text("SECRET=unsafe", encoding="utf-8")
    node_modules = project / "node_modules"
    node_modules.mkdir()
    (node_modules / "dependency.js").write_text("large dependency", encoding="utf-8")
    monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
    get_settings.cache_clear()

    archive = _build_project_archive(project)
    with ZipFile(archive) as zip_file:
        names = set(zip_file.namelist())

    assert "Safe Project/README.md" in names
    assert "Safe Project/.env" not in names
    assert "Safe Project/node_modules/dependency.js" not in names


def test_project_action_token_validates_direct_child(tmp_path: Path, monkeypatch) -> None:
    project = tmp_path / "Download Me"
    project.mkdir()
    monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
    get_settings.cache_clear()
    encoded_ref = _encode_project_ref(project)
    token = _build_project_action_token(encoded_ref, "download")

    validated = _validate_project_action(encoded_ref, token, "download")

    assert validated == project.resolve()


def test_project_browse_token_validates_direct_child(tmp_path: Path, monkeypatch) -> None:
    project = tmp_path / "Browse Me"
    project.mkdir()
    monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
    get_settings.cache_clear()
    encoded_ref = _encode_project_ref(project)
    token = _build_project_action_token(encoded_ref, "browse")

    validated = _validate_project_action(encoded_ref, token, "browse")

    assert validated == project.resolve()


def test_inspect_project_detects_presentation(tmp_path: Path, monkeypatch) -> None:
    project = tmp_path / "Pitch Deck"
    project.mkdir()
    (project / "deck.pptx").write_bytes(b"presentation")
    monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
    get_settings.cache_clear()

    inspected = _inspect_project(project, tmp_path)

    assert inspected["category"] == "presentation"


def test_system_project_is_protected(tmp_path: Path) -> None:
    project = tmp_path / "AI Assistants"
    project.mkdir()
    assert _project_is_protected(project) is True


def test_delete_confirmation_requires_admin_token(tmp_path: Path) -> None:
    project = tmp_path / "Delete Me"
    project.mkdir()
    html = _render_delete_confirmation_page(project, "encoded", "signed-token")
    assert 'name="confirm_name"' in html
    assert 'name="admin_token"' in html
    assert 'name="confirm_delete"' in html


def test_project_card_links_to_file_browser(tmp_path: Path, monkeypatch) -> None:
    project = tmp_path / "Readable Project"
    project.mkdir()
    (project / "README.md").write_text("# Readable", encoding="utf-8")
    monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
    get_settings.cache_clear()

    html = _render_project_card(_inspect_project(project, tmp_path))

    assert "Open files" in html
    assert "Download ZIP" in html


def test_project_browser_renders_file_preview(tmp_path: Path, monkeypatch) -> None:
    project = tmp_path / "Readable Project"
    project.mkdir()
    (project / "README.md").write_text("# Readable\n\nIsi project.", encoding="utf-8")
    monkeypatch.setenv("HERMES_PROJECTS_ROOT", str(tmp_path))
    get_settings.cache_clear()
    encoded_ref = _encode_project_ref(project)
    token = _build_project_action_token(encoded_ref, "browse")

    html = _render_project_browser_page(project, encoded_ref, token, "README.md")

    assert "Isi Project" in html
    assert "README.md" in html
    assert "Isi project." in html
    assert "Download ZIP" in html