Files
learningcircuit--local-deep…/tests/database/test_migration_0009_default_fetch_mode.py
wehub-resource-sync 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
chore: import upstream snapshot with attribution
2026-07-13 13:08:55 +08:00

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"