test(validation): 토큰 위생 및 플랫폼 통합 검증 체계 고도화
Snapshot Admin Web Validation / validate-snapshot-admin-smoke (push) Has been cancelled
Snapshot Admin Web Validation / validate-snapshot-admin-full (push) Has been cancelled
Quant Engine CI/CD Pipeline / validate-core (pull_request) Has been cancelled
Quant Engine CI/CD Pipeline / validate-ui-and-storage (pull_request) Has been cancelled
WBS-9.3 - NULL Policy CI Gate / NULL Policy Validation (pull_request) Has been cancelled

This commit is contained in:
2026-06-24 18:06:05 +09:00
parent 9abb8d3bc3
commit 27730704ae
24 changed files with 850 additions and 54 deletions
@@ -12,6 +12,8 @@ from __future__ import annotations
import json
import sys
import urllib.request
import socket
from datetime import date
from pathlib import Path
@@ -21,6 +23,8 @@ if str(ROOT) not in sys.path:
import unittest
import tools.validate_platform_transition_wbs_v1 as platform_transition_validator
import tools.validate_snapshot_admin_web_v1 as snapshot_admin_validator
from src.quant_engine import kis_data_collection_v1 as kdc
from src.quant_engine.data_collection_store_v1 import load_collection_dashboard_state
from src.quant_engine.qualitative_sell_strategy_v1 import compute_qualitative_sell_strategy
@@ -175,3 +179,166 @@ class TestKisCollectionIntegration(unittest.TestCase):
self.assertEqual(fetched[0]["action"], decision["action"])
self.assertEqual(fetched[0]["conviction"], decision["conviction"])
self.assertEqual(fetched[0]["market_regime"], decision["market_regime"])
def test_snapshot_admin_and_platform_transition_validators_remain_passable(self):
"""E2E 체인 결과물이 snapshot_admin 및 platform-transition 검증에 그대로 연결되는지 확인."""
snapshot_out = ROOT / "Temp" / "snapshot_admin_web_validation_v1.json"
transition_out = ROOT / "Temp" / "platform_transition_wbs_v1.json"
if snapshot_out.exists():
snapshot_out.unlink()
if transition_out.exists():
transition_out.unlink()
snapshot_rc = snapshot_admin_validator.main()
transition_rc = platform_transition_validator.main()
snapshot_payload = json.loads(snapshot_out.read_text(encoding="utf-8"))
transition_payload = json.loads(transition_out.read_text(encoding="utf-8"))
self.assertEqual(snapshot_rc, 0)
self.assertEqual(snapshot_payload["gate"], "PASS")
self.assertGreater(snapshot_payload["settings_rows"], 0)
self.assertGreater(snapshot_payload["account_snapshot_rows"], 0)
self.assertEqual(transition_rc, 0)
self.assertEqual(transition_payload["gate"], "PASS")
self.assertFalse(transition_payload["missing_criteria"])
self.assertFalse(transition_payload["roadmap_missing"])
self.assertTrue(transition_payload["checks"]["P1_kis_core_api_collector"]["gate"] == "PASS")
self.assertTrue(transition_payload["checks"]["P2_sqlite_canonical_store"]["gate"] == "PASS")
self.assertTrue(transition_payload["checks"]["P3_ci_scheduler_cutover"]["gate"] == "PASS")
self.assertTrue(transition_payload["checks"]["P4_gas_thin_adapter_minimize"]["gate"] == "PASS")
self.assertTrue(transition_payload["checks"]["P5_postgresql_upgrade_path"]["gate"] == "PASS")
def test_snapshot_admin_collection_run_updates_state_with_live_price(self):
"""실제 수집 후 snapshot_admin state가 최신 run 및 live price를 반영하는지 확인."""
import tempfile
import subprocess
import time
with tempfile.TemporaryDirectory() as tmpdir:
tmp_path = Path(tmpdir)
db_path = tmp_path / "snapshot_admin.db"
seed_path = tmp_path / "seed.json"
seed_payload = {
"data": {
"settings": [
{"ordinal": 1, "key": "total_asset_krw", "value": 500000000, "note": "seed"},
{"ordinal": 2, "key": "settlement_cash_d2_krw", "value": 250000000, "note": "seed"},
],
"data_feed": [
{"Ticker": "005930", "Name": "삼성전자", "Sector": "반도체"},
{"Ticker": "000660", "Name": "SK하이닉스", "Sector": "반도체"},
],
"account_snapshot": [
{
"captured_at": "2026-06-24T11:15:47+09:00",
"account": "real",
"account_type": "일반계좌",
"ticker": "005930",
"name": "삼성전자",
"holding_quantity": 10,
"available_quantity": 10,
"average_cost": 70000,
"total_cost": 700000,
"current_price": 71000,
"market_value": 710000,
"profit_loss": 10000,
"return_pct": 1.43,
"immediate_cash": 0,
"settlement_cash_d2": 0,
"available_cash": 0,
"open_order_amount": 0,
"monthly_contribution_limit": 0,
"monthly_contribution_used": 0,
"parse_status": "CAPTURE_PROVIDED_BUT_NOT_HOLDINGS",
"user_confirmed": "N",
"stop_price": 65000,
"highest_price_since_entry": 72000,
"entry_date": "2026-06-01",
"entry_stage": "stage_1",
"position_type": "core",
"last_updated": "2026-06-24T11:15:47+09:00",
}
],
}
}
seed_path.write_text(json.dumps(seed_payload, ensure_ascii=False, indent=2), encoding="utf-8")
with socket.socket(socket.AF_INET, socket.SOCK_STREAM) as sock:
sock.bind(("127.0.0.1", 0))
port = int(sock.getsockname()[1])
proc = subprocess.Popen(
[
sys.executable,
"-m",
"src.quant_engine.snapshot_admin_server_v1",
"--host",
"127.0.0.1",
"--port",
str(port),
"--db",
str(db_path),
"--seed",
str(seed_path),
],
cwd=ROOT,
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
text=True,
encoding="utf-8",
)
try:
base_url = f"http://127.0.0.1:{port}"
deadline = time.time() + 20
while time.time() < deadline:
if proc.poll() is not None:
output = proc.stdout.read() if proc.stdout else ""
self.fail(f"snapshot_admin server exited early with code {proc.returncode}:\n{output}")
try:
with urllib.request.urlopen(f"{base_url}/api/state", timeout=2) as response:
json.loads(response.read().decode("utf-8"))
break
except Exception:
time.sleep(0.25)
else:
output = proc.stdout.read() if proc.stdout else ""
self.fail(f"snapshot_admin server did not become ready:\n{output}")
request = urllib.request.Request(
f"{base_url}/api/collection/run",
data=json.dumps(
{
"mode": "real",
"kis_account": "real",
"include_live_kis": True,
"allow_naver_fallback": False,
},
ensure_ascii=False,
).encode("utf-8"),
headers={"Content-Type": "application/json"},
method="POST",
)
with urllib.request.urlopen(request, timeout=120) as response:
run_payload = json.loads(response.read().decode("utf-8"))
with urllib.request.urlopen(f"{base_url}/api/state", timeout=10) as response:
state_payload = json.loads(response.read().decode("utf-8"))
collection = state_payload.get("collection", {})
latest_report = collection.get("latest_report", {})
latest_run = collection.get("latest_run", {})
recent_runs = collection.get("runs", [])
self.assertEqual(run_payload["status"], "PASS")
self.assertTrue(any(run.get("run_id") == run_payload["summary"]["run_id"] for run in recent_runs))
self.assertIn(latest_report.get("run_id"), {run_payload["summary"]["run_id"], latest_run.get("run_id")})
self.assertGreaterEqual(latest_report["source_counts"]["kis_open_api"], 1)
self.assertGreaterEqual(len(latest_report.get("rows", [])), 1)
self.assertIsNotNone(latest_report["rows"][0].get("current_price"))
finally:
proc.terminate()
try:
proc.wait(timeout=10)
except Exception:
proc.kill()