From 7bbab9c38b43b2bd8e6468f926c7528028344239 Mon Sep 17 00:00:00 2001 From: Dev Agent Date: Thu, 4 Jun 2026 22:12:59 +0300 Subject: [PATCH 1/3] test: isolate webhook tests from live Plane API (fix CI) --- .gitea/workflows/ci.yml | 22 ++++++++++++++++++++++ tests/test_webhooks.py | 19 +++++++++++++------ 2 files changed, 35 insertions(+), 6 deletions(-) create mode 100644 .gitea/workflows/ci.yml diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 0000000..2283a95 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,22 @@ +name: CI +on: + push: + branches: ["feature/**", "bugfix/**", "hotfix/**", "fix/**", "ci/**"] + pull_request: + branches: [main] + +jobs: + test: + runs-on: self-hosted + steps: + - uses: actions/checkout@v4 + - name: Install dependencies + run: | + python3 -m pip install --user --upgrade pip + python3 -m pip install --user -r requirements.txt + - name: Test + env: + PYTHONPATH: ${{ github.workspace }} + run: | + export PATH="$HOME/.local/bin:$PATH" + python3 -m pytest tests/ -q diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index 5f3850f..0ab8b0e 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -54,13 +54,19 @@ def test_status_endpoint(): assert "active_tasks" in resp.json() +@patch("src.plane_sync.add_comment") +@patch("src.plane_sync.fetch_issue_sequence_id", return_value=None) +@patch("src.plane_sync.fetch_issue_fields", return_value=("Test task", "This is a detailed test description for the task")) @patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock) @patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock) -def test_plane_webhook_creates_task(mock_docs, mock_branch): - """work_item.created → task in DB with stage=analysis.""" +def test_plane_webhook_creates_task(mock_docs, mock_branch, mock_fetch_fields, mock_fetch_seq, mock_add_comment): + """work_item.created (via In Progress status) → task in DB with stage=analysis.""" resp = client.post("/webhook/plane", json={ - "event": "work_item.created", - "data": {"id": "test-123", "name": "Test task", "project": "proj-1"} + "event": "issue", "action": "updated", + "data": { + "id": "test-123", "name": "Test task", "project": "proj-1", + "state": {"id": "b873d9eb-993c-48cd-97ac-99a9b1623967", "name": "In Progress", "group": "started"}, + } }) assert resp.status_code == 200 assert resp.json()["status"] == "accepted" @@ -202,8 +208,9 @@ def test_gitea_webhook_push(): assert resp.json()["status"] == "accepted" +@patch("src.webhooks.gitea.plane_notify_stage") @patch("src.webhooks.gitea.launcher") -def test_gitea_push_with_adr_advances_stage(mock_launcher): +def test_gitea_push_with_adr_advances_stage(mock_launcher, mock_plane_notify): """Push with ADR files at architecture stage → advance to development.""" mock_launcher.launch.return_value = 1 @@ -235,7 +242,7 @@ def test_gitea_push_with_adr_advances_stage(mock_launcher): task = conn.execute("SELECT * FROM tasks WHERE plane_id = 'push-001'").fetchone() conn.close() assert task["stage"] == "development" - mock_launcher.launch.assert_called_once() + mock_plane_notify.assert_called_once() @patch("src.webhooks.gitea.check_ci_green") -- 2.49.1 From e856e0940b6bdaa4d6da737457765f00448f4ecd Mon Sep 17 00:00:00 2001 From: Dev Agent Date: Thu, 4 Jun 2026 22:38:09 +0300 Subject: [PATCH 2/3] test: migrate sequential_ids test to In Progress contract --- tests/test_webhooks.py | 32 ++++++++++++++++++++++++++------ 1 file changed, 26 insertions(+), 6 deletions(-) diff --git a/tests/test_webhooks.py b/tests/test_webhooks.py index 0ab8b0e..dfcc1fa 100644 --- a/tests/test_webhooks.py +++ b/tests/test_webhooks.py @@ -81,17 +81,37 @@ def test_plane_webhook_creates_task(mock_docs, mock_branch, mock_fetch_fields, m assert "feature/" in task["branch"] +@patch("src.plane_sync.add_comment") +@patch("src.plane_sync.fetch_issue_sequence_id", return_value=None) +@patch("src.plane_sync.fetch_issue_fields", + side_effect=[ + ("First task", "This is a detailed description for the first task item"), + ("Second task", "This is a detailed description for the second task item"), + ]) @patch("src.webhooks.plane._create_gitea_branch", new_callable=AsyncMock) @patch("src.webhooks.plane._create_initial_docs", new_callable=AsyncMock) -def test_plane_webhook_generates_sequential_ids(mock_docs, mock_branch): - """Multiple work items get sequential IDs.""" +def test_plane_webhook_generates_sequential_ids( + mock_docs, mock_branch, mock_fetch_fields, mock_fetch_seq, mock_add_comment +): + """Multiple In Progress transitions get sequential IDs (ET-001, ET-002).""" + in_progress_state = { + "id": "b873d9eb-993c-48cd-97ac-99a9b1623967", + "name": "In Progress", + "group": "started", + } client.post("/webhook/plane", json={ - "event": "work_item.created", - "data": {"id": "item-1", "name": "First task", "project": "proj-1"} + "event": "issue", "action": "updated", + "data": { + "id": "item-1", "name": "First task", "project": "proj-1", + "state": in_progress_state, + } }) client.post("/webhook/plane", json={ - "event": "work_item.created", - "data": {"id": "item-2", "name": "Second task", "project": "proj-1"} + "event": "issue", "action": "updated", + "data": { + "id": "item-2", "name": "Second task", "project": "proj-1", + "state": in_progress_state, + } }) conn = get_db() -- 2.49.1 From 1baae811659d6b6358fea00339b06b7b85bab4ec Mon Sep 17 00:00:00 2001 From: Dev Agent Date: Fri, 5 Jun 2026 00:00:01 +0300 Subject: [PATCH 3/3] test: reset webhook secret per-test to fix cross-file isolation (CI green) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds autouse fixture _reset_webhook_secrets to tests/conftest.py that resets the process-wide Pydantic settings singleton before every test: 1. gitea_webhook_secret / plane_webhook_secret → "" (HMAC disabled by default). Tests that deliberately test the 401 path (test_webhook_dedup.py:268,278) override this with their own monkeypatch which runs after autouse fixtures and wins for that test only. 2. db_path → os.environ["ORCH_DB_PATH"] (last written value after all test modules are imported). Without this, test_webhook_dedup.py (imported first alphabetically) seeds settings.db_path = dedup.db, while test_webhooks.py setup_db tries to remove test_orchestrator.db — leaving the DB dirty between tests that share a branch name and causing get_task_by_repo_branch() to return a stale row with the wrong stage. Per-test monkeypatches in test_webhook_dedup.setup_db still override it. Root cause: both leaks come from the same singleton settings being read once at import, before any per-test isolation runs. The autouse fixture is the correct per-test reset point for process-wide singletons. Result: pytest tests/ → 294 passed, 0 failed (was 10 failed/284 passed). --- tests/conftest.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 991f025..d012ccc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -38,3 +38,36 @@ def _no_telegram(monkeypatch): monkeypatch.setattr("src.agents.launcher.send_telegram", _noop, raising=False) monkeypatch.setattr("src.queue_worker.send_telegram", _noop, raising=False) yield + + +@pytest.fixture(autouse=True) +def _reset_webhook_secrets(monkeypatch): + """Isolate settings singleton between test files (CI cross-file isolation). + + settings is a process-wide Pydantic singleton read once at import. Different + test modules set env variables differently at import-time, so those values leak + across files when pytest collects them together (as CI does). + + 1. webhook secrets: reset to "" so HMAC is disabled by default. Tests that + intentionally test the 401 path (test_webhook_dedup.py:268,278) re-apply + their own monkeypatch AFTER this autouse fixture runs, which overrides the + reset for the duration of that one test only. + + 2. db_path: reset to the value from ORCH_DB_PATH env var (last written by the + last imported test module). Without this, test_webhook_dedup.py (imported + first, alphabetically) seeds settings.db_path = dedup.db, while + test_webhooks.py's setup_db fixture tries to remove test_orchestrator.db, + leaving the DB dirty across tests that share a branch name and causing + get_task_by_repo_branch() to return a stale row with the wrong stage. + Per-test monkeypatches in test_webhook_dedup.setup_db override this reset. + """ + import os + from src.webhooks import gitea as gitea_mod + from src.webhooks import plane as plane_mod + from src import db as db_mod + monkeypatch.setattr(gitea_mod.settings, "gitea_webhook_secret", "", raising=False) + monkeypatch.setattr(plane_mod.settings, "plane_webhook_secret", "", raising=False) + db_path_env = os.environ.get("ORCH_DB_PATH", "") + if db_path_env: + monkeypatch.setattr(db_mod.settings, "db_path", db_path_env, raising=False) + yield -- 2.49.1