Merge origin/main into codex/roadmap-publish

This commit is contained in:
2026-06-22 14:55:43 +09:00
14 changed files with 1001 additions and 375 deletions
+68 -65
View File
@@ -1,14 +1,14 @@
from __future__ import annotations
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
sys.path.insert(0, str(ROOT))
import pytest
from src.quant_engine.kis_api_client_v1 import (
KisCredentials,
OrderEndpointBlockedError,
@@ -30,69 +30,72 @@ FORBIDDEN_ORDER_TR_IDS = (
)
@pytest.mark.parametrize("path", FORBIDDEN_ORDER_PATHS)
def test_order_path_is_blocked(path: str):
with pytest.raises(OrderEndpointBlockedError):
_assert_read_only(path, "FHKST01010100")
class TestKisApiClientV1(unittest.TestCase):
def test_order_path_is_blocked(self):
for path in FORBIDDEN_ORDER_PATHS:
with self.assertRaises(OrderEndpointBlockedError):
_assert_read_only(path, "FHKST01010100")
def test_order_tr_id_is_blocked(self):
for tr_id in FORBIDDEN_ORDER_TR_IDS:
with self.assertRaises(OrderEndpointBlockedError):
_assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-price", tr_id)
def test_known_readonly_endpoints_pass(self):
_assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-price", "FHKST01010100")
_assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-asking-price-exp-ccn", "FHKST01010200")
_assert_read_only("/uapi/domestic-stock/v1/quotations/daily-short-sale", "FHPST04830000")
def test_no_order_endpoint_substring_anywhere_in_kis_client_source(self):
"""정적 검증 — 누군가 향후 주문 함수를 추가하더라도 경로 문자열이 소스에 남으면 즉시 탐지.
TTTC8434R/VTTC8434R(주식잔고조회)는 FORBIDDEN_TR_ID_PREFIXES 차단목록 '데이터'
이 파일에 의도적으로 존재한다(prefix가 아닌 전체 TR_ID라 prefix-매칭으로는 막을 수
없어 명시적으로 등재) — 이 두 개는 검사에서 제외한다. 전체 코드베이스 차원의
"차단목록 외 파일에는 한 글자도 없어야 한다"는 보장은
tools/validate_no_direct_api_trading_v1.py(ALLOWLISTED_FILES 제외 전체 스캔)가 맡는다.
"""
source = (ROOT / "src" / "quant_engine" / "kis_api_client_v1.py").read_text(encoding="utf-8")
blocklist_data_exceptions = {"TTTC8434R", "VTTC8434R"}
for forbidden_path in FORBIDDEN_ORDER_PATHS:
self.assertNotIn(forbidden_path, source, f"주문 엔드포인트 경로가 소스에 존재함: {forbidden_path}")
for forbidden_tr_id in FORBIDDEN_ORDER_TR_IDS:
if forbidden_tr_id in blocklist_data_exceptions:
continue
self.assertNotIn(forbidden_tr_id, source, f"주문 TR_ID가 소스에 존재함: {forbidden_tr_id}")
def test_kis_client_module_defines_no_order_submission_function(self):
import src.quant_engine.kis_api_client_v1 as kis_module
public_names = [name for name in dir(kis_module) if not name.startswith("_")]
banned_keywords = (
"place_order", "submit_order", "cancel_order", "revise_order", "send_order",
"inquire_balance", "account_balance",
)
for name in public_names:
lowered = name.lower()
for banned in banned_keywords:
self.assertNotIn(banned, lowered, f"주문 제출/정정/취소로 의심되는 함수가 존재함: {name}")
def test_kis_credentials_load_uses_required_env_vars(self):
with patch.dict("os.environ", {
"KIS_APP_Key": "real-key",
"KIS_APP_Secret": "real-secret",
"KIS_APP_Key_TEST": "mock-key",
"KIS_APP_Secret_TEST": "mock-secret"
}):
real = KisCredentials.load("real")
mock = KisCredentials.load("mock")
self.assertEqual(real.app_key, "real-key")
self.assertEqual(real.app_secret, "real-secret")
self.assertEqual(real.account, "real")
self.assertEqual(mock.app_key, "mock-key")
self.assertEqual(mock.app_secret, "mock-secret")
self.assertEqual(mock.account, "mock")
@pytest.mark.parametrize("tr_id", FORBIDDEN_ORDER_TR_IDS)
def test_order_tr_id_is_blocked(tr_id: str):
with pytest.raises(OrderEndpointBlockedError):
_assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-price", tr_id)
if __name__ == "__main__":
unittest.main()
def test_known_readonly_endpoints_pass():
_assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-price", "FHKST01010100")
_assert_read_only("/uapi/domestic-stock/v1/quotations/inquire-asking-price-exp-ccn", "FHKST01010200")
_assert_read_only("/uapi/domestic-stock/v1/quotations/daily-short-sale", "FHPST04830000")
def test_no_order_endpoint_substring_anywhere_in_kis_client_source():
"""정적 검증 — 누군가 향후 주문 함수를 추가하더라도 경로 문자열이 소스에 남으면 즉시 탐지.
TTTC8434R/VTTC8434R(주식잔고조회)는 FORBIDDEN_TR_ID_PREFIXES 차단목록 '데이터'
이 파일에 의도적으로 존재한다(prefix가 아닌 전체 TR_ID라 prefix-매칭으로는 막을 수
없어 명시적으로 등재) — 이 두 개는 검사에서 제외한다. 전체 코드베이스 차원의
"차단목록 외 파일에는 한 글자도 없어야 한다"는 보장은
tools/validate_no_direct_api_trading_v1.py(ALLOWLISTED_FILES 제외 전체 스캔)가 맡는다.
"""
source = (ROOT / "src" / "quant_engine" / "kis_api_client_v1.py").read_text(encoding="utf-8")
blocklist_data_exceptions = {"TTTC8434R", "VTTC8434R"}
for forbidden_path in FORBIDDEN_ORDER_PATHS:
assert forbidden_path not in source, f"주문 엔드포인트 경로가 소스에 존재함: {forbidden_path}"
for forbidden_tr_id in FORBIDDEN_ORDER_TR_IDS:
if forbidden_tr_id in blocklist_data_exceptions:
continue
assert forbidden_tr_id not in source, f"주문 TR_ID가 소스에 존재함: {forbidden_tr_id}"
def test_kis_client_module_defines_no_order_submission_function():
import src.quant_engine.kis_api_client_v1 as kis_module
public_names = [name for name in dir(kis_module) if not name.startswith("_")]
banned_keywords = (
"place_order", "submit_order", "cancel_order", "revise_order", "send_order",
"inquire_balance", "account_balance",
)
for name in public_names:
lowered = name.lower()
for banned in banned_keywords:
assert banned not in lowered, f"주문 제출/정정/취소로 의심되는 함수가 존재함: {name}"
def test_kis_credentials_load_uses_required_env_vars(monkeypatch):
monkeypatch.setenv("KIS_APP_Key", "real-key")
monkeypatch.setenv("KIS_APP_Secret", "real-secret")
monkeypatch.setenv("KIS_APP_Key_TEST", "mock-key")
monkeypatch.setenv("KIS_APP_Secret_TEST", "mock-secret")
real = KisCredentials.load("real")
mock = KisCredentials.load("mock")
assert real.app_key == "real-key"
assert real.app_secret == "real-secret"
assert real.account == "real"
assert mock.app_key == "mock-key"
assert mock.app_secret == "mock-secret"
assert mock.account == "mock"
@@ -2,6 +2,7 @@ from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
ROOT = Path(__file__).resolve().parents[2]
@@ -11,10 +12,17 @@ if str(ROOT) not in sys.path:
import tools.validate_gitea_secrets_contract_v1 as validator
def test_validate_gitea_secrets_contract_passes():
rc = validator.main()
payload = json.loads((ROOT / "Temp" / "gitea_secrets_contract_v1.json").read_text(encoding="utf-8"))
class TestValidateGiteaSecretsContract(unittest.TestCase):
def test_validate_gitea_secrets_contract_passes(self):
rc = validator.main()
payload = json.loads((ROOT / "Temp" / "gitea_secrets_contract_v1.json").read_text(encoding="utf-8"))
self.assertEqual(rc, 0)
self.assertEqual(payload["gate"], "PASS")
self.assertTrue(payload["evidence"][".gitea/workflows/kis_data_collection.yml"]["vars.KIS_APP_KEY"])
if __name__ == "__main__":
unittest.main()
assert rc == 0
assert payload["gate"] == "PASS"
assert payload["evidence"][".gitea/workflows/kis_data_collection.yml"]["vars.KIS_APP_KEY"] is True
@@ -2,7 +2,9 @@ from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
@@ -19,35 +21,54 @@ class _FakeCreds:
self.app_secret = f"{account}-secret"
def test_validate_kis_api_credentials_writes_pass_json(tmp_path, monkeypatch):
out = tmp_path / "kis_api_credentials_validation_v1.json"
class TestValidateKisApiCredentials(unittest.TestCase):
monkeypatch.setenv("KIS_APP_Key_TEST", "mock-key")
monkeypatch.setenv("KIS_APP_Secret_TEST", "mock-secret")
monkeypatch.setattr(validator, "KisCredentials", type("CredFactory", (), {"load": staticmethod(lambda account: _FakeCreds(account))}))
monkeypatch.setattr(validator, "get_current_price", lambda creds, ticker: (_ for _ in ()).throw(RuntimeError("network should not be called in dry-run")))
monkeypatch.setattr(sys, "argv", ["validate_kis_api_credentials_v1.py", "--account", "mock", "--ticker", "005930", "--dry-run", "--output", str(out)])
def test_validate_kis_api_credentials_writes_pass_json(self):
import tempfile
import shutil
tmp_dir = tempfile.mkdtemp()
try:
out = Path(tmp_dir) / "kis_api_credentials_validation_v1.json"
rc = validator.main()
payload = json.loads(out.read_text(encoding="utf-8"))
with patch.dict("os.environ", {"KIS_APP_Key_TEST": "mock-key", "KIS_APP_Secret_TEST": "mock-secret"}):
with patch.object(validator, "KisCredentials") as mock_creds:
mock_creds.load.side_effect = lambda account: _FakeCreds(account)
with patch.object(validator, "get_current_price") as mock_price:
mock_price.side_effect = RuntimeError("network should not be called in dry-run")
with patch.object(sys, "argv", ["validate_kis_api_credentials_v1.py", "--account", "mock", "--ticker", "005930", "--dry-run", "--output", str(out)]):
rc = validator.main()
payload = json.loads(out.read_text(encoding="utf-8"))
assert rc == 0
assert payload["gate"] == "PASS"
assert payload["evidence"]["account"] == "mock"
assert payload["evidence"]["ticker"] == "005930"
assert payload["evidence"]["dry_run"] is True
self.assertEqual(rc, 0)
self.assertEqual(payload["gate"], "PASS")
self.assertEqual(payload["evidence"]["account"], "mock")
self.assertEqual(payload["evidence"]["ticker"], "005930")
self.assertTrue(payload["evidence"]["dry_run"])
finally:
shutil.rmtree(tmp_dir)
def test_validate_kis_api_credentials_fails_when_api_call_errors(self):
import tempfile
import shutil
tmp_dir = tempfile.mkdtemp()
try:
out = Path(tmp_dir) / "kis_api_credentials_validation_v1.json"
with patch.object(validator, "KisCredentials") as mock_creds:
mock_creds.load.side_effect = lambda account: _FakeCreds(account)
with patch.object(validator, "get_current_price") as mock_price:
mock_price.side_effect = RuntimeError("boom")
with patch.object(sys, "argv", ["validate_kis_api_credentials_v1.py", "--account", "mock", "--ticker", "005930", "--output", str(out)]):
rc = validator.main()
payload = json.loads(out.read_text(encoding="utf-8"))
self.assertEqual(rc, 1)
self.assertEqual(payload["gate"], "FAIL")
self.assertTrue(payload["errors"])
finally:
shutil.rmtree(tmp_dir)
def test_validate_kis_api_credentials_fails_when_api_call_errors(tmp_path, monkeypatch):
out = tmp_path / "kis_api_credentials_validation_v1.json"
if __name__ == "__main__":
unittest.main()
monkeypatch.setattr(validator, "KisCredentials", type("CredFactory", (), {"load": staticmethod(lambda account: _FakeCreds(account))}))
monkeypatch.setattr(validator, "get_current_price", lambda creds, ticker: (_ for _ in ()).throw(RuntimeError("boom")))
monkeypatch.setattr(sys, "argv", ["validate_kis_api_credentials_v1.py", "--account", "mock", "--ticker", "005930", "--output", str(out)])
rc = validator.main()
payload = json.loads(out.read_text(encoding="utf-8"))
assert rc == 1
assert payload["gate"] == "FAIL"
assert payload["errors"]
@@ -2,7 +2,9 @@ from __future__ import annotations
import json
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
@@ -11,115 +13,93 @@ if str(ROOT) not in sys.path:
import tools.validate_platform_transition_wbs_v1 as validator
def test_validate_platform_transition_wbs_reports_failure_notes(monkeypatch):
spec = {
"phase_5_platform_transition": {
"P1_kis_core_api_collector": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
"P2_sqlite_canonical_store": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
"P3_ci_scheduler_cutover": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
"P4_gas_thin_adapter_minimize": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
"P5_postgresql_upgrade_path": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
class TestValidatePlatformTransitionWbs(unittest.TestCase):
def test_validate_platform_transition_wbs_reports_failure_notes(self):
spec = {
"phase_5_platform_transition": {
"P1_kis_core_api_collector": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
"P2_sqlite_canonical_store": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
"P3_ci_scheduler_cutover": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
"P4_gas_thin_adapter_minimize": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
"P5_postgresql_upgrade_path": {
"success_criteria": {
"expected_success_value": {},
"evidence_artifacts": [],
"verification_commands": [],
}
},
}
}
}
monkeypatch.setattr(
validator,
"_load_spec",
lambda: spec,
)
monkeypatch.setattr(
validator,
"_read_text",
lambda path: "Phase 5 데이터 플랫폼 전환 WBS 성공값 P1 KIS core collector P2 SQLite canonical store P3 CI scheduler cutover P4 GAS thin adapter minimize P5 PostgreSQL upgrade path",
)
monkeypatch.setattr(
validator,
"_check_p1",
lambda: {
"gate": "FAIL",
"expected_success_value": {},
"evidence": {"summary_path": "Temp/test_kis_data_collection.json", "db_path": "Temp/test_kis_data_collection.db"},
"errors": ["summary_status=None"],
},
)
monkeypatch.setattr(
validator,
"_check_p2",
lambda: {
"gate": "FAIL",
"expected_success_value": {},
"evidence": {"db_path": "Temp/test_kis_data_collection.db"},
"errors": ["sqlite_round_trip_missing"],
},
)
monkeypatch.setattr(
validator,
"_check_p3",
lambda: {
"gate": "PASS",
"expected_success_value": {},
"evidence": {"workflow_path": ".gitea/workflows/kis_data_collection.yml"},
"errors": [],
},
)
monkeypatch.setattr(
validator,
"_check_p4",
lambda: {
"gate": "FAIL",
"expected_success_value": {},
"evidence": {"validation_path": "Temp/gas_thin_adapter_validation_v1.json"},
"errors": ["gate=None", "function_inventory_coverage_pct<100"],
},
)
monkeypatch.setattr(
validator,
"_check_p5",
lambda: {
"gate": "PASS",
"expected_success_value": {},
"evidence": {},
"errors": [],
},
)
with patch.object(validator, "_load_spec", return_value=spec):
with patch.object(validator, "_read_text", return_value="Phase 5 데이터 플랫폼 전환 WBS 성공값 P1 KIS core collector P2 SQLite canonical store P3 CI scheduler cutover P4 GAS thin adapter minimize P5 PostgreSQL upgrade path"):
with patch.object(validator, "_check_p1", return_value={
"gate": "FAIL",
"expected_success_value": {},
"evidence": {"summary_path": "Temp/test_kis_data_collection.json", "db_path": "Temp/test_kis_data_collection.db"},
"errors": ["summary_status=None"],
}):
with patch.object(validator, "_check_p2", return_value={
"gate": "FAIL",
"expected_success_value": {},
"evidence": {"db_path": "Temp/test_kis_data_collection.db"},
"errors": ["sqlite_round_trip_missing"],
}):
with patch.object(validator, "_check_p3", return_value={
"gate": "PASS",
"expected_success_value": {},
"evidence": {"workflow_path": ".gitea/workflows/kis_data_collection.yml"},
"errors": [],
}):
with patch.object(validator, "_check_p4", return_value={
"gate": "FAIL",
"expected_success_value": {},
"evidence": {"validation_path": "Temp/gas_thin_adapter_validation_v1.json"},
"errors": ["gate=None", "function_inventory_coverage_pct<100"],
}):
with patch.object(validator, "_check_p5", return_value={
"gate": "PASS",
"expected_success_value": {},
"evidence": {},
"errors": [],
}):
rc = validator.main()
payload = json.loads((ROOT / "Temp" / "platform_transition_wbs_v1.json").read_text(encoding="utf-8"))
rc = validator.main()
payload = json.loads((ROOT / "Temp" / "platform_transition_wbs_v1.json").read_text(encoding="utf-8"))
self.assertEqual(rc, 1)
self.assertEqual(payload["gate"], "FAIL")
self.assertTrue(payload["message"].startswith("Platform transition WBS check failed"))
self.assertEqual(len(payload["failure_notes"]), 3)
self.assertIn("P1 failed", payload["failure_notes"][0])
self.assertIn("P2 failed", payload["failure_notes"][1])
self.assertIn("P4 failed", payload["failure_notes"][2])
if __name__ == "__main__":
unittest.main()
assert rc == 1
assert payload["gate"] == "FAIL"
assert payload["message"].startswith("Platform transition WBS check failed")
assert len(payload["failure_notes"]) == 3
assert "P1 failed" in payload["failure_notes"][0]
assert "P2 failed" in payload["failure_notes"][1]
assert "P4 failed" in payload["failure_notes"][2]
+77 -53
View File
@@ -2,7 +2,9 @@
from __future__ import annotations
import sys
import unittest
from pathlib import Path
from unittest.mock import patch
ROOT = Path(__file__).resolve().parents[2]
if str(ROOT) not in sys.path:
@@ -11,59 +13,81 @@ if str(ROOT) not in sys.path:
import tools.validate_specs as vs
def test_real_repo_has_no_missing_code_path():
"""현재 저장소 상태에서 1차 태깅된 파일들은 모두 code_path가 실존해야 한다."""
errors: list[str] = []
result = vs.validate_spec_code_sync(errors)
assert result["gate"] == "PASS"
assert result["missing_code_path_count"] == 0
assert result["checked_count"] >= 10
assert not errors
class TestValidateSpecCodeSync(unittest.TestCase):
def test_real_repo_has_no_missing_code_path(self):
"""현재 저장소 상태에서 1차 태깅된 파일들은 모두 code_path가 실존해야 한다."""
errors: list[str] = []
result = vs.validate_spec_code_sync(errors)
self.assertEqual(result["gate"], "PASS")
self.assertEqual(result["missing_code_path_count"], 0)
self.assertTrue(result["checked_count"] >= 10)
self.assertFalse(errors)
def test_missing_code_path_fails(self):
import tempfile
import shutil
tmp_dir = tempfile.mkdtemp()
try:
tmp_path = Path(tmp_dir)
(tmp_path / "spec").mkdir()
(tmp_path / "governance").mkdir()
(tmp_path / "spec" / "fake_contract.yaml").write_text(
"meta:\n has_code_implementation: true\n code_path: \"tools/does_not_exist_v1.py\"\n",
encoding="utf-8",
)
with patch.object(vs, "ROOT", tmp_path):
errors: list[str] = []
result = vs.validate_spec_code_sync(errors)
self.assertEqual(result["gate"], "FAIL")
self.assertEqual(result["missing_code_path_count"], 1)
self.assertTrue(any("does_not_exist_v1.py" in e for e in errors))
finally:
shutil.rmtree(tmp_dir)
def test_redirect_only_and_has_code_is_contradiction(self):
import tempfile
import shutil
tmp_dir = tempfile.mkdtemp()
try:
tmp_path = Path(tmp_dir)
(tmp_path / "spec").mkdir()
(tmp_path / "governance").mkdir()
(tmp_path / "spec" / "contradiction.yaml").write_text(
"meta:\n has_code_implementation: true\n redirect_only: true\n",
encoding="utf-8",
)
with patch.object(vs, "ROOT", tmp_path):
errors: list[str] = []
result = vs.validate_spec_code_sync(errors)
self.assertEqual(result["gate"], "FAIL")
self.assertTrue(any("contradiction" in e for e in errors))
finally:
shutil.rmtree(tmp_dir)
def test_files_without_the_field_are_skipped_not_failed(self):
import tempfile
import shutil
tmp_dir = tempfile.mkdtemp()
try:
tmp_path = Path(tmp_dir)
(tmp_path / "spec").mkdir()
(tmp_path / "governance").mkdir()
(tmp_path / "spec" / "untouched.yaml").write_text(
"meta:\n title: legacy doc with no sync field\n",
encoding="utf-8",
)
with patch.object(vs, "ROOT", tmp_path):
errors: list[str] = []
result = vs.validate_spec_code_sync(errors)
self.assertEqual(result["gate"], "PASS")
self.assertEqual(result["checked_count"], 0)
self.assertEqual(result["total_spec_files"], 1)
self.assertFalse(errors)
finally:
shutil.rmtree(tmp_dir)
def test_missing_code_path_fails(tmp_path, monkeypatch):
(tmp_path / "spec").mkdir()
(tmp_path / "governance").mkdir()
(tmp_path / "spec" / "fake_contract.yaml").write_text(
"meta:\n has_code_implementation: true\n code_path: \"tools/does_not_exist_v1.py\"\n",
encoding="utf-8",
)
monkeypatch.setattr(vs, "ROOT", tmp_path)
if __name__ == "__main__":
unittest.main()
errors: list[str] = []
result = vs.validate_spec_code_sync(errors)
assert result["gate"] == "FAIL"
assert result["missing_code_path_count"] == 1
assert any("does_not_exist_v1.py" in e for e in errors)
def test_redirect_only_and_has_code_is_contradiction(tmp_path, monkeypatch):
(tmp_path / "spec").mkdir()
(tmp_path / "governance").mkdir()
(tmp_path / "spec" / "contradiction.yaml").write_text(
"meta:\n has_code_implementation: true\n redirect_only: true\n",
encoding="utf-8",
)
monkeypatch.setattr(vs, "ROOT", tmp_path)
errors: list[str] = []
result = vs.validate_spec_code_sync(errors)
assert result["gate"] == "FAIL"
assert any("contradiction" in e for e in errors)
def test_files_without_the_field_are_skipped_not_failed(tmp_path, monkeypatch):
(tmp_path / "spec").mkdir()
(tmp_path / "governance").mkdir()
(tmp_path / "spec" / "untouched.yaml").write_text(
"meta:\n title: legacy doc with no sync field\n",
encoding="utf-8",
)
monkeypatch.setattr(vs, "ROOT", tmp_path)
errors: list[str] = []
result = vs.validate_spec_code_sync(errors)
assert result["gate"] == "PASS"
assert result["checked_count"] == 0
assert result["total_spec_files"] == 1
assert not errors