mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 06:52:20 +02:00
static/js/document.js is 17,579 lines and is about to be decomposed behind a
re-export wrapper. 51 test files read it off disk and grep it as text, and 28
of those slice it with `src.split("function a", 1)[1].split("function b", 1)[0]`
-- "the region between a and b", which only means what the test intends while a
and b are neighbours in one file. Several also hard-code the file's two-space
indentation, which no extracted module reproduces. Left alone, the first
extraction makes those assertions cover the wrong region, and an `x in region`
check passes while covering more than it was written for.
tests/helpers/document_source is the one place that names the file now:
- document_source() is the entry plus everything under static/js/document/,
so a membership assertion keeps finding its subject wherever it lands;
- function_body()/declaration() locate a construct by name in whichever
module defines it and end at its real closing brace, so neither moving it
nor moving its neighbour changes the region.
The rewrite only collapses a slice when the old terminator sat at the
construct's end. 28 slices deliberately span a whole family of functions --
everything from _docxHexColor to exportAsDocx -- and collapsing one to its
first member drops what the assertions look for, so those stay as they are and
are listed in KNOWN_ADJACENCY_SLICES, to be converted as each family becomes a
module. That list may only shrink.
Two guards come with it:
- test_document_source_test_hygiene fails on a direct read of the entry file
and on any new adjacency slice;
- test_frontend_module_graph resolves every relative import under static/
(718 of them, none broken today) and requires the document module set to
stay in the sw.js precache, since the worker fetches the URLs it lists and
not what they import.
test_document_module_api pins the 38 default-export keys and 29 named exports
by loading the module in a browser and reading what it actually exports, rather
than grepping for the literal object -- after extraction that object may be
assembled from imports, and a source-shape check would pass while the export
was broken.
No JavaScript moves here. static/ is untouched.
183 lines
8.0 KiB
Python
183 lines
8.0 KiB
Python
from pathlib import Path
|
|
from tests.helpers.document_source import document_source
|
|
|
|
|
|
_REPO = Path(__file__).resolve().parents[1]
|
|
_EMAIL_LIBRARY = _REPO / "static" / "js" / "emailLibrary.js"
|
|
_EMAIL_ROUTES = _REPO / "routes" / "email_routes.py"
|
|
_EMAIL_MCP_SERVER = _REPO / "mcp_servers" / "email_server.py"
|
|
_EMAIL_FIXTURE_HELPER = _REPO / "scripts" / "ody_eval_email_fixture.py"
|
|
|
|
|
|
def _bulk_action_source() -> str:
|
|
text = _EMAIL_LIBRARY.read_text(encoding="utf-8")
|
|
start = text.index("async function _bulkAction(action)")
|
|
end = text.index("\n}\n\n// _extractName", start) + 3
|
|
return text[start:end]
|
|
|
|
|
|
def _function_source(name: str) -> str:
|
|
text = _EMAIL_LIBRARY.read_text(encoding="utf-8")
|
|
start = text.index(f"function {name}")
|
|
next_function = text.find("\nfunction ", start + 1)
|
|
next_async = text.find("\nasync function ", start + 1)
|
|
candidates = [idx for idx in (next_function, next_async) if idx != -1]
|
|
end = min(candidates) if candidates else len(text)
|
|
return text[start:end]
|
|
|
|
|
|
def test_email_bulk_read_unread_calls_provider_write_routes():
|
|
"""Bulk read/unread must persist to IMAP/provider, not only mutate UI state.
|
|
|
|
Regression for issue #800's email follow-up: list select -> Actions ->
|
|
Mark Read used to update `em.is_read` locally and cache that fake state,
|
|
then refresh from the provider made the message unread again.
|
|
"""
|
|
src = _bulk_action_source()
|
|
|
|
assert "Local toggle for now" not in src
|
|
assert "mark-read" in src
|
|
assert "mark-unread" in src
|
|
assert "method: 'POST'" in src
|
|
assert "_syncEmailReadState(uid, action === 'read')" in src
|
|
|
|
|
|
def test_email_bulk_read_unread_checks_backend_success_before_syncing_cache():
|
|
src = _bulk_action_source()
|
|
|
|
assert "data?.success === false" in src
|
|
assert "throw new Error(data?.error" in src
|
|
assert "_libCacheWriteBack()" in src
|
|
|
|
|
|
def test_email_bulk_export_attachments_is_ui_only_selected_context():
|
|
frontend = _EMAIL_LIBRARY.read_text(encoding="utf-8")
|
|
backend = _EMAIL_ROUTES.read_text(encoding="utf-8")
|
|
export_src = frontend[
|
|
frontend.index("async function _exportSelectedAttachments()"):
|
|
frontend.index("\n}\n\n// URL-suffix helper", frontend.index("async function _exportSelectedAttachments()")) + 3
|
|
]
|
|
payload_src = _function_source("_selectedEmailExportPayload")
|
|
|
|
assert "Export Attachments" in frontend
|
|
assert "All in view" in frontend
|
|
assert "/api/email/attachments-download-bulk" in export_src
|
|
assert "method: 'POST'" in export_src
|
|
assert "res.blob()" in export_src
|
|
assert "account_id: String(em.account_id || state._libAccountId || '')" in payload_src
|
|
assert "folder: String(em.folder || state._libFolder || 'INBOX')" in payload_src
|
|
|
|
assert '@router.post("/attachments-download-bulk")' in backend
|
|
assert "Download visible attachments from selected emails as one zip archive." in backend
|
|
assert "_fixture_email_read(uid, folder, owner)" in backend
|
|
assert "_is_likely_signature_image_attachment(att)" in backend
|
|
|
|
|
|
def test_fixture_tag_filter_supports_receipt_tag_flow():
|
|
backend = _EMAIL_ROUTES.read_text(encoding="utf-8")
|
|
|
|
assert 'row.get("tags") or row.get("category_tags") or []' in backend
|
|
assert 'elif str(filter_).startswith("tag:"):' in backend
|
|
assert 'rows = [e for e in rows if tag_name in (e.get("tags") or [])]' in backend
|
|
|
|
|
|
def test_email_context_changes_clear_bulk_selection_state():
|
|
"""IMAP UIDs are folder/account scoped, so stale bulk selections must die.
|
|
|
|
Folder, account, filter, quick-filter, attachment, and search basis changes
|
|
must exit select mode before the next list/search view can run bulk actions.
|
|
"""
|
|
text = _EMAIL_LIBRARY.read_text(encoding="utf-8")
|
|
reset_src = _function_source("_resetBulkSelectionForContextChange")
|
|
fresh_src = _function_source("_resetEmailListForFreshLoad")
|
|
add_pill_src = _function_source("_addSearchPill")
|
|
remove_pill_src = _function_source("_removeSearchPillAt")
|
|
search_src = text[text.index("async function _doSearch()"):text.index("// Custom dropdown", text.index("async function _doSearch()"))]
|
|
|
|
assert "state._selectedUids.clear()" in reset_src
|
|
assert "state._selectMode = false" in reset_src
|
|
assert "_updateBulkBar()" in reset_src
|
|
|
|
assert "_resetBulkSelectionForContextChange()" in fresh_src
|
|
assert "_resetBulkSelectionForContextChange({ rerender: true })" in add_pill_src
|
|
assert "_resetBulkSelectionForContextChange({ rerender: true })" in remove_pill_src
|
|
assert "_resetBulkSelectionForContextChange({ rerender: true })" in search_src
|
|
|
|
assert "state._libFolder = _resolveEmailFolderAlias(e.target.value);" in text
|
|
assert "state._libFilter = e.target.value;" in text
|
|
assert "state._libHasAttachments = !state._libHasAttachments;" in text
|
|
assert "const nextAccountId = btn.dataset.accId || null;" in text
|
|
assert "state._libAccountId = nextAccountId;" in text
|
|
assert text.count("_loadEmailsFresh();") >= 5
|
|
assert "state._libSearchDraft = input.value;" in text
|
|
|
|
|
|
def test_email_refresh_uses_explicit_server_refresh_contract():
|
|
"""Manual refresh must bypass more than the browser/list response cache.
|
|
|
|
The server has a durable index and pooled IMAP handles for speed. The
|
|
refresh button should keep the old rows visible while asking the server to
|
|
evict those fast paths and refetch the visible mailbox slice.
|
|
"""
|
|
frontend = _EMAIL_LIBRARY.read_text(encoding="utf-8")
|
|
backend = _EMAIL_ROUTES.read_text(encoding="utf-8")
|
|
|
|
assert "refresh=1&_=${Date.now()}" in frontend
|
|
assert "refresh: int = Query(0)" in backend
|
|
assert "manual_refresh = bool(refresh)" in backend
|
|
assert "_invalidate_list_cache(account_id, folder)" in backend
|
|
assert "_invalidate_imap_pool(account_id, owner)" in backend
|
|
assert "cached_rows = {} if refresh else _email_index_rows" in backend
|
|
|
|
|
|
def test_fixture_email_requires_explicit_eval_flag():
|
|
"""A leftover fixture file must not replace a user's real mailbox.
|
|
|
|
The deterministic fixture mailbox is useful for evals, but production
|
|
routes and MCP tools must not activate it merely because
|
|
data/fixture_email_messages.json exists.
|
|
"""
|
|
routes = _EMAIL_ROUTES.read_text(encoding="utf-8")
|
|
mcp = _EMAIL_MCP_SERVER.read_text(encoding="utf-8")
|
|
helper = _EMAIL_FIXTURE_HELPER.read_text(encoding="utf-8")
|
|
|
|
guard = 'os.environ.get("ODYSSEUS_EMAIL_FIXTURE") == "1" and _fixture_email_file().exists()'
|
|
assert guard in routes
|
|
assert guard in mcp
|
|
assert 'old_fixture_env = os.environ.get("ODYSSEUS_EMAIL_FIXTURE")' in helper
|
|
assert 'os.environ["ODYSSEUS_EMAIL_FIXTURE"] = "1"' in helper
|
|
assert 'os.environ.pop("ODYSSEUS_EMAIL_FIXTURE", None)' in helper
|
|
|
|
|
|
def test_email_client_cache_drops_fixture_rows():
|
|
"""Old fixture rows in browser storage must not keep rendering."""
|
|
frontend = _EMAIL_LIBRARY.read_text(encoding="utf-8")
|
|
|
|
assert "function _looksLikeFixtureEmailRow(row)" in frontend
|
|
assert "function _libCacheHasFixtureRows(value)" in frontend
|
|
assert "function _libDropCacheKey(key)" in frontend
|
|
assert "older inbox message" in frontend
|
|
assert "fixture-email-" in frontend
|
|
assert "if (_libCacheHasFixtureRows(memory))" in frontend
|
|
assert "if (_libCacheHasFixtureRows(stored))" in frontend
|
|
assert "if (_libCacheHasFixtureRows(value))" in frontend
|
|
|
|
|
|
def test_email_compose_can_attach_gallery_images():
|
|
"""Compose attachments should support local files, documents, and Gallery images."""
|
|
frontend_text = document_source()
|
|
backend = _EMAIL_ROUTES.read_text(encoding="utf-8")
|
|
|
|
assert "Upload from computer" in frontend_text
|
|
assert 'data-ody-attach-kind="document"' in frontend_text
|
|
assert "Gallery images" in frontend_text
|
|
assert 'data-ody-attach-kind="gallery"' in frontend_text
|
|
assert "api/gallery/library" in frontend_text
|
|
assert "compose-from-odysseus" in frontend_text
|
|
|
|
assert 'kind not in {"document", "gallery"}' in backend
|
|
assert 'if kind == "gallery":' in backend
|
|
assert "GalleryImage" in backend
|
|
assert "_gallery_image_path(img.filename)" in backend
|
|
assert "_stage_compose_file" in backend
|