Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file removed .coverage
Binary file not shown.
30 changes: 30 additions & 0 deletions .github/copilot-instructions.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# Copilot Instructions for Immermatch

## Environment

- **Always** activate the virtual environment first: `source .venv/bin/activate`
- Python 3.10+, all dependencies installed in `.venv`
- Gemini model: `gemini-3-flash-preview` via `google-genai` package (NOT the deprecated `google.generativeai`)

## After every code change

Run the full check suite without asking — just do it:

```bash
source .venv/bin/activate && pytest tests/ -x -q && ruff check . && mypy .
```

## Testing conventions

- **Framework:** pytest + pytest-cov
- **Test file naming:** `tests/test_<module>.py` for `immermatch/<module>.py`
- **Mock all external services** (Gemini API, SerpAPI, Supabase, Resend) — no API keys needed to run tests
- **Shared fixtures** in `tests/conftest.py`: `sample_profile`, `sample_job`, `sample_evaluation`, `sample_evaluated_job`
- **Test fixture files** (sample CVs) live in `tests/fixtures/`
- Pydantic models live in `immermatch/models.py` — follow existing patterns

## Code conventions

- All DB writes use `get_admin_client()`, never the anon client
- Log subscriber UUIDs, never email addresses
- All `st.error()` calls show generic messages; real exceptions go to `logger.exception()`
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ jobs:
cache: pip
- run: pip install -e ".[test]"
- name: Tests
run: pytest -v --cov=immermatch --cov-report=term
run: pytest -v --cov=immermatch --cov-report=term --cov-fail-under=50

audit:
runs-on: ubuntu-latest
Expand Down
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ __pycache__/
*$py.class
*.so
.Python
.coverage
build/
develop-eggs/
dist/
Expand Down Expand Up @@ -41,7 +42,7 @@ Thumbs.db
# Project specific
*.pdf
output/
*cv*.md
total_cv.md
.immermatch_cache/

