Files
odysseus/tests/test_document_diff_discard_on_update_js.py
T
Alexandre Teixeira f74a262f73 merge: reconcile PR 40 with current lab
Integrate lab fff55a78 into PR #40 (cc25d5ba). Lab's modular email
backend/frontend, modular settings, split stylesheets (static/style.css
stays deleted), procfs compatibility, and request-scoped TurnContract
authority win; PR #40's routing classifiers, editor/email/task features,
and style.css changes are ported into lab's module and stylesheet homes.

Integration fixes:
- settings/api.js imports ui.js under its canonical versioned URL
- browser observations keep legacy CAPTCHA/access-block evidence
- artifact turns do not re-trigger broad-web research recovery
- env reference documents PR test-tool variables; page regenerated

PR #40 defects surfaced by lab gates and fixed here:
- web_fetch generic schema drops top-level anyOf (OpenAI contract);
  the compact preview contract still requires url or urls
- get_weather registered as a brokered network read
- new lazy editor modules precached for offline use
- SearXNG pin mirrored into GPU standalone compose files
- image model picker again skips offline endpoints

Tests updated where PR #40 changed behaviour on purpose, and PR tests
moved onto lab's document_source helpers.
2026-10-01 05:03:58 +01:00

84 lines
3.9 KiB
Python

"""Regression guard for issue #2467 — cross-document overwrite via a stale AI-edit diff.
document.js keeps the AI-edit diff state (``_diffModeActive`` / ``_diffOldContent`` /
``_diffNewContent`` / ``_diffChunks``) as a module-global singleton bound to whatever
document was active when the diff opened. ``handleDocUpdate()`` switches the active
document (``activeDocId``) whenever an AI update targets a different doc. If a pending
diff is not discarded first, a later tab switch (``switchToDoc`` → ``exitDiffMode(true)``)
or Accept/Reject-All flushes the stale diff's content into the now-active document and
silently overwrites it.
The fix discards any pending diff while ``activeDocId`` still points at the
previously-active doc, mirroring the guard ``switchToDoc()`` and ``enterDiffMode()``
already use. It must run in BOTH places that switch the active document for an AI
update: ``handleDocUpdate()`` and ``streamDocOpen()``. The streamed path matters most —
when the AI creates a NEW document (the issue's own repro), ``streamDocOpen`` reassigns
``activeDocId`` first, so a guard only in ``handleDocUpdate`` would fire too late and
still overwrite the new doc. Kept as a static source check because document.js is
browser-coupled and not importable in pytest.
"""
from pathlib import Path
from tests.helpers.document_source import document_source
ROOT = Path(__file__).resolve().parents[1]
DOC_JS = document_source()
GUARD = "if (_diffModeActive) exitDiffMode(true);"
def _function_body(src: str, signature: str) -> str:
"""Return the full text of a JS function, brace-matched from its signature."""
start = src.index(signature)
depth = 0
i = src.index("{", start)
while i < len(src):
if src[i] == "{":
depth += 1
elif src[i] == "}":
depth -= 1
if depth == 0:
return src[start : i + 1]
i += 1
raise AssertionError(f"unbalanced braces after {signature!r}")
HANDLE_DOC_UPDATE = _function_body(DOC_JS, "export function handleDocUpdate(data)")
STREAM_DOC_OPEN = _function_body(DOC_JS, "export function streamDocOpen(title, language)")
def test_handle_doc_update_discards_pending_diff():
# A new AI update on a different document must not leave a stale diff bound
# to the old doc, or a later tab switch / Accept-All overwrites the wrong doc.
assert "if (_diffModeActive) exitDiffMode(true, { persist: data.doc_id !== activeDocId });" in HANDLE_DOC_UPDATE
def test_diff_discard_runs_before_active_doc_is_switched():
# The discard must run while activeDocId still points at the previously
# active doc, so exitDiffMode(true) restores and saves THAT doc — not the new
# one. Any activeDocId reassignment inside handleDocUpdate must come after it.
guard_at = HANDLE_DOC_UPDATE.index("if (_diffModeActive) exitDiffMode(true,")
reassign_at = HANDLE_DOC_UPDATE.index("activeDocId = docId;")
assert guard_at < reassign_at
def test_stream_doc_open_discards_pending_diff_before_switching():
# The AI-creates-a-new-document path switches activeDocId inside
# streamDocOpen (before any doc_update reaches handleDocUpdate), so the guard
# must be here too — and before streamDocOpen reassigns activeDocId, or the
# streamed new doc gets overwritten by the stale diff (the issue's own repro).
assert GUARD in STREAM_DOC_OPEN
assert STREAM_DOC_OPEN.index(GUARD) < STREAM_DOC_OPEN.index("activeDocId = docId;")
def test_diff_discard_reuses_the_existing_idiom():
# Sanity: this exact guard is the established pattern (switchToDoc,
# enterDiffMode, handleDocUpdate, streamDocOpen, …) — the fix reuses it
# rather than inventing a new mechanism.
assert DOC_JS.count(GUARD) >= 4
def test_same_document_update_does_not_save_stale_diff_over_new_server_content():
assert "persist: data.doc_id !== activeDocId" in HANDLE_DOC_UPDATE
assert "if (persist) saveDocument({ silent: true });" in DOC_JS