From 3536ef1fbb3dbebdd0c7bc0ae050374f285e2ba9 Mon Sep 17 00:00:00 2001 From: kronecker <2026692687@qq.com> Date: Mon, 12 Oct 2026 03:48:26 +0800 Subject: [PATCH] test(admin): preserve baseline data through maintenance migrations --- backend/tests/test_admin_upgrade_vm.py | 223 +++++++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 backend/tests/test_admin_upgrade_vm.py diff --git a/backend/tests/test_admin_upgrade_vm.py b/backend/tests/test_admin_upgrade_vm.py new file mode 100644 index 0000000..f206172 --- /dev/null +++ b/backend/tests/test_admin_upgrade_vm.py @@ -0,0 +1,223 @@ +"""Approved baseline upgrade/retention: actual PostgreSQL in an independent capped VM.""" + +import asyncio +import hashlib +import json +import os +import subprocess +import sys +from pathlib import Path +from uuid import uuid4 + +from conftest import connect, migrate +from sqlalchemy.engine import make_url +from test_admin_import_vm import independent_import_vm as independent_import_vm +from test_admin_import_vm import package + +from kf_oj.contracts.judging_v2 import AdmissionRegistry + +ROOT = Path(__file__).resolve().parents[2] + + +def policy(): + return AdmissionRegistry.model_validate_json( + (ROOT / "tests/fixtures/contracts-v2/judge-registry.valid.json").read_bytes() + ) + + +def proof(name, value): + print( + "KF_FINAL_ACCEPTANCE " + json.dumps({"name": name, "result": value}, default=str), + flush=True, + ) + + +def test_baseline_upgrade_preserves_existing_data(independent_import_vm, postgres_url, tmp_path): + baseline = Path("/kf/baseline-platform") + assert baseline.is_dir(), "curated approved baseline required" + url = make_url(postgres_url).set(database="kf_oj_test_upgrade_" + uuid4().hex) + rendered = url.render_as_string(hide_password=False) + + async def manage(command): + connection = await connect(postgres_url) + try: + await connection.execute(command) + finally: + await connection.close() + + def old_run(args): + result = subprocess.run( + [sys.executable, *args], + cwd=baseline, + env=os.environ + | { + "PYTHONPATH": "/kf/baseline-platform/src:/kf/site", + "KF_OJ_DATABASE_URL": rendered, + "KF_BASELINE_FIXTURES": str(tmp_path), + }, + capture_output=True, + timeout=300, + ) + assert result.returncode == 0, (result.stdout + result.stderr).decode(errors="replace")[ + -2000: + ] + return result + + # Real old-schema data is created through the old public services. No student + # source executes here; historical report fixtures are explicitly unmeasured. + for multi in (False, True): + directory = tmp_path / ("v2" if multi else "v1") + directory.mkdir(mode=0o700) + _, payload = package(policy(), multi=multi) + for name, data in payload.items(): + (directory / name).write_bytes(data) + (directory / name).chmod(0o600) + seed = """ +import asyncio,hashlib,json,os,secrets +from datetime import datetime,timedelta,timezone +from pathlib import Path +from uuid import uuid4 +from sqlalchemy import text,update +from kf_oj.api.app import create_app +from kf_oj.core.settings import Settings +from kf_oj.contracts.judging import ArtifactReference +from kf_oj.contracts.judging_v2 import AdmissionRegistry +from kf_oj.contracts.judging_versions import manifest_bytes +from kf_oj.contracts.submissions import CreateSubmission +from kf_oj.modules.submissions.models import Evaluation,JudgeAttempt +async def main(): + root=Path(os.environ['KF_BASELINE_FIXTURES']) + fixture=Path('/kf/tests/fixtures/contracts-v2/judge-registry.valid.json') + policy=AdmissionRegistry.model_validate_json(fixture.read_bytes()) + database=os.environ['KF_OJ_DATABASE_URL'] + app=create_app(Settings(_env_file=None,database_url=database, + artifact_root=root/'objects',judge_admission=policy)) + async with app.router.lifespan_context(app): + password='Synthetic upgrade acceptance only 2026!' + user=await app.state.identity.create_account('upgrade_student',password) + for name in ('v1','v2'): + version=await app.state.problems.import_directory(root/name) + request=CreateSubmission(problem_version_id=version,language='c17', + idempotency_key=uuid4(),source='int main(void){return 0;}') + created=await app.state.submissions.create(user,request) + # Controlled, unmeasured historical database fixtures prove preservation of + # reports and their digests. They never claim execution or trusted performance. + async with app.state.sessions.begin() as session: + projected=await app.state.problems.for_submission(session,version) + source=ArtifactReference(digest=hashlib.sha256(request.source.encode()).hexdigest(), + bytes=len(request.source.encode())) + manifest=projected.manifest(created.evaluation_id,created.submission_id,'c17',source) + md=hashlib.sha256(manifest_bytes(manifest)).hexdigest() + aid,wid=uuid4(),uuid4() + template=('contracts' if name=='v1' else 'contracts-v2') + file=('judge-result.valid.json' if name=='v1' else 'judge-result.valid-c17.json') + report=json.loads((Path('/kf/tests/fixtures')/template/file).read_text()) + report['binding']={'evaluation_id':str(created.evaluation_id),'attempt_id':str(aid), + 'worker_id':str(wid),'fencing_version':1} + report['manifest_digest']=md + report['cases']=[{'case_id':str(case.case_id),'verdict':'accepted', + 'cpu_time_us':100,'memory_peak_bytes':262144} + for case in manifest.cases] + report['run_cpu_time_us']=100*len(manifest.cases) + report['run_memory_peak_bytes']=262144 + digest=hashlib.sha256(json.dumps(report,sort_keys=True,separators=(',',':'), + ensure_ascii=False).encode()).hexdigest() + await session.execute(text('SET LOCAL ROLE kf_oj_coordinator')) + session.add(JudgeAttempt(id=aid,evaluation_id=created.evaluation_id,worker_id=wid, + number=1,fencing_version=1,lease_token_hash=hashlib.sha256( + secrets.token_hex(32).encode()).hexdigest(), + expires_at=datetime.now(timezone.utc)+timedelta(minutes=10), + manifest=manifest.model_dump(mode='json'),manifest_digest=md, + report=report,report_digest=digest,cleanup_complete=True)) + await session.flush() + await session.execute(update(Evaluation).where(Evaluation.id==created.evaluation_id) + .values(current_attempt_id=aid,state='finished',attempts_count=1, + fencing_version=1,verdict='accepted',final_report=report, + final_report_digest=digest)) +asyncio.run(main()) +""" + asyncio.run(manage(f'CREATE DATABASE "{url.database}"')) + try: + old_run(["-m", "alembic", "upgrade", "head"]) + old_run(["-c", seed]) + + async def snapshot(columns=None): + connection = await connect(rendered) + try: + if columns is None: + rows = await connection.fetch(""" + SELECT table_name,column_name FROM information_schema.columns + WHERE table_schema='public' AND table_name<>'alembic_version' + ORDER BY table_name,ordinal_position + """) + columns = {} + for row in rows: + columns.setdefault(row["table_name"], []).append(row["column_name"]) + data = {} + for table, names in columns.items(): + query = ( + "SELECT " + ",".join('"' + n + '"' for n in names) + ' FROM "' + table + '"' + ) + records = sorted( + json.dumps(dict(r), sort_keys=True, default=str) + for r in await connection.fetch(query) + ) + data[table] = { + "rows": len(records), + "sha256": hashlib.sha256("\n".join(records).encode()).hexdigest(), + } + return columns, data + finally: + await connection.close() + + columns, before = asyncio.run(snapshot()) + assert before["accounts"]["rows"] == 1 and before["problem_versions"]["rows"] == 2 + assert before["submissions"]["rows"] == 2 + assert before["judge_attempts"]["rows"] == 2 + migrate(rendered, "upgrade", "head") + _, after = asyncio.run(snapshot(columns)) + assert after == before + migrate(rendered, "check") + + async def defaults(): + connection = await connect(rendered) + try: + account = await connection.fetchrow( + "SELECT is_administrator,authorization_version FROM accounts" + ) + assert not account["is_administrator"] and account["authorization_version"] == 1 + assert await connection.fetchval("SELECT count(*) FROM evaluation_performance") == 0 + assert ( + await connection.fetchval("SELECT count(*) FROM problem_maintenance_events") + == 0 + ) + for role in ("kf_oj_identity_operator", "kf_oj_problem_import"): + row = await connection.fetchrow("SELECT * FROM pg_roles WHERE rolname=$1", role) + assert not any( + row[k] + for k in ( + "rolcanlogin", + "rolsuper", + "rolcreatedb", + "rolcreaterole", + "rolreplication", + "rolbypassrls", + ) + ) + finally: + await connection.close() + + asyncio.run(defaults()) + proof( + "baseline_upgrade", + { + "baseline": "5df3c370aab0b12a1c3f8670a0c066f8b77a4078", + "retained_tables": before, + "old_v1_v2_manifests_and_source_bytes": "exact", + "historical_report_retention_fixture_unmeasured": True, + "fixture_reports_not_student_execution_or_performance_acceptance": True, + "fresh_current_database": postgres_url.split("/")[-1].split("?")[0], + }, + ) + finally: + asyncio.run(manage(f'DROP DATABASE "{url.database}"'))