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
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:
@@ -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()
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import sys
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
@@ -88,3 +89,29 @@ def test_intended_price_must_be_positive(tmp_path):
|
||||
actual_fill_price=100,
|
||||
recorded_at="2026-06-21",
|
||||
)
|
||||
|
||||
|
||||
def test_cli_report_is_data_gated_below_minimum_sample(tmp_path):
|
||||
db_path = tmp_path / "execution_slippage.db"
|
||||
report_path = ROOT / "Temp" / "execution_slippage_report_v1.json"
|
||||
if report_path.exists():
|
||||
report_path.unlink()
|
||||
|
||||
result = subprocess.run(
|
||||
[
|
||||
sys.executable,
|
||||
"tools/evaluate_execution_slippage_v1.py",
|
||||
"--db",
|
||||
str(db_path),
|
||||
"report",
|
||||
],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
assert "DATA_GATED" in result.stdout
|
||||
payload = report_path.read_text(encoding="utf-8")
|
||||
assert '"status": "DATA_GATED"' in payload
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(ROOT))
|
||||
|
||||
import tools.validate_kis_token_hygiene_v1 as validator
|
||||
|
||||
|
||||
class TestKisTokenHygieneV1(unittest.TestCase):
|
||||
def test_validator_reports_pass(self):
|
||||
rc = validator.main()
|
||||
self.assertEqual(rc, 0)
|
||||
payload = json.loads((ROOT / "Temp" / "kis_token_hygiene_v1.json").read_text(encoding="utf-8"))
|
||||
self.assertEqual(payload["gate"], "PASS")
|
||||
self.assertIn("sanitized_token_refresh_error", payload["evidence"][str(ROOT / "src" / "quant_engine" / "kis_api_client_v1.py")])
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -4,6 +4,7 @@ import json
|
||||
import sys
|
||||
import unittest
|
||||
from pathlib import Path
|
||||
from unittest.mock import Mock, patch
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[2]
|
||||
if str(ROOT) not in sys.path:
|
||||
@@ -57,6 +58,14 @@ class TestSnapshotAdminWebV1(unittest.TestCase):
|
||||
self.assertIn("/api/account_snapshot/save", html)
|
||||
self.assertIn("opsBanner", html)
|
||||
self.assertIn("bannerApprovalSummary", html)
|
||||
self.assertIn("heroNextAction", html)
|
||||
self.assertIn("heroPrimaryAction", html)
|
||||
self.assertIn("heroCollection", html)
|
||||
self.assertIn("approvalPanel", html)
|
||||
self.assertIn("collectionPanel", html)
|
||||
self.assertIn("selectionPanel", html)
|
||||
self.assertIn("open when you need row-level operations", html)
|
||||
self.assertIn("open", html)
|
||||
self.assertIn("snapshot-panel", html)
|
||||
self.assertIn("selected-field", html)
|
||||
self.assertIn("settingsCountChip", html)
|
||||
@@ -68,6 +77,7 @@ class TestSnapshotAdminWebV1(unittest.TestCase):
|
||||
self.assertIn("Export approval packet", html)
|
||||
self.assertIn("Selection Inspector", html)
|
||||
self.assertIn("Recent row history", html)
|
||||
self.assertIn("Recent change summary", html)
|
||||
self.assertIn("Save view", html)
|
||||
self.assertIn("Apply TSV to selection", html)
|
||||
self.assertIn("Ctrl+S", html)
|
||||
@@ -80,26 +90,238 @@ class TestSnapshotAdminWebV1(unittest.TestCase):
|
||||
self.assertIn("/collection", html)
|
||||
self.assertIn("Open collection dashboard", html)
|
||||
|
||||
def test_render_home_html_contains_role_based_entrances(self):
|
||||
from src.quant_engine.snapshot_admin_server_v1 import render_home_html
|
||||
|
||||
html = render_home_html()
|
||||
self.assertIn("Snapshot Admin Home", html)
|
||||
self.assertIn("1. Workspace", html)
|
||||
self.assertIn("2. Collection", html)
|
||||
self.assertIn("3. Tables", html)
|
||||
self.assertIn("Open workspace", html)
|
||||
self.assertIn("Open collection", html)
|
||||
self.assertIn("Open tables", html)
|
||||
self.assertIn("/workspace", html)
|
||||
self.assertIn("/tables", html)
|
||||
|
||||
def test_render_tables_html_contains_table_group_summary(self):
|
||||
html = render_tables_html()
|
||||
self.assertIn("Snapshot Admin — Table Browser", html)
|
||||
self.assertIn("DB별 수정, JSON별 검토, 수집 증빙 확인", html)
|
||||
self.assertIn("DB 먼저 / JSON은 증빙", html)
|
||||
self.assertIn("tablePurposeNavigator", html)
|
||||
self.assertIn("DB 먼저", html)
|
||||
self.assertIn("JSON은 증빙", html)
|
||||
self.assertIn("Edit only canonical rows", html)
|
||||
self.assertIn("DB tables", html)
|
||||
self.assertIn("Editable source of truth.", html)
|
||||
self.assertIn("Workbook sheets", html)
|
||||
self.assertIn("Derived report evidence.", html)
|
||||
self.assertIn("tablesVersionTop", html)
|
||||
self.assertIn("tablesVersionBottom", html)
|
||||
self.assertIn("final_updated_at=", html)
|
||||
self.assertIn("tablesCoverageWarning", html)
|
||||
self.assertIn("workbookSheetSelect", html)
|
||||
self.assertIn("Workbook sheets only", html)
|
||||
self.assertIn("workbookSheetMeta", html)
|
||||
self.assertIn("workbookSheetSurfaceMeta", html)
|
||||
self.assertIn("workbookSheetDetail", html)
|
||||
self.assertIn("workbookSheetPreview", html)
|
||||
self.assertIn("tableSelect", html)
|
||||
self.assertIn("DB Table", html)
|
||||
self.assertIn("tableGroupSummary", html)
|
||||
self.assertIn("tableSourceSummary", html)
|
||||
self.assertIn("Registry details", html)
|
||||
self.assertIn("History details", html)
|
||||
self.assertIn("Workspace", html)
|
||||
self.assertIn("Collection", html)
|
||||
self.assertIn("Strategy", html)
|
||||
self.assertIn("JSON", html)
|
||||
self.assertIn("tableWorkspaceSection", html)
|
||||
self.assertIn("tableJsonSection", html)
|
||||
self.assertIn("tableEditState", html)
|
||||
self.assertIn("bg-danger-lt", html)
|
||||
self.assertIn("Save applies only to the canonical workspace DB.", html)
|
||||
self.assertIn("Derived JSON Evidence Preview", html)
|
||||
self.assertIn("jsonReportStatus", html)
|
||||
self.assertIn("jsonReportFocus", html)
|
||||
self.assertIn("jsonReportStats", html)
|
||||
self.assertIn("jsonReportDetail", html)
|
||||
self.assertIn("Purpose: edit canonical workspace rows only.", html)
|
||||
self.assertIn("Collection purpose: monitor collector runs and snapshots.", html)
|
||||
self.assertIn("Latest run summary loads from `/api/state`.", html)
|
||||
self.assertIn("Purpose: inspect DB-backed JSON evidence and row payloads.", html)
|
||||
self.assertIn("Workspace tables are editable only when the table is in the canonical workspace DB.", html)
|
||||
self.assertIn("DB별 / JSON별 조회 기준", html)
|
||||
self.assertIn("This page is intentionally a triage surface, not a generic table dump.", html)
|
||||
self.assertIn("read-only because this table belongs to collector or strategy storage", html)
|
||||
self.assertIn("Table Browser", html)
|
||||
self.assertIn("Save changes", html)
|
||||
self.assertIn("Clear filters", html)
|
||||
self.assertIn("• current", html)
|
||||
self.assertIn("workbookRegistrySummary", html)
|
||||
self.assertIn("dbRegistryBody", html)
|
||||
self.assertIn("Derived report registry", html)
|
||||
self.assertIn("DB tables", html)
|
||||
self.assertIn("workbookPurposeFilters", html)
|
||||
self.assertIn("Recorded surface", html)
|
||||
self.assertIn("History details", html)
|
||||
self.assertIn("historyChangeBody", html)
|
||||
self.assertIn("historyRunBody", html)
|
||||
self.assertIn("Derived JSON evidence view", html)
|
||||
self.assertIn("Derived JSON Evidence Preview", html)
|
||||
self.assertIn("JSON evidence", html)
|
||||
|
||||
def test_render_tables_html_exposes_workbook_registry_surface(self):
|
||||
html = render_tables_html()
|
||||
self.assertIn("Workbook sheets", html)
|
||||
self.assertIn("XLSX:", html)
|
||||
self.assertIn("JSON evidence:", html)
|
||||
self.assertIn("JSON role:", html)
|
||||
self.assertIn("Unmapped:", html)
|
||||
self.assertIn("Purpose filter:", html)
|
||||
self.assertIn("destination:", html)
|
||||
self.assertIn("recorded surface:", html)
|
||||
self.assertIn("Selected sheet:", html)
|
||||
self.assertIn("surface:", html)
|
||||
self.assertIn("version=", html)
|
||||
self.assertIn("No workbook sheet selected.", html)
|
||||
self.assertIn("tableSelect", html)
|
||||
self.assertIn("DB tables only", html)
|
||||
self.assertIn("Registry details", html)
|
||||
|
||||
def test_workbook_registry_maps_xlsx_sheets_to_recording_surfaces(self):
|
||||
from src.quant_engine.snapshot_admin_server_v1 import load_workbook_sheet_registry
|
||||
|
||||
registry = load_workbook_sheet_registry()
|
||||
self.assertEqual(registry["sheet_count"], 19)
|
||||
entries = {row["sheet"]: row for row in registry["entries"]}
|
||||
self.assertEqual(entries["settings"]["kind"], "workspace_db")
|
||||
self.assertEqual(entries["account_snapshot"]["kind"], "workspace_db")
|
||||
self.assertEqual(entries["data_feed"]["kind"], "collector_db")
|
||||
self.assertEqual(entries["settings"]["purpose"], "workspace_edit")
|
||||
self.assertEqual(entries["data_feed"]["purpose"], "collector_run")
|
||||
self.assertEqual(registry["json_role"], "derived_report_evidence")
|
||||
for sheet in [
|
||||
"sector_universe_refresh_audit",
|
||||
"daily_history",
|
||||
"event_calendar",
|
||||
"pa1_feedback",
|
||||
"alpha_history",
|
||||
"backdata_feature_bank",
|
||||
"sell_priority",
|
||||
"harness_context",
|
||||
"monthly_history",
|
||||
"sector_flow_history",
|
||||
"sector_universe",
|
||||
"core_satellite",
|
||||
"universe",
|
||||
"event_risk",
|
||||
"macro",
|
||||
"sector_flow",
|
||||
]:
|
||||
self.assertEqual(entries[sheet]["kind"], "json_payload")
|
||||
self.assertEqual(entries[sheet]["destination"], "GatherTradingData.json")
|
||||
self.assertEqual(entries[sheet]["source_role"], "derived_report_evidence")
|
||||
self.assertEqual(entries["sector_universe_refresh_audit"]["kind"], "json_payload")
|
||||
self.assertEqual(entries["daily_history"]["kind"], "json_payload")
|
||||
self.assertEqual(entries["sector_universe_refresh_audit"]["purpose"], "refresh_audit")
|
||||
self.assertEqual(entries["daily_history"]["purpose"], "history_ledger")
|
||||
self.assertEqual(entries["event_calendar"]["purpose"], "audit_history")
|
||||
self.assertEqual(entries["alpha_history"]["purpose"], "analysis_report")
|
||||
self.assertEqual(entries["harness_context"]["purpose"], "execution_context")
|
||||
self.assertEqual(entries["monthly_history"]["purpose"], "history_ledger")
|
||||
self.assertEqual(entries["sector_universe"]["purpose"], "universe_registry")
|
||||
self.assertEqual(entries["event_risk"]["purpose"], "macro_risk_context")
|
||||
self.assertEqual(entries["sector_flow"]["purpose"], "flow_leadership")
|
||||
self.assertNotIn("sector_universe_refresh_audit", registry["unmapped_sheets"])
|
||||
self.assertNotIn("daily_history", registry["unmapped_sheets"])
|
||||
|
||||
def test_render_collection_html_contains_dashboard_surface(self):
|
||||
html = render_collection_html()
|
||||
self.assertIn("KIS Collection Dashboard", html)
|
||||
self.assertIn("/api/state", html)
|
||||
self.assertIn("/api/collection/run", html)
|
||||
self.assertIn("Collect now", html)
|
||||
self.assertIn("collectionModeInput", html)
|
||||
self.assertIn("collectionAccountInput", html)
|
||||
self.assertIn("collectionRunLog", html)
|
||||
self.assertIn("collectionRunBanner", html)
|
||||
self.assertIn("collectionModeBadge", html)
|
||||
self.assertIn("collectionLiveStatus", html)
|
||||
self.assertIn("collectionProgressChip", html)
|
||||
self.assertIn("collectionStageChip", html)
|
||||
self.assertIn("collectionResultChip", html)
|
||||
self.assertIn("collectionTrendSummary", html)
|
||||
self.assertIn("collectionTrendChart", html)
|
||||
self.assertIn("Auto refreshes every 15 seconds", html)
|
||||
self.assertIn("older → newer", html)
|
||||
self.assertIn("Snapshots / run", html)
|
||||
self.assertIn("Errors / run", html)
|
||||
self.assertIn("[RUN]", html)
|
||||
self.assertIn("[SNAPSHOT]", html)
|
||||
self.assertIn("[ERROR]", html)
|
||||
self.assertIn("live source: active | KIS rows=", html)
|
||||
self.assertIn("collectionDetailAnchor", html)
|
||||
self.assertIn("collectionRunLogAnchor", html)
|
||||
self.assertIn("Live KIS on", html)
|
||||
self.assertIn("live source: unknown", html)
|
||||
self.assertNotIn('value="mock"', html)
|
||||
self.assertIn("Download raw JSON", html)
|
||||
self.assertIn("Download CSV", html)
|
||||
self.assertIn("Filter runs / snapshots / errors", html)
|
||||
self.assertIn("Unified activity timeline", html)
|
||||
self.assertIn("date", html)
|
||||
self.assertIn("Ticker quick search", html)
|
||||
self.assertIn("Date quick search", html)
|
||||
|
||||
def test_run_collection_job_returns_progress_payload(self):
|
||||
import tempfile
|
||||
import shutil
|
||||
from src.quant_engine.snapshot_admin_server_v1 import KIS_COLLECTION_DB, KIS_COLLECTION_REPORT, run_collection_job
|
||||
|
||||
tmp_dir = tempfile.mkdtemp()
|
||||
try:
|
||||
sqlite_db = Path(tmp_dir) / "kis_data_collection.db"
|
||||
output_json = Path(tmp_dir) / "kis_data_collection_v1.json"
|
||||
output_json.write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"generated_at": "2026-06-24T10:00:00+09:00",
|
||||
"row_count": 1,
|
||||
"source_counts": {"kis_open_api": 1},
|
||||
},
|
||||
ensure_ascii=False,
|
||||
indent=2,
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
fake_proc = Mock(returncode=0, stdout="ok", stderr="")
|
||||
with patch("src.quant_engine.snapshot_admin_server_v1.subprocess.run", return_value=fake_proc) as run_mock:
|
||||
payload = run_collection_job(
|
||||
sqlite_db=sqlite_db,
|
||||
input_json=Path(tmp_dir) / "GatherTradingData.json",
|
||||
output_json=output_json,
|
||||
kis_account="real",
|
||||
include_live_kis=True,
|
||||
allow_naver_fallback=False,
|
||||
)
|
||||
|
||||
self.assertEqual(payload["status"], "PASS")
|
||||
self.assertEqual(payload["returncode"], 0)
|
||||
self.assertEqual(payload["stdout"], "ok")
|
||||
self.assertIn("started_at", payload)
|
||||
self.assertIn("finished_at", payload)
|
||||
self.assertIn("elapsed_ms", payload)
|
||||
self.assertEqual(payload["summary"]["row_count"], 1)
|
||||
self.assertEqual(payload["summary"]["source_counts"]["kis_open_api"], 1)
|
||||
self.assertEqual(payload["state"]["db_path"], str(sqlite_db))
|
||||
run_mock.assert_called_once()
|
||||
self.assertTrue(str(KIS_COLLECTION_DB).endswith("kis_data_collection.db"))
|
||||
self.assertTrue(str(KIS_COLLECTION_REPORT).endswith("kis_data_collection_v1.json"))
|
||||
finally:
|
||||
shutil.rmtree(tmp_dir, ignore_errors=True)
|
||||
|
||||
def test_build_ui_state_exposes_expected_columns(self):
|
||||
import tempfile
|
||||
import shutil
|
||||
|
||||
Reference in New Issue
Block a user