diff --git a/.gitea/workflows/snapshot_admin.yml b/.gitea/workflows/snapshot_admin.yml new file mode 100644 index 00000000..bca8fff5 --- /dev/null +++ b/.gitea/workflows/snapshot_admin.yml @@ -0,0 +1,22 @@ +name: Snapshot Admin Validation + +on: + push: + paths: + - "src/quant_engine/snapshot_admin_*.py" + - "tools/validate_snapshot_admin_*.py" + - "tests/unit/test_snapshot_admin_*.py" + - ".gitea/workflows/snapshot_admin.yml" + workflow_dispatch: + +jobs: + validate: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - name: Install Python dependencies + run: python3 -m pip install --disable-pip-version-check --quiet pyyaml pytest + - name: Validate snapshot admin workflow + run: python3 tools/validate_snapshot_admin_workflow_v1.py + - name: Run snapshot admin tests + run: python3 -m pytest tests/unit/test_snapshot_admin_store_v1.py tests/unit/test_snapshot_admin_web_v1.py -q diff --git a/src/quant_engine/snapshot_admin_store_v1.py b/src/quant_engine/snapshot_admin_store_v1.py index 780b19aa..2dddc3a6 100644 --- a/src/quant_engine/snapshot_admin_store_v1.py +++ b/src/quant_engine/snapshot_admin_store_v1.py @@ -908,6 +908,8 @@ def validate_account_snapshot_rows(rows: list[dict[str, Any]]) -> list[str]: errors.append(f"account_snapshot row {idx}: invalid account_type {account_type!r}") if not ticker and name != "예수금/D+2현금": errors.append(f"account_snapshot row {idx}: ticker required") + if ticker and not re.fullmatch(r"(?:\d{6}|[A-Z0-9]{6}|[A-Z]{1,5})", ticker): + errors.append(f"account_snapshot row {idx}: ticker must be 6 digits or an uppercase symbol") if not name: errors.append(f"account_snapshot row {idx}: name required") if parse_status not in ALLOWED_PARSE_STATUS: @@ -936,7 +938,7 @@ def validate_account_snapshot_rows(rows: list[dict[str, Any]]) -> list[str]: errors.append(f"account_snapshot row {idx}: CAPTURE_READ_OK rows require user_confirmed=Y") if entry_stage and canonical.get("entry_stage", {}).get("allowed") and entry_stage not in canonical["entry_stage"]["allowed"]: errors.append(f"account_snapshot row {idx}: invalid entry_stage {entry_stage!r}") - if position_type and canonical.get("position_type", {}).get("allowed") and position_type not in canonical["position_type"]["allowed"]: + if position_type and name != "예수금/D+2현금" and canonical.get("position_type", {}).get("allowed") and position_type not in canonical["position_type"]["allowed"]: errors.append(f"account_snapshot row {idx}: invalid position_type {position_type!r}") return errors diff --git a/tests/unit/test_snapshot_admin_store_v1.py b/tests/unit/test_snapshot_admin_store_v1.py index 0f8d78ce..53a11af5 100644 --- a/tests/unit/test_snapshot_admin_store_v1.py +++ b/tests/unit/test_snapshot_admin_store_v1.py @@ -223,7 +223,7 @@ def test_validation_helpers_detect_invalid_rows(): assert "account_snapshot row 1: ticker required" in validate_account_snapshot_rows( [{"captured_at": "2026-06-21", "account": "real", "name": "삼성전자", "parse_status": "BAD"}] ) - assert "account_snapshot row 1: ticker must be 6 digits" in validate_account_snapshot_rows( + assert "account_snapshot row 1: ticker must be 6 digits or an uppercase symbol" in validate_account_snapshot_rows( [{"captured_at": "2026-06-21", "account": "real", "account_type": "일반계좌", "ticker": "5930", "name": "삼성전자", "parse_status": "NOT_PROVIDED"}] ) assert "account_snapshot row 1: holding_quantity must be >= 0" in validate_account_snapshot_rows(