From 1ae8b8d9d2138193a95e7f889dce136e733ddabc Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 07:55:23 +0000 Subject: [PATCH 1/2] =?UTF-8?q?fix(webapp):=20=D7=94=D7=99=D7=99=D7=91?= =?UTF-8?q?=D7=95=D7=90=20=D7=97=D7=95=D7=96=D7=A8=20=D7=90=D7=97=D7=A8?= =?UTF-8?q?=D7=99=20=D7=94=D7=9B=D7=A0=D7=AA=20sys.path=20=E2=80=94=20?= =?UTF-8?q?=D7=94=D7=A4=D7=A8=D7=95=D7=93=D7=A7=D7=A9=D7=9F=20=D7=A7=D7=A8?= =?UTF-8?q?=D7=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **הפלתי את הווב.** ``ModuleNotFoundError: No module named 'sticky_notes_target'`` בכל boot של גוניקורן, בלולאה. השורש: גוניקורן טוען ``app:app`` מתוך ``webapp/``, כלומר ``sys.path[0]`` הוא ``webapp/`` ולא שורש הפרויקט. הקובץ פותר את זה בעצמו בשורה 159 — ``sys.path.insert(0, ROOT_DIR)`` — ויש שם שתי הערות שאומרות את זה במפורש, כולל "מייבא לאחר הוספת ROOT_DIR ל-PYTHONPATH כדי למנוע כשל ייבוא בדיפלוי". שמתי את הייבוא בשורה 21, לפני ההכנה, ועברתי ליד האזהרה. **למה זה קרה:** ממצא ריוויו ביקש להסיר fallback שקט סביב הייבוא. הסרתי — נכון — אבל ייבוא ישיר באמצע הקובץ מייצר E402, ובחרתי "לתקן" אותו בהעברה לראש הקובץ במקום להוסיף ``noqa``. זו הייתה החלטה שלי, לא של הריוויו, והיא אופטימיזציה ל-linter על חשבון סדר טעינה אמיתי. ה-``noqa`` כאן אינו השתקה אלא הסימון הנכון לייבוא שתלוי בהכנת נתיב — בדיוק כמו שאר הייבואים באותו בלוק. **למה הבדיקה שלי לא תפסה:** הרצתי ``import webapp.app`` משורש הריפו, ושם השורש כבר ב-``sys.path``. אימות שאינו מריץ כמו שהצרכן מריץ אינו אימות. **אימות הפעם:** ``cd webapp && python3 -c "import app"`` — בדיוק מה שגוניקורן עושה. שוחזרה אותה שגיאה לפני התיקון, ואחריו הטעינה עוברת. נבדקו גם שני מקומות נוספים שנגעתי בהם באותו סבב: - ``mcp_server/handlers.py`` — בטוח. נטען תמיד כחבילה מהשורש, ו- ``mcp_server/app.py`` בכלל לא ניתן להרצה מתוך התיקייה (ייבוא יחסי). - ``webapp/sticky_notes_api.py`` — בטוח, כי הוא נטען אחרי ``app.py``. נוספה הערה שמתעדת את התלות במקום להשאיר אותה מרומזת. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019YULCppaQRPYN1RgeY6NBu --- webapp/app.py | 9 ++++++--- webapp/sticky_notes_api.py | 6 ++++++ 2 files changed, 12 insertions(+), 3 deletions(-) diff --git a/webapp/app.py b/webapp/app.py index aa06d3a96..65e89332e 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -16,9 +16,6 @@ import inspect import socket -# תקרת אורך פתק, לשימוש בתבניות. ייבוא ישיר ובלי fallback — ערך מקומי היה -# מחזיר מספר שני בצד השרת, בדיוק מה שהאיחוד בא למנוע. -from sticky_notes_target import MAX_NOTE_CHARS as MAX_NOTE_CHARS_FOR_TEMPLATES from datetime import datetime, timezone from functools import wraps, lru_cache from types import SimpleNamespace @@ -174,6 +171,12 @@ def _env_log_level_name(default: str = "INFO") -> str: # מייבא לאחר הוספת ROOT_DIR ל-PYTHONPATH כדי למנוע כשל ייבוא בדיפלוי from http_sync import request as http_request # noqa: E402 +# תקרת אורך פתק, לשימוש בתבניות. **חייב להיות כאן ולא בראש הקובץ**: בדיפלוי +# גוניקורן טוען ``app:app`` מתוך ``webapp/``, ושורש הפרויקט נכנס ל-``sys.path`` +# רק בשורה שמעל. ייבוא בראש הקובץ הפיל את כל השירות ב-ModuleNotFoundError. +# ה-``noqa`` כאן אינו השתקה של lint אלא הסימון הנכון לייבוא שתלוי בהכנת נתיב. +from sticky_notes_target import MAX_NOTE_CHARS as MAX_NOTE_CHARS_FOR_TEMPLATES # noqa: E402 + # נרמול טקסט/קוד לפני שמירה (הסרת תווים נסתרים, כיווניות, אחידות שורות) from utils import normalize_code, TimeUtils, detect_language_from_filename # noqa: E402 from user_stats import user_stats # noqa: E402 diff --git a/webapp/sticky_notes_api.py b/webapp/sticky_notes_api.py index eaf96625c..071722fef 100644 --- a/webapp/sticky_notes_api.py +++ b/webapp/sticky_notes_api.py @@ -21,6 +21,12 @@ # שמחזיר ערך מקומי הוא בדיוק מקור האמת השני שהשינוי הזה בא לבטל — הוא היה # שקט, ומופיע רק כשהמגבלות כבר נבדלות בין ה-API ל-MCP. כשל ייבוא צריך # להיכשל מוקדם ובקול. +# +# הייבוא הזה תלוי בכך ש-``webapp/app.py`` כבר הוסיף את שורש הפרויקט ל- +# ``sys.path``. זה מתקיים בכל מסלול טעינה אמיתי — המודול נטען תמיד כ- +# ``webapp.sticky_notes_api``, ו-``app.py`` רושם את ה-blueprint הרבה אחרי +# הכנת הנתיב. הערה ולא ניחוש: אומת בהרצת ``import app`` מתוך ``webapp/``, +# בדיוק כמו שגוניקורן טוען אותו בפרודקשן. from sticky_notes_target import MAX_NOTE_CHARS # Robust ObjectId/InvalidId import with fallbacks for stub environments try: # type: ignore From b0b4eb338b298d92edb1b407a20b59986ce9a3d1 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 21 Aug 2026 08:11:46 +0000 Subject: [PATCH 2/2] =?UTF-8?q?test(webapp):=20=D7=A9=D7=95=D7=9E=D7=A8=20?= =?UTF-8?q?=D7=A2=D7=9C=20=D7=A1=D7=93=D7=A8=20=D7=94=D7=98=D7=A2=D7=99?= =?UTF-8?q?=D7=A0=D7=94,=20=D7=95=D7=94=D7=94=D7=A1=D7=91=D7=A8=20=D7=A2?= =?UTF-8?q?=D7=95=D7=91=D7=A8=20=D7=9C=D7=9E=D7=A7=D7=95=D7=9D=20=D7=90?= =?UTF-8?q?=D7=97=D7=93?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit **ממצא 1 — ההערות ארוכות ודחוסות.** תקף. היו תשע שורות הערה בשני קבצים שמסבירות את אותו דבר. עכשיו ההסבר המלא חי במקום אחד — ``tests/test_webapp_import_paths.py`` — ובקוד נשארו שתי שורות שמצביעות אליו. **ממצא 2 — ``ensure_project_root_in_path()``.** נבדק ולא מומש, והנה הסיבה: ה-helper אינו ניתן לייבוא בנקודה שבה הוא נחוץ. מתוך ``webapp/``, עם ``sys.path[0]`` שהוא התיקייה עצמה: import project_root → No module named 'project_root' from webapp import _bootstrap → No module named 'webapp' ביצה ותרנגולת: ייבוא ה-helper שמתקן את הנתיב דורש שהנתיב כבר תוקן. הוא היה מחייב ``try/except ImportError`` כפול בכל אתר קריאה — שביר יותר משורת ``noqa`` אחת ליד בלוק שכבר מתועד. **אבל החשש נכון**, והמנגנון שכן סוגר את מחלקת הבאגים הוא בדיקה שנופלת ב-CI במקום בפרודקשן. helper הוא עדיין משהו שצריך לזכור לקרוא לו — אותה מחלקה בדיוק. בדיקה לא שוכחת. שתי בדיקות נוספו: 1. ``cd webapp && python -c "import app"`` בתת-תהליך — בדיוק פקודת הטעינה של גוניקורן. זו הבדיקה היחידה שיכולה ליפול על סדר ייבוא שגוי, כי היא היחידה שרצה בסביבה הנכונה. 2. בדיקה סטטית שמצביעה על השורה המדויקת, בלי תת-תהליך. **מוטציה:** החזרתי בדיוק את הייבוא ששבר את הפרודקשן. שתיהן נפלו, והשנייה אמרה: "ייבוא ממודול-שורש לפני הכנת ה-sys.path (שורה 157) — יפיל את גוניקורן: [(18, 'from sticky_notes_target import ...')]". flake8 זהה ל-main בשני הקבצים. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019YULCppaQRPYN1RgeY6NBu --- tests/test_webapp_import_paths.py | 84 +++++++++++++++++++++++++++++++ webapp/app.py | 6 +-- webapp/sticky_notes_api.py | 13 ++--- 3 files changed, 89 insertions(+), 14 deletions(-) create mode 100644 tests/test_webapp_import_paths.py diff --git a/tests/test_webapp_import_paths.py b/tests/test_webapp_import_paths.py new file mode 100644 index 000000000..f40a522f2 --- /dev/null +++ b/tests/test_webapp_import_paths.py @@ -0,0 +1,84 @@ +"""שומר על סדר הטעינה של ``webapp/app.py``. + +**למה הקובץ הזה קיים:** גוניקורן טוען בפרודקשן ``app:app`` **מתוך +``webapp/``**, ולכן ``sys.path[0]`` הוא ``webapp/`` ולא שורש הפרויקט. +מודולים כמו ``sticky_notes_target`` יושבים בשורש, וניתן לייבא אותם רק אחרי +ש-``app.py`` מוסיף את ``ROOT_DIR`` ל-``sys.path``. + +ייבוא כזה שהוצב בראש הקובץ הפיל את כל השירות ב-``ModuleNotFoundError``, +בלולאת boot. הבדיקה שלא תפסה את זה הרצה ``import webapp.app`` **משורש +הריפו** — ושם השורש כבר בנתיב, אז היא עברה. + +הבדיקה כאן מריצה בדיוק כמו שגוניקורן מריץ, ולכן היא היחידה שיכולה ליפול +על סדר ייבוא שגוי. + +**למה לא ``ensure_project_root_in_path()``:** נשקל ונבדק. helper כזה אינו +ניתן לייבוא בנקודה שבה הוא נחוץ — מתוך ``webapp/`` גם ``import +project_root`` וגם ``from webapp import _bootstrap`` נכשלים, כי בדיוק זה +המצב שהוא בא לתקן. הוא היה מחייב ``try/except ImportError`` כפול בכל אתר +קריאה, וזה שביר יותר משורת ``noqa`` אחת ליד בלוק שכבר מתועד. מה שכן סוגר +את מחלקת הבאגים הוא בדיקה שנכשלת ב-CI במקום בפרודקשן — כלומר הקובץ הזה. +""" + +from __future__ import annotations + +import subprocess +import sys +from pathlib import Path + +import pytest + +REPO_ROOT = Path(__file__).resolve().parent.parent +WEBAPP_DIR = REPO_ROOT / "webapp" + + +def test_app_imports_the_way_gunicorn_loads_it(): + """``cd webapp && python -c "import app"`` — בדיוק פקודת הטעינה בפרודקשן. + + נופלת על כל ייבוא של מודול-שורש שהוצב לפני ``sys.path.insert(0, + ROOT_DIR)`` ב-``app.py``. + """ + proc = subprocess.run( + [sys.executable, "-c", "import app"], + cwd=str(WEBAPP_DIR), + capture_output=True, + text=True, + timeout=280, + ) + + if proc.returncode != 0: + tail = (proc.stderr or "").strip().splitlines()[-12:] + pytest.fail( + "‏app.py אינו נטען מתוך webapp/ — כך גוניקורן טוען אותו בפרודקשן.\n" + "‏ייבוא של מודול משורש הפרויקט חייב לשבת **אחרי** " + "‏sys.path.insert(0, ROOT_DIR), עם # noqa: E402.\n\n" + "\n".join(tail) + ) + + +def test_root_module_imports_come_after_the_path_setup(): + """הבדיקה הסטטית שמסבירה **למה** נפל, כשהראשונה אומרת **ש**נפל. + + היא זולה, רצה בלי תת-תהליך, ומצביעה על השורה המדויקת. + """ + source = (WEBAPP_DIR / "app.py").read_text(encoding="utf-8") + lines = source.splitlines() + + setup_line = next( + (i for i, ln in enumerate(lines) if "sys.path.insert(0, ROOT_DIR)" in ln), + None, + ) + assert setup_line is not None, "‏הכנת ה-sys.path נעלמה מ-app.py" + + # מודולים שיושבים בשורש הריפו ואינם נגישים מ-webapp/ בלי ההכנה + root_modules = ("sticky_notes_target", "sticky_notes_scope", "user_roles", "note_boards") + offenders = [ + (i + 1, ln.strip()) + for i, ln in enumerate(lines[:setup_line]) + if ln.startswith(("from ", "import ")) + and any(f"{m} import" in ln or ln.endswith(m) for m in root_modules) + ] + + assert not offenders, ( + "‏ייבוא ממודול-שורש לפני הכנת ה-sys.path (שורה " + f"{setup_line + 1}) — יפיל את גוניקורן: {offenders}" + ) diff --git a/webapp/app.py b/webapp/app.py index 65e89332e..d560f1c74 100644 --- a/webapp/app.py +++ b/webapp/app.py @@ -171,10 +171,8 @@ def _env_log_level_name(default: str = "INFO") -> str: # מייבא לאחר הוספת ROOT_DIR ל-PYTHONPATH כדי למנוע כשל ייבוא בדיפלוי from http_sync import request as http_request # noqa: E402 -# תקרת אורך פתק, לשימוש בתבניות. **חייב להיות כאן ולא בראש הקובץ**: בדיפלוי -# גוניקורן טוען ``app:app`` מתוך ``webapp/``, ושורש הפרויקט נכנס ל-``sys.path`` -# רק בשורה שמעל. ייבוא בראש הקובץ הפיל את כל השירות ב-ModuleNotFoundError. -# ה-``noqa`` כאן אינו השתקה של lint אלא הסימון הנכון לייבוא שתלוי בהכנת נתיב. +# תקרת אורך פתק, לשימוש בתבניות. חייב לשבת כאן ולא בראש הקובץ — +# ראו tests/test_webapp_import_paths.py, ששומר על הכלל ונופל אם הוא מופר. from sticky_notes_target import MAX_NOTE_CHARS as MAX_NOTE_CHARS_FOR_TEMPLATES # noqa: E402 # נרמול טקסט/קוד לפני שמירה (הסרת תווים נסתרים, כיווניות, אחידות שורות) diff --git a/webapp/sticky_notes_api.py b/webapp/sticky_notes_api.py index 071722fef..5f29b8375 100644 --- a/webapp/sticky_notes_api.py +++ b/webapp/sticky_notes_api.py @@ -17,16 +17,9 @@ import threading import asyncio -# תקרת אורך התוכן. ייבוא ישיר ובלי fallback במכוון: ``except Exception`` -# שמחזיר ערך מקומי הוא בדיוק מקור האמת השני שהשינוי הזה בא לבטל — הוא היה -# שקט, ומופיע רק כשהמגבלות כבר נבדלות בין ה-API ל-MCP. כשל ייבוא צריך -# להיכשל מוקדם ובקול. -# -# הייבוא הזה תלוי בכך ש-``webapp/app.py`` כבר הוסיף את שורש הפרויקט ל- -# ``sys.path``. זה מתקיים בכל מסלול טעינה אמיתי — המודול נטען תמיד כ- -# ``webapp.sticky_notes_api``, ו-``app.py`` רושם את ה-blueprint הרבה אחרי -# הכנת הנתיב. הערה ולא ניחוש: אומת בהרצת ``import app`` מתוך ``webapp/``, -# בדיוק כמו שגוניקורן טוען אותו בפרודקשן. +# תקרת אורך התוכן — מקור אמת אחד, בלי fallback שקט. +# תלוי בכך ש-``app.py`` כבר הכין את ``sys.path``; ראו את ההסבר המלא ב- +# tests/test_webapp_import_paths.py. from sticky_notes_target import MAX_NOTE_CHARS # Robust ObjectId/InvalidId import with fallbacks for stub environments try: # type: ignore