[verified] add user subset reset scope

(cherry picked from commit e1e3ec96a2d314d39e35ce2c34f6f67df1c2f182)
This commit is contained in:
kcar 2026-06-08 01:22:14 +01:00 committed by CC Worker
parent 7819e6e346
commit 7f7e843563
2 changed files with 92 additions and 3 deletions

View File

@ -5,6 +5,7 @@ Clears:
- Neo4j: drops ALL databases except system, neo4j (including gaisdata, cc.users.*, cc.institutes.*)
- Supabase: deletes ALL data tables except gais_local_authorities and gais_schools
- Supabase: deletes all auth users except kcar, then re-seeds kcar profile state
- Granular scopes can clear exam corpus, timetable data, or --user-subset seed copies
Safe invariants (never touched):
- kcar auth account
@ -261,10 +262,40 @@ def _clear_exam_storage() -> Dict[str, Any]:
return {"removed": removed, "buckets": list(by_bucket)}
def _clear_user_subset_files() -> Dict[str, Any]:
"""Remove files rows and cc.users storage objects created by --user-subset seeding.
Reuses the seed/unseed implementation so reset(scope="user-subset") has the
same storage-before-row deletion order and idempotency guarantees as
seed_exam_corpus.py --unseed. The helper only targets rows marked by the seeder:
bucket='cc.users', source='exam-corpus-seed', path LIKE 'exam-marker/%'.
"""
try:
from modules.database.supabase.utils.client import SupabaseServiceRoleClient
from modules.database.supabase.utils.storage import StorageAdmin
from run.initialization.seed_exam_corpus import LoadReport, _delete_user_subset_files
except Exception as exc:
logger.warning(f" user-subset clear skipped (import): {exc}")
return {"files_rows_deleted": 0, "storage_objects_removed": 0, "errors": [str(exc)]}
rep = LoadReport()
_delete_user_subset_files(
SupabaseServiceRoleClient(),
StorageAdmin(),
exam_codes=None,
rep=rep,
)
return {
"files_rows_deleted": rep.unseed_user_files,
"storage_objects_removed": rep.unseed_objects,
"errors": rep.errors,
}
# ─── Main reset ───────────────────────────────────────────────────────────────
def reset(scope: str = "all") -> Dict[str, Any]:
"""Destructive reset. scope ∈ {all, exam-corpus, timetable}.
"""Destructive reset. scope ∈ {all, exam-corpus, timetable, user-subset}.
- all : full wipe (Neo4j + Supabase data + auth users) AND the entire
exam-marker subsystem listed below.
@ -273,10 +304,12 @@ def reset(scope: str = "all") -> Dict[str, Any]:
templates, template layouts, questions, boundaries, response
areas, marking batches, student submissions, and mark entries.
- timetable : ONLY timetable/calendar materialization tables.
- user-subset : ONLY files rows and cc.users storage objects created by
seed_exam_corpus.py --user-subset.
"""
scope = (scope or "all").lower()
if scope not in ("all", "exam-corpus", "timetable"):
raise ValueError(f"invalid scope {scope!r} (want all|exam-corpus|timetable)")
if scope not in ("all", "exam-corpus", "timetable", "user-subset"):
raise ValueError(f"invalid scope {scope!r} (want all|exam-corpus|timetable|user-subset)")
url, headers = _sb_headers()
_assert_reset_allowed(url, scope)
@ -291,6 +324,11 @@ def reset(scope: str = "all") -> Dict[str, Any]:
cleared, failed = _clear_tables(url, headers, TIMETABLE_TABLES)
return {"scope": scope, "tables_cleared": cleared, "tables_failed": failed}
if scope == "user-subset":
logger.info("RESET (scope=user-subset) — --user-subset cc.users storage objects and files rows")
user_subset = _clear_user_subset_files()
return {"scope": scope, "user_subset": user_subset}
logger.info("=" * 60)
logger.info("RESET ENVIRONMENT — full destructive wipe starting")
logger.info("=" * 60)

View File

@ -0,0 +1,51 @@
from run.initialization import reset_environment
def test_reset_user_subset_scope_only_runs_user_subset_cleanup(monkeypatch):
calls = []
monkeypatch.setattr(
reset_environment,
"_sb_headers",
lambda: ("http://192.168.0.94:8000", {"Authorization": "Bearer redacted"}),
)
monkeypatch.setattr(
reset_environment,
"_assert_reset_allowed",
lambda url, scope: calls.append(("guard", url, scope)),
)
monkeypatch.setattr(
reset_environment,
"_clear_user_subset_files",
lambda: {"files_rows_deleted": 2, "storage_objects_removed": 2, "errors": []},
)
def fail_if_called(*_args, **_kwargs):
raise AssertionError("reset(scope='user-subset') must not clear unrelated tables or databases")
monkeypatch.setattr(reset_environment, "_clear_tables", fail_if_called)
monkeypatch.setattr(reset_environment, "_neo4j_drop_all_non_system", fail_if_called)
monkeypatch.setattr(reset_environment, "_clear_exam_storage", fail_if_called)
result = reset_environment.reset(scope="user-subset")
assert calls == [("guard", "http://192.168.0.94:8000", "user-subset")]
assert result == {
"scope": "user-subset",
"user_subset": {"files_rows_deleted": 2, "storage_objects_removed": 2, "errors": []},
}
def test_reset_accepts_case_insensitive_user_subset_scope(monkeypatch):
monkeypatch.setattr(reset_environment, "_sb_headers", lambda: ("http://192.168.0.94:8000", {}))
monkeypatch.setattr(reset_environment, "_assert_reset_allowed", lambda *_args, **_kwargs: None)
monkeypatch.setattr(
reset_environment,
"_clear_user_subset_files",
lambda: {"files_rows_deleted": 0, "storage_objects_removed": 0, "errors": []},
)
assert reset_environment.reset(scope="USER-SUBSET") == {
"scope": "user-subset",
"user_subset": {"files_rows_deleted": 0, "storage_objects_removed": 0, "errors": []},
}