Skip to content

Commit 6a9ae9d

Browse files
fukukei23claude
andcommitted
test: 退避ブランチからカバレッジテスト7ファイル・約200テストを回収
archive/* タグ監査の成果: - base_agent/api_keys/fastapi_app/auth_dependency/policy_interface のカバレッジテスト回収 - feature/fail のミューテーションキルテスト回収(retry_policy 32→52テスト) - 現行コードに適合修正(import先変更・モック修正・print→logger検証化) - 全スイート 4,585 passed(+99) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent af308fa commit 6a9ae9d

8 files changed

Lines changed: 1508 additions & 2 deletions

docs/変更履歴.md

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,12 +7,22 @@ NexusCore のプロジェクト固有変更履歴。
77

88
## [Unreleased]
99

10+
### Added
11+
- 退避ブランチからカバレッジテスト7ファイル・約200テストを回収(`archive/*` タグ監査の成果)
12+
- `tests/agents/test_base_agent_fallback_paths.py`(issue-86)
13+
- `tests/api/test_api_keys_coverage.py`(issue-90)
14+
- `tests/api/test_fastapi_app_coverage.py`(issue-92)
15+
- `tests/api/test_auth_dependency.py`(issue-96、モック記述ミス1件修正)
16+
- `tests/agents/test_policy_interface_coverage_boost.py`(import先を `config.policy_interface` に修正、print検証→loggerモック検証に更新)
17+
- `tests/core/test_retry_policy.py` / `test_errors.py`(feature/fail のミューテーションキルテスト、32→52テスト)
18+
- 回収見送り: `test_api_auth.py`(対象の `api/auth.py` はPhase 5で統合済み)、`test_requirement_agent_fallback.py`(対象のTextLocalizationは8.2.1で削除済み)
19+
1020
### Fixed
1121
- `api/routes/github_webhook.py`: PRコメント投稿・Slack通知を `asyncio.to_thread` でバックグラウンド実行化(Webhookレスポンスのイベントループブロック解消)
1222

1323
### Changed
1424
- `.gitignore`: デモGIF生成の中間ファイル(`docs/demo/frames/`, `docs/demo/svgs/`)を追跡対象外に
15-
- ローカルブランチ掃除: マージ済み9本を削除、未マージ13本は `archive/*` タグに退避して削除
25+
- ローカルブランチ掃除: マージ済み9本を削除、未マージ13本は `archive/*` タグに退避して削除(Issue #50自動PR・Issue #75 CI並列化はmain取込済みを確認)
1626

1727
---
1828

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
1+
import json
2+
import pytest
3+
from unittest.mock import MagicMock, patch
4+
5+
MODULE_PATH = "nexuscore.agents.base_agent"
6+
7+
8+
@pytest.fixture
9+
def base_agent():
10+
from nexuscore.agents.base_agent import BaseAgent
11+
return BaseAgent()
12+
13+
14+
class TestImportFallbacks:
15+
"""モジュールレベルのインポート失敗時のフォールバックパス (L19-20, 25-26, 40-48)"""
16+
17+
def test_dotenv_load_fallback_does_not_crash(self):
18+
"""dotenv.load_dotenv 例外でも BaseAgent は初期化できる (L19-20)"""
19+
with patch.dict("sys.modules", {"dotenv": None}):
20+
from nexuscore.agents.base_agent import BaseAgent
21+
agent = BaseAgent()
22+
assert agent is not None
23+
24+
def test_llm_router_none_fallback(self):
25+
"""LLMRouter が None の時、BaseAgent は警告を出して None を保持 (L25-26)"""
26+
with patch(f"{MODULE_PATH}.LLMRouter", None):
27+
from nexuscore.agents.base_agent import BaseAgent
28+
agent = BaseAgent()
29+
assert agent.llm_router is None
30+
31+
def test_has_retry_false_fallback(self):
32+
"""HAS_RETRY=False の時、リトライなしで直接実行される (L40-48)"""
33+
with patch(f"{MODULE_PATH}.HAS_RETRY", False):
34+
from nexuscore.agents.base_agent import BaseAgent
35+
agent = BaseAgent()
36+
mock_llm = MagicMock()
37+
mock_llm.execute.return_value = "ok"
38+
agent.llm_router = MagicMock()
39+
agent.llm_router.get_llm_for_task.return_value = mock_llm
40+
result = agent.execute_llm_task("test")
41+
assert result == "ok"
42+
43+
44+
class TestExecuteLlmFallbackPaths:
45+
"""execute_llm_task のエラーフォールバックパス (L141, 147)"""
46+
47+
def test_invalid_json_fallback_with_retry(self, base_agent):
48+
"""as_json=True + retry_with_context=None → フォールバックで '{}' を返す (L141→147→175)"""
49+
mock_llm = MagicMock()
50+
mock_llm.execute.return_value = "not json"
51+
base_agent.llm_router = MagicMock()
52+
base_agent.llm_router.get_llm_for_task.return_value = mock_llm
53+
54+
with patch(f"{MODULE_PATH}.HAS_RETRY", True), \
55+
patch(f"{MODULE_PATH}.retry_with_context", None):
56+
result = base_agent.execute_llm_task("prompt", as_json=True)
57+
assert result == "{}"
58+
59+
def test_convert_http_error_on_execute(self, base_agent):
60+
"""LLM実行エラー時にNexusエラー変換が呼ばれる (L147)"""
61+
mock_llm = MagicMock()
62+
mock_llm.execute.side_effect = Exception("HTTP 500")
63+
base_agent.llm_router = MagicMock()
64+
base_agent.llm_router.get_llm_for_task.return_value = mock_llm
65+
66+
mock_converter = MagicMock(side_effect=ConnectionError("nexus error"))
67+
with patch(f"{MODULE_PATH}.HAS_RETRY", True), \
68+
patch(f"{MODULE_PATH}.convert_http_error_to_nexus_error", mock_converter), \
69+
patch(f"{MODULE_PATH}.retry_with_context", None):
70+
# convert_http_error_to_nexus_error raises → caught by HAS_RETRY=False fallback path
71+
result = base_agent.execute_llm_task("prompt", as_json=True)
72+
assert result == "{}"
73+
74+
def test_no_llm_client_returns_empty_json(self, base_agent):
75+
"""LLMクライアントなし → as_json=True で '{}' を返す"""
76+
base_agent.llm_router = MagicMock()
77+
base_agent.llm_router.get_llm_for_task.return_value = None
78+
result = base_agent.execute_llm_task("prompt", as_json=True)
79+
assert result == "{}"
80+
81+
def test_no_llm_client_returns_empty_string(self, base_agent):
82+
"""LLMクライアントなし → as_json=False で '' を返す"""
83+
base_agent.llm_router = MagicMock()
84+
base_agent.llm_router.get_llm_for_task.return_value = None
85+
result = base_agent.execute_llm_task("prompt", as_json=False)
86+
assert result == ""
87+
88+
def test_execute_error_no_retry_fallback(self, base_agent):
89+
"""HAS_RETRY=False で実行エラー → フォールバック値を返す (L180-183)"""
90+
base_agent.llm_router = MagicMock()
91+
base_agent.llm_router.get_llm_for_task.side_effect = Exception("fail")
92+
93+
with patch(f"{MODULE_PATH}.HAS_RETRY", False):
94+
result = base_agent.execute_llm_task("prompt", as_json=True)
95+
assert result == "{}"
Lines changed: 260 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,260 @@
1+
"""
2+
policy_interface.py のカバレッジ向上テスト(追加)
3+
4+
未カバー行: 30, 84-92, 96-107, 156-157, 181-185, 205-211
5+
既存テストと重複しないテストケースを提供する。
6+
"""
7+
8+
from __future__ import annotations
9+
10+
import queue
11+
from datetime import datetime
12+
from unittest.mock import MagicMock, Mock, patch
13+
14+
import pytest
15+
16+
import nexuscore.config.policy_interface as pi_module
17+
from nexuscore.config.policy_interface import PolicyInterface
18+
19+
20+
class TestLine30GradioAvailableButGrNone:
21+
"""行30: GRADIO_AVAILABLE=True, gr=None の場合のImportError"""
22+
23+
@patch.object(pi_module, "GRADIO_AVAILABLE", True)
24+
@patch.object(pi_module, "gr", None)
25+
def test_gradio_available_true_but_gr_none_raises_import_error(self):
26+
"""GRADIO_AVAILABLE=TrueでもgrがNoneなら行30のImportErrorが発生する"""
27+
pi = PolicyInterface()
28+
with pytest.raises(ImportError, match="Gradio がインストールされていません"):
29+
pi.create_gradio_interface()
30+
31+
32+
class TestInternalCallbackFunctions:
33+
"""行84-92 (update_preview) と行96-107 (save_policy) の内部関数をテスト"""
34+
35+
def _build_mock_gr(self):
36+
"""Gradioモックを構築し、コールバックをキャプチャ可能にする"""
37+
mock_gr = MagicMock()
38+
39+
# gr.Blocks() のコンテキストマネージャーをシミュレート
40+
blocks_ctx = MagicMock()
41+
mock_gr.Blocks.return_value.__enter__ = Mock(return_value=blocks_ctx)
42+
mock_gr.Blocks.return_value.__exit__ = Mock(return_value=False)
43+
44+
# gr.Row(), gr.Column() のコンテキストマネージャー
45+
for method_name in ["Row", "Column"]:
46+
ctx = MagicMock()
47+
getattr(mock_gr, method_name).return_value.__enter__ = Mock(return_value=ctx)
48+
getattr(mock_gr, method_name).return_value.__exit__ = Mock(return_value=False)
49+
50+
# gr.themes.Soft()
51+
mock_gr.themes.Soft.return_value = MagicMock()
52+
53+
return mock_gr, blocks_ctx
54+
55+
@patch.object(pi_module, "GRADIO_AVAILABLE", True)
56+
def test_update_preview_called_via_load_callback(self):
57+
"""行84-92: update_previewがinterface.load()コールバック経由で実行されることを検証"""
58+
mock_gr, blocks_ctx = self._build_mock_gr()
59+
60+
with patch.object(pi_module, "gr", mock_gr):
61+
pi = PolicyInterface()
62+
pi.create_gradio_interface()
63+
64+
# interface.load() が呼ばれる - lambda内でupdate_previewが実行される
65+
# blocks_ctx.load がコールバックを受け取っているはず
66+
assert blocks_ctx.load.called
67+
# loadコールバックの第一引数(fn)を取得して実行
68+
load_call_kwargs = blocks_ctx.load.call_args
69+
fn = load_call_kwargs.kwargs.get("fn") or load_call_kwargs[1].get("fn")
70+
if fn is None and load_call_kwargs[0]:
71+
fn = load_call_kwargs[0][0]
72+
73+
# コールバックを実行してupdate_previewのロジックを検証
74+
result = fn()
75+
assert result["test_import_policy"] == "関数を直接埋め込み"
76+
assert result["error_language"] == "日本語"
77+
assert "preview_generated_at" in result
78+
79+
@patch.object(pi_module, "GRADIO_AVAILABLE", True)
80+
def test_save_policy_puts_to_queue(self):
81+
"""行96-107: save_policyがresult_queueにputすることを検証"""
82+
mock_gr, blocks_ctx = self._build_mock_gr()
83+
84+
with patch.object(pi_module, "gr", mock_gr):
85+
pi = PolicyInterface()
86+
pi.create_gradio_interface()
87+
88+
# submit_btn.click() が呼ばれる - save_policyがfnとして渡される
89+
# Button mockのclickメソッドを確認
90+
btn_mock = mock_gr.Button.return_value
91+
assert btn_mock.click.called
92+
93+
# save_policyコールバックを取得
94+
click_call = btn_mock.click.call_args
95+
save_fn = click_call.kwargs.get("fn") or click_call[1].get("fn")
96+
if save_fn is None and click_call[0]:
97+
save_fn = click_call[0][0]
98+
99+
# save_policyを実行
100+
test_args = ("インポート文を使用", "英語", ["型ヒント必須"], ["ログ出力制限"])
101+
policy_result, message = save_fn(*test_args)
102+
103+
# 戻り値を検証
104+
assert policy_result["test_import_policy"] == "インポート文を使用"
105+
assert policy_result["error_language"] == "英語"
106+
assert policy_result["quality_requirements"] == ["型ヒント必須"]
107+
assert policy_result["security_policy"] == ["ログ出力制限"]
108+
assert policy_result["method"] == "gradio_ui"
109+
assert "configured_at" in policy_result
110+
assert "✅ 設定が保存されました" in message
111+
112+
# キューにputされていることを検証
113+
assert not pi.result_queue.empty()
114+
queued = pi.result_queue.get_nowait()
115+
assert queued == policy_result
116+
117+
@patch.object(pi_module, "GRADIO_AVAILABLE", True)
118+
def test_update_preview_change_callback(self):
119+
"""行84-92: changeイベント経由のupdate_previewコールバックを検証"""
120+
mock_gr, blocks_ctx = self._build_mock_gr()
121+
122+
with patch.object(pi_module, "gr", mock_gr):
123+
pi = PolicyInterface()
124+
pi.create_gradio_interface()
125+
126+
# Radio, CheckboxGroup モックのchangeメソッドが呼ばれている
127+
radio_mocks = mock_gr.Radio.call_args_list
128+
checkbox_mocks = mock_gr.CheckboxGroup.call_args_list
129+
130+
# すべてのコンポーネントでchangeが呼ばれる
131+
all_components = [mock_gr.Radio.return_value, mock_gr.CheckboxGroup.return_value]
132+
for comp in all_components:
133+
if comp.change.called:
134+
call = comp.change.call_args
135+
fn = call.kwargs.get("fn") or (call[0][0] if call[0] else None)
136+
if fn:
137+
result = fn("混在OK", "自動", ["docstring必須"], ["APIキー環境変数管理"])
138+
assert result["test_import_policy"] == "混在OK"
139+
assert result["error_language"] == "自動"
140+
assert "preview_generated_at" in result
141+
break
142+
143+
144+
class TestLaunchGradioExceptionPrint:
145+
"""launch_gradio内の例外ハンドリングログ"""
146+
147+
@patch.object(pi_module, "GRADIO_AVAILABLE", True)
148+
def test_launch_thread_exception_prints_message(self):
149+
"""Gradioのlaunch()が例外を投げたとき、エラーログが出力されることを検証"""
150+
mock_gr = MagicMock()
151+
mock_blocks = MagicMock()
152+
mock_gr.Blocks.return_value.__enter__ = Mock(return_value=mock_blocks)
153+
mock_gr.Blocks.return_value.__exit__ = Mock(return_value=False)
154+
mock_gr.themes.Soft.return_value = MagicMock()
155+
156+
# コンテキストマネージャー用
157+
for method_name in ["Row", "Column"]:
158+
ctx = MagicMock()
159+
getattr(mock_gr, method_name).return_value.__enter__ = Mock(return_value=ctx)
160+
getattr(mock_gr, method_name).return_value.__exit__ = Mock(return_value=False)
161+
162+
# launchが例外を投げる
163+
mock_blocks.launch.side_effect = RuntimeError("Simulated launch failure")
164+
165+
with patch.object(pi_module, "gr", mock_gr):
166+
pi = PolicyInterface()
167+
# Thread.start()が実際にtargetを実行するようにする
168+
original_thread = __import__("threading").Thread
169+
170+
def mock_thread_init(target=None, args=(), kwargs=None, daemon=None, **kw):
171+
if target:
172+
try:
173+
target(*args)
174+
except Exception:
175+
pass # スレッド内の例外はスレッド内で処理される
176+
t = MagicMock()
177+
t.start = MagicMock()
178+
t.daemon = True
179+
return t
180+
181+
with patch.object(pi_module, "_logger") as mock_logger:
182+
with patch("threading.Thread", side_effect=mock_thread_init):
183+
result = pi.launch_and_wait_for_input(timeout=0.01)
184+
185+
assert any(
186+
"Gradio起動エラー" in str(call.args[0]) for call in mock_logger.error.call_args_list
187+
)
188+
189+
190+
class TestFinallyBlockCloseException:
191+
"""finally内のinterface.close()例外ハンドリング"""
192+
193+
@patch.object(pi_module, "GRADIO_AVAILABLE", True)
194+
@patch.object(pi_module, "gr", MagicMock())
195+
def test_close_exception_in_finally_prints_message(self):
196+
"""finallyでself.interface.close()が例外を投げた場合の警告ログを検証"""
197+
pi = PolicyInterface()
198+
199+
# self.interface にcloseで例外を投げるモックを設定
200+
mock_interface = MagicMock()
201+
mock_interface.close.side_effect = RuntimeError("close failed")
202+
pi.interface = mock_interface
203+
204+
# create_gradio_interfaceが例外を投げるようにしてfinallyブロックへ
205+
with patch.object(pi_module, "_logger") as mock_logger:
206+
with patch.object(pi, "create_gradio_interface", side_effect=RuntimeError("create error")):
207+
result = pi.launch_and_wait_for_input(timeout=1)
208+
209+
assert result is not None
210+
assert result["method"] == "safe_default"
211+
212+
assert any(
213+
"Gradioを閉じる際にエラーが発生" in str(call.args[0])
214+
for call in mock_logger.warning.call_args_list
215+
)
216+
217+
@patch.object(pi_module, "GRADIO_AVAILABLE", True)
218+
@patch.object(pi_module, "gr", MagicMock())
219+
def test_close_success_in_finally_no_error_print(self):
220+
"""finallyでself.interface.close()が成功した場合の情報ログを検証"""
221+
pi = PolicyInterface()
222+
223+
mock_interface = MagicMock()
224+
pi.interface = mock_interface
225+
226+
with patch.object(pi_module, "_logger") as mock_logger:
227+
with patch.object(pi, "create_gradio_interface", side_effect=RuntimeError("create error")):
228+
result = pi.launch_and_wait_for_input(timeout=1)
229+
230+
assert result is not None
231+
mock_interface.close.assert_called_once()
232+
233+
assert any(
234+
"Gradioインターフェースを閉じました" in str(call.args[0])
235+
for call in mock_logger.info.call_args_list
236+
)
237+
238+
239+
class TestMainBlock:
240+
"""行205-211: __main__ ブロックのロジックを検証"""
241+
242+
def test_main_block_logic_with_mock(self, capsys):
243+
"""__main__ブロックのロジックが正しく動作することを検証"""
244+
mock_result = {"method": "safe_default", "test_import_policy": "関数を直接埋め込み"}
245+
246+
with patch.object(
247+
PolicyInterface, "launch_and_wait_for_input", return_value=mock_result
248+
):
249+
# __main__ブロックの内容をインライン実行
250+
print("Policy Interface テスト開始")
251+
interface = PolicyInterface()
252+
result = interface.launch_and_wait_for_input(timeout=30)
253+
print("受信した設定:")
254+
print(result)
255+
print("テスト完了")
256+
257+
captured = capsys.readouterr()
258+
assert "Policy Interface テスト開始" in captured.out
259+
assert "受信した設定:" in captured.out
260+
assert "テスト完了" in captured.out

0 commit comments

Comments
 (0)