7a0da7932b
Backwards Compatibility / Verify Encryption Constants (push) Waiting to run
Backwards Compatibility / PyPI Version Compatibility (push) Waiting to run
Backwards Compatibility / Database Migration Tests (push) Waiting to run
CodeQL Advanced / Analyze (javascript-typescript) (push) Waiting to run
CodeQL Advanced / Analyze (python) (push) Waiting to run
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-form] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-metrics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [research-workflow] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [settings-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [history-news] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [library] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [link-analytics] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [mobile] (push) Blocked by required conditions
Docker Tests (Consolidated) / detect-changes (push) Waiting to run
Docker Tests (Consolidated) / Build Test Image (push) Waiting to run
Docker Tests (Consolidated) / All Pytest Tests + Coverage (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [accessibility] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [api-crud] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-login] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-pages] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [auth-register] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-core] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [chat-lifecycle] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) [error-benchmark] (push) Blocked by required conditions
Docker Tests (Consolidated) / UI Tests (Puppeteer) (push) Blocked by required conditions
Docker Tests (Consolidated) / Accessibility Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Unit Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / LLM Example Tests (push) Blocked by required conditions
Docker Tests (Consolidated) / Production Image Smoke Test (push) Blocked by required conditions
Docker Tests (Consolidated) / Infrastructure Tests (push) Blocked by required conditions
OSSF Scorecard / OSSF Security Scorecard Analysis (push) Waiting to run
OSV-Scanner (Scheduled) / scan-scheduled (push) Failing after 0s
Create Release / test-gate (push) Has been cancelled
Create Release / release-gate (push) Has been cancelled
Create Release / ci-gate (push) Has been cancelled
Create Release / version-check (push) Has been cancelled
Create Release / e2e-test-gate (push) Has been cancelled
Create Release / responsive-test-gate (push) Has been cancelled
Create Release / compat-test-gate (push) Has been cancelled
Create Release / compose-integration-gate (push) Has been cancelled
Create Release / vulture-gate (push) Has been cancelled
Create Release / build (push) Has been cancelled
Create Release / provenance (push) Has been cancelled
Create Release / prerelease-docker (push) Has been cancelled
Create Release / publish-docker (push) Has been cancelled
Create Release / create-release (push) Has been cancelled
Create Release / cleanup-changelog (push) Has been cancelled
Create Release / trigger-pypi (push) Has been cancelled
Create Release / monitor-pypi (push) Has been cancelled
Create Release / Clean up orphan prerelease tags and signatures (push) Has been cancelled
221 lines
7.3 KiB
Python
221 lines
7.3 KiB
Python
"""Tests for migration 0009: default search.fetch.mode 'full' → 'summary_focus_query'.
|
|
|
|
Pins the upgrade/downgrade semantics:
|
|
- Rows with the legacy default ``"full"`` get flipped.
|
|
- Rows users explicitly chose (``summary_focus``, ``summary_focus_query``,
|
|
``disabled``) are left untouched.
|
|
- Other settings keys with value ``"full"`` are not affected.
|
|
- Idempotency: a second upgrade is a no-op once values are migrated.
|
|
|
|
The on-disk encoding is JSON-text (``"full"`` with the surrounding
|
|
quotes), so the test inserts through SQLAlchemy's JSON column type to
|
|
match production storage exactly.
|
|
"""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
from alembic import command
|
|
from sqlalchemy import create_engine, text
|
|
|
|
from local_deep_research.database.alembic_runner import (
|
|
get_alembic_config,
|
|
)
|
|
|
|
|
|
def _run_upgrade_to(engine, revision):
|
|
config = get_alembic_config(engine)
|
|
with engine.begin() as conn:
|
|
config.attributes["connection"] = conn
|
|
command.upgrade(config, revision)
|
|
|
|
|
|
def _run_downgrade_to(engine, revision):
|
|
config = get_alembic_config(engine)
|
|
with engine.begin() as conn:
|
|
config.attributes["connection"] = conn
|
|
command.downgrade(config, revision)
|
|
|
|
|
|
def _seed_setting(engine, key, value):
|
|
"""Insert a setting matching production's JSON-text storage.
|
|
|
|
SQLAlchemy's JSON column writes ``json.dumps(value)``; we mirror that
|
|
explicitly so raw SQL produces the same on-disk bytes the migration
|
|
expects to match in its WHERE clause.
|
|
"""
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text(
|
|
"INSERT INTO settings "
|
|
"(key, value, type, name, ui_element, visible, editable) "
|
|
"VALUES (:key, :value, 'search', :name, 'select', 1, 1)"
|
|
),
|
|
{"key": key, "value": json.dumps(value), "name": key},
|
|
)
|
|
|
|
|
|
def _read_setting(engine, key):
|
|
with engine.begin() as conn:
|
|
row = conn.execute(
|
|
text("SELECT value FROM settings WHERE key = :key"),
|
|
{"key": key},
|
|
).fetchone()
|
|
if row is None:
|
|
return None
|
|
raw = row[0]
|
|
# JSON column round-trips through json.loads on read, but raw text()
|
|
# bypasses the column type — so decode manually.
|
|
return json.loads(raw) if isinstance(raw, str) else raw
|
|
|
|
|
|
@pytest.fixture
|
|
def migrated_to_0008_engine(tmp_path):
|
|
"""Database fully migrated through 0008 (the revision before 0009)."""
|
|
db_path = tmp_path / "test_0009.db"
|
|
engine = create_engine(f"sqlite:///{db_path}")
|
|
_run_upgrade_to(engine, "0008")
|
|
yield engine
|
|
engine.dispose()
|
|
|
|
|
|
class TestMigration0009Upgrade:
|
|
def test_full_value_is_migrated_to_summary_focus_query(
|
|
self, migrated_to_0008_engine
|
|
):
|
|
engine = migrated_to_0008_engine
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text("DELETE FROM settings WHERE key = 'search.fetch.mode'")
|
|
)
|
|
_seed_setting(engine, "search.fetch.mode", "full")
|
|
|
|
_run_upgrade_to(engine, "0009")
|
|
|
|
assert (
|
|
_read_setting(engine, "search.fetch.mode") == "summary_focus_query"
|
|
)
|
|
|
|
@pytest.mark.parametrize(
|
|
"explicit_value",
|
|
["summary_focus", "summary_focus_query", "disabled"],
|
|
)
|
|
def test_explicit_non_full_choices_are_preserved(
|
|
self, migrated_to_0008_engine, explicit_value
|
|
):
|
|
engine = migrated_to_0008_engine
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text("DELETE FROM settings WHERE key = 'search.fetch.mode'")
|
|
)
|
|
_seed_setting(engine, "search.fetch.mode", explicit_value)
|
|
|
|
_run_upgrade_to(engine, "0009")
|
|
|
|
assert _read_setting(engine, "search.fetch.mode") == explicit_value
|
|
|
|
def test_other_keys_with_full_value_are_not_touched(
|
|
self, migrated_to_0008_engine
|
|
):
|
|
engine = migrated_to_0008_engine
|
|
|
|
# An unrelated key that happens to hold 'full' must not be flipped.
|
|
_seed_setting(engine, "test.unrelated.mode", "full")
|
|
|
|
_run_upgrade_to(engine, "0009")
|
|
|
|
assert _read_setting(engine, "test.unrelated.mode") == "full"
|
|
|
|
def test_upgrade_is_idempotent(self, migrated_to_0008_engine):
|
|
"""Running the upgrade a second time is a no-op (already at head)."""
|
|
engine = migrated_to_0008_engine
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text("DELETE FROM settings WHERE key = 'search.fetch.mode'")
|
|
)
|
|
_seed_setting(engine, "search.fetch.mode", "full")
|
|
|
|
_run_upgrade_to(engine, "0009")
|
|
# Second invocation: nothing left to do; alembic short-circuits.
|
|
_run_upgrade_to(engine, "0009")
|
|
|
|
assert (
|
|
_read_setting(engine, "search.fetch.mode") == "summary_focus_query"
|
|
)
|
|
|
|
def test_no_settings_row_does_not_error(self, migrated_to_0008_engine):
|
|
"""If the row never existed, the migration is a clean no-op."""
|
|
engine = migrated_to_0008_engine
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text("DELETE FROM settings WHERE key = 'search.fetch.mode'")
|
|
)
|
|
|
|
_run_upgrade_to(engine, "0009")
|
|
|
|
assert _read_setting(engine, "search.fetch.mode") is None
|
|
|
|
|
|
class TestMigration0009Downgrade:
|
|
def test_downgrade_reverts_summary_focus_query_to_full(
|
|
self, migrated_to_0008_engine
|
|
):
|
|
engine = migrated_to_0008_engine
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text("DELETE FROM settings WHERE key = 'search.fetch.mode'")
|
|
)
|
|
_seed_setting(engine, "search.fetch.mode", "full")
|
|
|
|
_run_upgrade_to(engine, "0009")
|
|
_run_downgrade_to(engine, "0008")
|
|
|
|
assert _read_setting(engine, "search.fetch.mode") == "full"
|
|
|
|
@pytest.mark.parametrize("preserved_value", ["summary_focus", "disabled"])
|
|
def test_downgrade_preserves_other_explicit_choices(
|
|
self, migrated_to_0008_engine, preserved_value
|
|
):
|
|
engine = migrated_to_0008_engine
|
|
|
|
with engine.begin() as conn:
|
|
conn.execute(
|
|
text("DELETE FROM settings WHERE key = 'search.fetch.mode'")
|
|
)
|
|
_seed_setting(engine, "search.fetch.mode", preserved_value)
|
|
|
|
_run_upgrade_to(engine, "0009")
|
|
_run_downgrade_to(engine, "0008")
|
|
|
|
assert _read_setting(engine, "search.fetch.mode") == preserved_value
|
|
|
|
|
|
class TestMigration0009HeadAlignment:
|
|
def test_0009_chains_correctly_to_0008(self):
|
|
"""0009 (default_fetch_mode_summary) chains directly off 0008.
|
|
|
|
Originally this asserted ``get_head_revision() == "0009"``,
|
|
but a later migration added 0010 (chat tables, including the
|
|
partial unique chat-in-progress index) on top, so head moved
|
|
past 0009. The substantive invariant the original test was
|
|
protecting — that 0009 is correctly anchored in the chain —
|
|
survives by checking down_revision instead.
|
|
"""
|
|
from alembic.config import Config
|
|
from alembic.script import ScriptDirectory
|
|
|
|
from local_deep_research.database.alembic_runner import (
|
|
get_migrations_dir,
|
|
)
|
|
|
|
config = Config()
|
|
config.set_main_option("script_location", str(get_migrations_dir()))
|
|
script = ScriptDirectory.from_config(config)
|
|
rev_0009 = script.get_revision("0009")
|
|
assert rev_0009.down_revision == "0008"
|