# Streamlit secrets (contains API keys)
Expand Down
6 changes: 2 additions & 4 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -41,9 +41,7 @@ repos:
hooks:
- id: test-suite
name: Run tests
entry: pytest tests/ -x -q --tb=short
language: python
additional_dependencies:
- pytest
entry: .venv/bin/pytest tests/ -x -q --tb=short
language: system
pass_filenames: false
stages: [pre-push]
6 changes: 5 additions & 1 deletion .secrets.baseline
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,10 @@
{
"path": "detect_secrets.filters.allowlist.is_line_allowlisted"
},
{
"path": "detect_secrets.filters.common.is_baseline_file",
"filename": ".secrets.baseline"
},
{
"path": "detect_secrets.filters.common.is_ignored_due_to_verification_policies",
"min_level": 2
Expand Down Expand Up @@ -123,5 +127,5 @@
}
],
"results": {},
"generated_at": "2026-02-21T16:02:22Z"
"generated_at": "2026-02-28T18:42:35Z"
}
6 changes: 6 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -544,6 +544,12 @@ source .venv/bin/activate
# All: ruff check . && mypy . && pytest tests/ -x -q
```

**IMPORTANT:** After every code change, run the check suite **without asking for permission** — just do it:
```bash
source .venv/bin/activate && pytest tests/ -x -q && ruff check . && mypy .
```
Do not ask the user "Shall I run the tests?" — always run them automatically.

### Conventions for AI agents

- **Always activate the virtual environment** (`source .venv/bin/activate`) before running any command (`pytest`, `ruff`, `mypy`, `streamlit`, etc.). The project's dependencies are installed only in `.venv`.
Expand Down
2 changes: 1 addition & 1 deletion ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ Based on the current state (private repo, hosted on Streamlit Community Cloud) a

### 0.1 — Testing & CI
- [x] **Write unit tests** for core modules: `llm.py`, `cache.py`, `cv_parser.py`, `db.py`, `search_agent.py`, `evaluator_agent.py` (mock API calls) — 146 tests across 9 test files
- [ ] **Write integration tests** for the full pipeline (profile → queries → search → evaluate → summary) using fixture CVs
- [x] **Write integration tests** for the full pipeline (profile → queries → search → evaluate → summary) using fixture CVs — 11 tests in `tests/test_integration.py` with tech + sustainability CV fixtures
- [x] **Set up GitHub Actions CI** — run `pytest` on every push/PR, lint with `ruff`
- [x] **Add type checking** — run `mypy` in CI (Pydantic models already help here)

Expand Down
15 changes: 15 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ include = ["immermatch*"]

[tool.pytest.ini_options]
testpaths = ["tests"]
markers = ["integration: Full pipeline integration tests (profile → queries → search → evaluate → summary)"]

[tool.ruff]
target-version = "py310"
Expand All @@ -64,6 +65,20 @@ ignore = ["E501"]
"daily_task.py" = ["E402"]
"tests/**" = ["S101", "S105", "S106"]

[tool.coverage.run]
source = ["immermatch", "daily_task"]
omit = [
"immermatch/pages/impressum.py",
"immermatch/pages/privacy.py",
]

[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"if __name__ == .__main__.",
"if TYPE_CHECKING:",
]

[tool.mypy]
ignore_missing_imports = true
warn_unused_configs = true
Expand Down
46 changes: 46 additions & 0 deletions tests/fixtures/sustainability_cv.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
# Areeba Ilyas Qureshi
## Climate & Sustainability Strategist | Environmental Engineer

**Location:** Munich, Germany

---

### Professional Summary
Results-driven Environmental Engineer and Sustainability Strategist with over 3 years of experience guiding global corporations toward Net Zero. Expert in GHG Protocol, CSRD/ESRS, SBTi, and ISO 14001. Proven track record in carbon accounting (Scopes 1-3), Life Cycle Assessments, and stakeholder engagement.

---

### Work Experience

**Sustainability Consultant** at Global Climate GmbH (May 2023 – Present)
- Key account management for 10-15 global manufacturing corporations
- Authored 25+ sustainability reports (CSRD, ESRS, GRI, TCFD)
- Developed Net Zero roadmaps and SBTi targets
- Conducted 20+ GHG assessments (CCF, PCF, Scopes 1-3)

**Research Assistant** at Technical University of Munich (May 2021 – April 2023)
- Designed circular economy wastewater management concepts
- Developed environmental models using MATLAB and Python
- Supported 150+ international sustainability projects

**VP Customer Experience** at AIESEC Munich (Feb 2020 – Feb 2021)
- Led international team of 10, managed CRM with HubSpot
- Built task management tools using Excel

**Intern – Construction Management** at NESPAK (Oct 2017 – Nov 2017)
- Urban planning research and infrastructure design using AutoCAD

---

### Education
- MSc Environmental Engineering, Technical University of Munich (2019–2023)
- BEng Urban and Infrastructure Planning, NED University (2014–2018)

### Skills
CSRD, ESRS, GRI, TCFD, SBTi, GHG Protocol, ISO 14001, ISO 14064, LCA, Carbon Accounting, MATLAB, Python, Power BI, AutoCAD, ArcGIS, HubSpot, Agile Project Management, Stakeholder Management

### Languages
- Urdu: Native
- English: C1/C2
- German: B1/B2
- Hindi: Spoken
60 changes: 60 additions & 0 deletions tests/test_app_ui.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
"""Tests for the Streamlit main app UI (app.py).

Uses Streamlit's AppTest framework to verify Phase A landing page renders,
the consent checkbox is present, and sidebar elements appear.
"""

from unittest.mock import MagicMock, patch

from streamlit.testing.v1 import AppTest

_FAKE_ENV = {
"GOOGLE_API_KEY": "fake-google-key", # pragma: allowlist secret
"SERPAPI_KEY": "fake-serpapi-key", # pragma: allowlist secret
"SUPABASE_URL": "https://fake.supabase.co",
"SUPABASE_KEY": "fake-anon-key", # pragma: allowlist secret
"SUPABASE_SERVICE_KEY": "fake-service-key", # pragma: allowlist secret
"RESEND_API_KEY": "fake-resend-key", # pragma: allowlist secret
"RESEND_FROM": "test@example.com",
"APP_URL": "https://app.example.com",
}

APP_FILE = "immermatch/app.py"


class TestPhaseALanding:
"""Phase A: no CV uploaded — landing page should render."""

@patch.dict("os.environ", _FAKE_ENV, clear=False)
@patch("immermatch.db.get_admin_client", return_value=MagicMock())
@patch("immermatch.db.purge_inactive_subscribers", return_value=0)
def test_app_loads_without_errors(self, _mock_purge: MagicMock, _mock_db: MagicMock) -> None:
at = AppTest.from_file(APP_FILE)
at.run()

# The app should not raise any uncaught exceptions
assert not at.exception, f"App raised exception: {at.exception}"

@patch.dict("os.environ", _FAKE_ENV, clear=False)
@patch("immermatch.db.get_admin_client", return_value=MagicMock())
@patch("immermatch.db.purge_inactive_subscribers", return_value=0)
def test_consent_checkbox_present(self, _mock_purge: MagicMock, _mock_db: MagicMock) -> None:
at = AppTest.from_file(APP_FILE)
at.run()

# GDPR consent checkbox should be on the page
checkboxes = at.checkbox
consent_found = any(
"consent" in (cb.label or "").lower() or "agree" in (cb.label or "").lower() for cb in checkboxes
)
assert consent_found, f"No consent checkbox found. Checkboxes: {[cb.label for cb in checkboxes]}"

@patch.dict("os.environ", _FAKE_ENV, clear=False)
@patch("immermatch.db.get_admin_client", return_value=MagicMock())
@patch("immermatch.db.purge_inactive_subscribers", return_value=0)
def test_sidebar_renders(self, _mock_purge: MagicMock, _mock_db: MagicMock) -> None:
at = AppTest.from_file(APP_FILE)
at.run()

# Sidebar should have a slider (min score)
assert len(at.slider) >= 1, "Expected at least one slider (min score) in sidebar"
Loading