From 10af310070d8b04e9e7fb64806a2031493dbb740 Mon Sep 17 00:00:00 2001 From: rrs <276464689+robotrocketscience@users.noreply.github.com> Date: Sat, 2 May 2026 20:35:17 -0700 Subject: [PATCH 1/2] test(e2e): add v1.4 snapshot fixture + builder for #334 scenario 5 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The snapshot is built from the v1.4.0 PyPI release seeded with three distinctive lock statements (quokka/aardvark/wibble). build-v14-snapshot.sh regenerates the fixture deterministically from a fresh aelfrice==1.4.0 install — re-run when the v1.4 install path changes or to verify reproducibility. Unignore tests/e2e/fixtures/*.db so the binary fixture checks in (avoids requiring PyPI access at test time). --- .gitignore | 1 + tests/e2e/fixtures/build-v14-snapshot.sh | 33 +++++++++++++++++++++++ tests/e2e/fixtures/v14-snapshot.db | Bin 0 -> 126976 bytes 3 files changed, 34 insertions(+) create mode 100755 tests/e2e/fixtures/build-v14-snapshot.sh create mode 100644 tests/e2e/fixtures/v14-snapshot.db diff --git a/.gitignore b/.gitignore index 0459fbd48..33ba5fbb6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,4 +10,5 @@ build/ *.db *.db-wal *.db-shm +!tests/e2e/fixtures/*.db telemetry/ diff --git a/tests/e2e/fixtures/build-v14-snapshot.sh b/tests/e2e/fixtures/build-v14-snapshot.sh new file mode 100755 index 000000000..758b75514 --- /dev/null +++ b/tests/e2e/fixtures/build-v14-snapshot.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# Regenerate tests/e2e/fixtures/v14-snapshot.db from a clean install of +# aelfrice 1.4.0. Used by scenario #5 of the e2e suite (#334). +# +# Idempotent. Run from the repo root or anywhere — paths resolve to the +# location of this script. +# +# The snapshot is checked in as a binary fixture so the e2e suite does +# not need network access to PyPI at test time. Re-run this script when +# the v1.4 install path changes (rare) or to verify the fixture is +# reproducible. +set -euo pipefail + +here="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +out="${here}/v14-snapshot.db" +build="$(mktemp -d)" +trap 'rm -rf "${build}"' EXIT + +uv venv --python 3.12 "${build}/.venv" >/dev/null +uv pip install --python "${build}/.venv/bin/python" --quiet aelfrice==1.4.0 + +db="${build}/v14.sqlite3" +aelf="${build}/.venv/bin/aelf" + +AELFRICE_DB="${db}" "${aelf}" lock \ + "Quokkas calibrate the knob carefully on Tuesdays." +AELFRICE_DB="${db}" "${aelf}" lock \ + "The aardvark counter resets at midnight." +AELFRICE_DB="${db}" "${aelf}" lock \ + "Wibble pickling requires the canonical protocol header bytes." + +cp "${db}" "${out}" +echo "wrote ${out} ($(wc -c <"${out}") bytes)" diff --git a/tests/e2e/fixtures/v14-snapshot.db b/tests/e2e/fixtures/v14-snapshot.db new file mode 100644 index 0000000000000000000000000000000000000000..07daa90a81bfe709b9db5e510a96677c6c931c65 GIT binary patch literal 126976 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 aKLNh}zvocf|8Sh Date: Sat, 2 May 2026 20:35:25 -0700 Subject: [PATCH 2/2] =?UTF-8?q?test(e2e):=20scenario=205=20=E2=80=94=20v1.?= =?UTF-8?q?4=20->=20current=20migration=20roundtrip=20(#334)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Loads the v1.4 fixture DB through the installed aelf binary and asserts: 1. aelf search returns each seeded statement's distinctive token after the implicit migration on first connect. 2. The migration adds the post-v1.4 tables that current code requires (belief_corroborations, ingest_log, belief_versions). 3. The original belief rows are preserved verbatim through the additive schema changes. Catches the class of regression where unit tests stay green against fresh-init DBs but a migration silently drops or corrupts existing rows on real on-disk shapes. Advances #334 acceptance gate from 3/4 to 4/4 seed scenarios. --- tests/e2e/test_migration_v14_to_current.py | 185 +++++++++++++++++++++ 1 file changed, 185 insertions(+) create mode 100644 tests/e2e/test_migration_v14_to_current.py diff --git a/tests/e2e/test_migration_v14_to_current.py b/tests/e2e/test_migration_v14_to_current.py new file mode 100644 index 000000000..7f5495c6a --- /dev/null +++ b/tests/e2e/test_migration_v14_to_current.py @@ -0,0 +1,185 @@ +"""E2E scenario #5 (#334): v1.4 fixture DB -> current binary -> reads still work. + +Catches: schema-migration regressions on real DB shapes. Unit-test +suites historically run against fresh-init DBs which exercise only the +current schema; they do not catch a migration that drops or corrupts +existing rows. This test opens a real v1.4 snapshot built from the +v1.4.0 PyPI release (see fixtures/build-v14-snapshot.sh) and asserts: + + 1. The current `aelf search` reads the pre-existing locked beliefs. + 2. The post-migration DB has gained the v1.5/v1.6 tables that the + current schema requires (belief_corroborations, ingest_log). + 3. The pre-existing belief rows survive the upgrade (count + content). + +Boundary rule: invokes the binary as installed (subprocess), not via +in-process imports. The fixture is a binary file, treated read-only — +each test copies it into the ephemeral DB path before running. +""" +from __future__ import annotations + +import shutil +import sqlite3 +import subprocess +from pathlib import Path +from typing import Callable + +import pytest + + +pytestmark = pytest.mark.timeout(120) + +FIXTURE = Path(__file__).parent / "fixtures" / "v14-snapshot.db" + +# The three statements seeded into the v1.4 snapshot by build-v14-snapshot.sh. +# Hard-coded so a regeneration that changes the seed corpus trips review. +SEEDED_STATEMENTS = ( + "Quokkas calibrate the knob carefully on Tuesdays.", + "The aardvark counter resets at midnight.", + "Wibble pickling requires the canonical protocol header bytes.", +) + + +def _table_names(db_path: Path) -> set[str]: + uri = f"file:{db_path}?mode=ro" + with sqlite3.connect(uri, uri=True) as conn: + rows = conn.execute( + "SELECT name FROM sqlite_master WHERE type='table'" + ).fetchall() + return {str(r[0]) for r in rows} + + +def _belief_statements(db_path: Path) -> set[str]: + uri = f"file:{db_path}?mode=ro" + with sqlite3.connect(uri, uri=True) as conn: + rows = conn.execute("SELECT content FROM beliefs").fetchall() + return {str(r[0]) for r in rows} + + +@pytest.fixture +def v14_db(tmp_path: Path) -> Path: + """Working copy of the v1.4 snapshot. The fixture file is read-only; + each test gets its own mutable copy so the binary can migrate it. + """ + if not FIXTURE.exists(): + pytest.skip( + f"v1.4 snapshot missing at {FIXTURE}; " + "run tests/e2e/fixtures/build-v14-snapshot.sh" + ) + dest = tmp_path / "v14-working.sqlite3" + shutil.copyfile(FIXTURE, dest) + return dest + + +def test_v14_snapshot_seeds_are_searchable_after_migration( + installed_aelf, + v14_db: Path, +) -> None: + """Current binary opens the v1.4 DB and `aelf search` returns each + seeded statement. Failure means migration corrupted the belief rows + or broke the FTS index that the search path relies on. + """ + pre_tables = _table_names(v14_db) + # The v1.4 snapshot must lack the post-v1.4 tables; if it doesn't, + # the fixture is stale and the test is no longer testing migration. + assert "belief_corroborations" not in pre_tables, ( + f"fixture {FIXTURE} already has belief_corroborations; " + "regenerate via build-v14-snapshot.sh against aelfrice==1.4.0" + ) + assert "ingest_log" not in pre_tables, ( + f"fixture {FIXTURE} already has ingest_log; " + "regenerate via build-v14-snapshot.sh against aelfrice==1.4.0" + ) + + # Search distinctive tokens unique to each seeded statement. The + # statement bodies appear verbatim in `aelf search` output. + for token in ("quokka", "aardvark", "wibble"): + proc = subprocess.run( # noqa: S603 — argv list, not shell + [*installed_aelf, "search", token], + env={"AELFRICE_DB": str(v14_db), "PATH": _path()}, + capture_output=True, + text=True, + check=True, + timeout=60, + ) + assert token in proc.stdout.lower(), ( + f"expected {token!r} hit in search output; got:\n{proc.stdout!r}" + ) + + +def test_v14_migration_grows_tables_and_preserves_belief_rows( + installed_aelf, + v14_db: Path, +) -> None: + """After the current binary touches the v1.4 DB, the schema must + have the post-v1.4 tables (regression on additive migration) and + the original belief rows must be intact (regression on content). + """ + pre_statements = _belief_statements(v14_db) + assert pre_statements == set(SEEDED_STATEMENTS), ( + f"fixture {FIXTURE} seed corpus drifted; " + f"got {sorted(pre_statements)}" + ) + + # Any read-write operation triggers the migration on first connect. + # `aelf locked` lists locked beliefs and is read-mostly. + subprocess.run( # noqa: S603 + [*installed_aelf, "locked"], + env={"AELFRICE_DB": str(v14_db), "PATH": _path()}, + capture_output=True, + text=True, + check=True, + timeout=60, + ) + + post_tables = _table_names(v14_db) + required_post_tables = { + "belief_corroborations", # v1.5 corroboration tracking + "ingest_log", # v2.0 #205 ingest source-of-truth + "belief_versions", # version-vector backfill + } + missing = required_post_tables - post_tables + assert not missing, ( + f"migration did not add expected tables: {sorted(missing)}; " + f"observed: {sorted(post_tables)}" + ) + + post_statements = _belief_statements(v14_db) + assert post_statements == set(SEEDED_STATEMENTS), ( + f"migration altered belief rows; pre={sorted(pre_statements)}, " + f"post={sorted(post_statements)}" + ) + + +def _path() -> str: + """Minimal PATH for subprocess. `installed_aelf` may be an absolute + path or `uv run aelf`; either way the resolver is the parent's PATH. + """ + import os + + return os.environ.get("PATH", "") + + +def test_search_after_migration_returns_seeded_belief( + aelf_run: Callable[..., subprocess.CompletedProcess[str]], + tmp_path: Path, +) -> None: + """End-to-end variant using the suite's `aelf_run` fixture, which + pins AELFRICE_DB through the same env-overlay path as the other + e2e tests. Catches regressions where the migration would succeed + via direct argv invocation but fail under the fixture's env shape. + """ + if not FIXTURE.exists(): + pytest.skip( + f"v1.4 snapshot missing at {FIXTURE}; " + "run tests/e2e/fixtures/build-v14-snapshot.sh" + ) + # `aelf_run` reads AELFRICE_DB from its own ephemeral_db fixture; we + # have to overwrite that target with the v1.4 snapshot before the + # first invocation so the migration runs on the real fixture data. + db = tmp_path / "aelf.sqlite3" + shutil.copyfile(FIXTURE, db) + + result = aelf_run("search", "wibble") + assert "wibble" in result.stdout.lower(), ( + f"expected wibble hit after migration; stdout:\n{result.stdout!r}" + )