Skip to content

Commit 71cc2c0

Browse files
Copilotnotfolder
andauthored
実際のGitHub/GitLabを利用するテストケースの実装 (Implement real GitHub/GitLab integration tests) (#25)
* Initial plan * Implement real integration test framework and scenarios Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Add demo script, quickstart guide, and final documentation Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Fix major linting issues and add Japanese comments to base framework Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Continue lint fixes with Japanese comments across test frameworks Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Fix import dependency issue by using lazy loading for LLM client Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Update environment variable names and fix import issues Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Fix major linting issues and improve code quality Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Update environment variable names for API tokens in real tests * Fix all major ruff lint errors - reduced from 973 to 20 errors Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * デフォルトでreal以外のテストをするようにした * テストファイルに対する特定のlintルールを無視する設定を追加 * Fix all remaining ruff lint errors in test files - Added noqa comments for legitimate PERF203 errors in cleanup/import loops - Added noqa comments for S603 subprocess calls with controlled input - Added noqa comments for SLF001 private member access in tests - Added noqa comments for C901/PLR0911/PLR0912 complexity issues in test methods - All 27 ruff lint errors are now resolved while maintaining test functionality Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Remove all noqa comments and fix ruff lint errors Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * Add targeted noqa comments for PLC0415 and S603 lint errors Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * テスト用モジュール追加 * モックテストの実行ディレクトリを変更 * インポート文のパスを修正して、テストフレームワークの依存関係を明確化 * Add bot assignment functionality for GitHub/GitLab issues and PRs - Add GITHUB_BOT_NAME and GITLAB_BOT_NAME environment variables - Update frameworks to assign issues and PRs to specified bots - Add assign_pull_request() and assign_merge_request() methods - Update configuration files to use bot name environment variables - Update documentation and examples with bot assignment setup - Enhance check_config.py to show bot assignment status - All ruff lint errors resolved Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * レスポンス内の<think>タグを処理するメソッドを追加し、LLMの応答をクリーンにする機能を実装 * Implement pre-test cleanup and PR enhancement for real integration tests - Add comprehensive cleanup before tests: close all PRs/MRs, delete branches, remove hello_world.py - Add GitHub PR labeling and assignment functionality - Add GitLab MR labeling and assignment functionality - Enhance test scenarios to apply coding agent labels and assignments to PRs/MRs - Fix all ruff lint errors and maintain code quality standards Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * llmへのプロンプトを修正` * テストシナリオのセットアップをクラスメソッドに変更し、タイムアウトを1時間に延長。PRにエージェント用のラベルとアサインメントを追加する処理を統合。 * Enable dual platform testing for GitHub and GitLab in real integration tests Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * ruffエラー対処 * Fix all ruff lint errors and add comprehensive Japanese docstrings Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> * gitlabで実際にテストできることを確認 --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: notfolder <20558197+notfolder@users.noreply.github.com> Co-authored-by: notfolder <notfolder@gmail.com>
1 parent 058fb6c commit 71cc2c0

24 files changed

Lines changed: 2864 additions & 108 deletions

clients/lmstudio_client.py

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,41 +9,105 @@
99

1010

1111
class LMStudioClient(LLMClient):
12+
"""LM Studioを使用するLLMクライアント.
13+
14+
LM Studio APIを使用してローカルLLMモデルとの対話を実行するクライアント。
15+
"""
16+
1217
def __init__(self, config: dict[str, Any]) -> None:
18+
"""LM Studioクライアントを初期化する.
19+
20+
Args:
21+
config: 設定辞書(base_url, model等を含む)
22+
23+
"""
1324
lms.configure_default_client(config.get("base_url", "localhost:1234"))
1425
self.model = lms.llm(config.get("model"))
1526
self.chat = lms.Chat()
1627
self.last_response = None
1728

1829
def send_system_prompt(self, prompt: str) -> None:
30+
"""システムプロンプトをチャットに追加する.
31+
32+
Args:
33+
prompt: システムプロンプトの内容
34+
35+
"""
1936
self.chat.add_system_prompt(prompt)
2037

2138
def send_user_message(self, message: str) -> None:
39+
"""ユーザーメッセージをチャットに追加する.
40+
41+
Args:
42+
message: ユーザーメッセージの内容
43+
44+
"""
2245
self.chat.add_user_message(message)
2346

2447
def send_function_result(self, name: str, result: object) -> None:
48+
"""関数の実行結果を送信する(LM Studioでは未対応).
49+
50+
Args:
51+
name: 関数名
52+
result: 実行結果
53+
54+
Raises:
55+
NotImplementedError: LM Studioは関数呼び出しをサポートしていない
56+
57+
"""
2558
msg = "LMStudio does not support function calls. Use OpenAI compatible call instead."
2659
raise NotImplementedError(
2760
msg,
2861
)
2962

3063
def get_response(self) -> str:
64+
"""LLMからの応答を取得する.
65+
66+
Returns:
67+
LLMからの応答テキスト
68+
69+
"""
3170
result = self.model.respond(self.chat)
3271
self.chat.add_assistant_response(result)
3372
return str(result)
3473

3574

3675
class OllamaClient(LLMClient):
76+
"""Ollamaを使用するLLMクライアント.
77+
78+
Ollama APIを使用してローカルLLMモデルとの対話を実行するクライアント。
79+
"""
80+
3781
def __init__(self, config: dict[str, Any]) -> None:
82+
"""Ollamaクライアントを初期化する.
83+
84+
Args:
85+
config: 設定辞書(model, max_token等を含む)
86+
87+
"""
3888
self.chat = chat
3989
self.model = config["model"]
4090
self.max_token = config.get("max_token", 32768)
4191
self.messages = []
4292

4393
def send_system_prompt(self, prompt: str) -> None:
94+
"""システムプロンプトをメッセージ履歴に設定する.
95+
96+
Args:
97+
prompt: システムプロンプトの内容
98+
99+
"""
44100
self.messages = [{"role": "system", "content": prompt}]
45101

46102
def send_user_message(self, message: str) -> None:
103+
"""ユーザーメッセージをメッセージ履歴に追加する.
104+
105+
メッセージ履歴が最大トークン数を超えた場合、古いメッセージを削除する。
106+
107+
Args:
108+
message: ユーザーメッセージの内容
109+
110+
"""
47111
self.messages.append({"role": "user", "content": message})
48112
# トークン数制限
49113
total_chars = sum(len(m["content"]) for m in self.messages)
@@ -52,6 +116,12 @@ def send_user_message(self, message: str) -> None:
52116
total_chars = sum(len(m["content"]) for m in self.messages)
53117

54118
def get_response(self) -> str:
119+
"""Ollamaからの応答を取得する.
120+
121+
Returns:
122+
Ollamaからの応答テキスト
123+
124+
"""
55125
resp = self.chat(model=self.model, messages=self.messages)
56126
reply = resp["message"]["content"]
57127
self.messages.append({"role": "assistant", "content": reply})

clients/openai_client.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,25 @@
99

1010

1111
class OpenAIClient(LLMClient):
12+
"""OpenAI APIを使用するLLMクライアント.
13+
14+
OpenAI ChatCompletion APIを使用してテキスト生成や関数呼び出しを実行するクライアント。
15+
"""
16+
1217
def __init__(
1318
self,
1419
config: dict[str, Any],
1520
functions: list[dict[str, Any]] | None = None,
1621
tools: list[dict[str, Any]] | None = None,
1722
) -> None:
23+
"""OpenAIクライアントを初期化する.
24+
25+
Args:
26+
config: 設定辞書(api_key, base_url, model等を含む)
27+
functions: 利用可能な関数の定義リスト
28+
tools: 利用可能なツールの定義リスト
29+
30+
"""
1831
api_key = config.get("api_key", "OPENAI_API_KEY")
1932
base_url = config.get("base_url", "https://api.openai.com/")
2033
self.openai = openai.OpenAI(api_key=api_key, base_url=base_url, timeout=3600)
@@ -25,23 +38,50 @@ def __init__(
2538
self.tools = tools
2639

2740
def send_system_prompt(self, prompt: str) -> None:
41+
"""システムプロンプトをメッセージ履歴に追加する.
42+
43+
Args:
44+
prompt: システムプロンプトの内容
45+
46+
"""
2847
self.messages.append({"role": "system", "content": prompt})
2948

3049
def send_user_message(self, message: str) -> None:
50+
"""ユーザーメッセージをメッセージ履歴に追加する.
51+
52+
メッセージ履歴が最大トークン数を超えた場合、古いメッセージを削除する。
53+
54+
Args:
55+
message: ユーザーメッセージの内容
56+
57+
"""
3158
self.messages.append({"role": "user", "content": message})
3259
total_chars = sum(len(m["content"]) for m in self.messages)
3360
while total_chars // 4 > self.max_token:
3461
self.messages.pop(1)
3562
total_chars = sum(len(m["content"]) for m in self.messages)
3663

3764
def send_function_result(self, name: str, result: object) -> None:
65+
"""関数の実行結果をメッセージ履歴に追加する.
66+
67+
Args:
68+
name: 実行された関数の名前
69+
result: 関数の実行結果
70+
71+
"""
3872
self.messages.append({"role": "tool", "name": name, "content": json.dumps(result)})
3973
total_chars = sum(len(m["content"]) for m in self.messages)
4074
while total_chars // 4 > self.max_token:
4175
self.messages.pop(1)
4276
total_chars = sum(len(m["content"]) for m in self.messages)
4377

4478
def get_response(self) -> tuple[str, list[Any]]:
79+
"""OpenAI APIから応答を取得する.
80+
81+
Returns:
82+
tuple: (応答テキスト, 関数呼び出しリスト)
83+
84+
"""
4585
resp = self.openai.chat.completions.create(
4686
model=self.model,
4787
messages=self.messages,

condaenv.yaml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ dependencies:
1010
- requests
1111
- pika
1212
- mcp-server-fetch
13+
- ruff
14+
- pytest
15+
- coverage
1316
- pip:
1417
- fastmcp
1518
- pydantic==2.10.1

handlers/task_getter.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -20,7 +20,7 @@ def factory(
2020
mcp_clients: dict[str, Any],
2121
task_source: str,
2222
) -> TaskGetterFromGitHub | TaskGetterFromGitLab:
23-
# Import here to avoid circular import issues
23+
# Import here to avoid circular import issues - this is a legitimate use case
2424
if task_source == "github":
2525
from .task_getter_github import TaskGetterFromGitHub # noqa: PLC0415
2626
return TaskGetterFromGitHub(config, mcp_clients)

handlers/task_handler.py

Lines changed: 6 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -199,11 +199,8 @@ def _execute_functions(self, task: Task, functions: list, error_state: dict) ->
199199
]
200200
task.comment(f"関数呼び出し: {', '.join(list(comments))}")
201201

202-
# 各関数を順次実行
203-
for function in functions:
204-
if self._execute_single_function(task, function, error_state):
205-
return True
206-
return False
202+
# 各関数を順次実行し、いずれか一つでも成功すればTrueを返す
203+
return any(self._execute_single_function(task, function, error_state) for function in functions)
207204

208205
def _execute_single_function(self, task: Task, function: dict, error_state: dict) -> bool:
209206
"""単一の関数を実行する.
@@ -337,6 +334,10 @@ def _process_done_field(self, task: Task, data: dict) -> None:
337334
task.comment(comment_text, mention=True)
338335
task.finish()
339336

337+
def get_system_prompt(self) -> str:
338+
"""システムプロンプトを取得する(テスト用の公開メソッド)."""
339+
return self._make_system_prompt()
340+
340341
def _make_system_prompt(self) -> str:
341342
"""システムプロンプトを生成する.
342343

main.py

Lines changed: 2 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -343,11 +343,8 @@ def main() -> None:
343343
# LLMクライアントの初期化
344344
llm_client = get_llm_client(config, functions, tools)
345345

346-
# タスクキューの初期化
347-
if config.get("use_rabbitmq", False):
348-
task_queue = RabbitMQTaskQueue(config)
349-
else:
350-
task_queue = InMemoryTaskQueue()
346+
# タスクキューの初期化 - RabbitMQまたはインメモリキューを使用
347+
task_queue = RabbitMQTaskQueue(config) if config.get("use_rabbitmq", False) else InMemoryTaskQueue()
351348

352349
# タスクハンドラーの初期化
353350
handler = TaskHandler(llm_client, mcp_clients, config)

pyproject.toml

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
[tool.ruff]
2-
line-length = 100
2+
line-length = 120
33
indent-width = 4
44

55
[tool.ruff.format]
@@ -19,3 +19,6 @@ unfixable = [
1919
"F401", # unused import
2020
"F841", # unused variable
2121
]
22+
23+
[tool.ruff.lint.per-file-ignores]
24+
"tests/**/*.py" = ["S101"]

system_prompt_function_call.txt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ You are an AI coding assistant that cooperates with a controlling program to aut
4141
3. When the task is complete, return the JSON with `{ "done": true, ... }`.
4242
4. Infer project language by file extensions and generate or modify files accordingly.
4343
5. After writing or modifying a file using create_or_update_file, verify the result by reading it back with a suitable tool (e.g., get_file_contents) before proceeding.
44+
6. Before calling `github_create_or_update_file`, you **must** first call `github_get_file_contents` to retrieve the current `sha` of the target file. Include this `sha` in the `create_or_update_file` arguments when updating an existing file.
4445

4546
Always adhere strictly to JSON-only output under this system prompt.
4647

@@ -50,3 +51,4 @@ Always adhere strictly to JSON-only output under this system prompt.
5051
- Do **not** wrap the arguments in a string.
5152
- Correct: `"arguments": { "owner": "my-org", "repo": "test" }`
5253
- Incorrect: `"arguments": "{ \"owner\": \"my-org\", \"repo\": \"test\" }"`
54+
- github_create_or_update_file,github_get_file_contents

tests/integration/test_simple_workflow.py

Lines changed: 13 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -12,10 +12,18 @@
1212
sys.modules["mcp"] = MagicMock()
1313
sys.modules["mcp"].McpError = Exception
1414

15-
from handlers.task_getter_github import TaskGitHubIssue # noqa: E402
16-
from handlers.task_handler import TaskHandler # noqa: E402
17-
from tests.mocks.mock_llm_client import MockLLMClient # noqa: E402
18-
from tests.mocks.mock_mcp_client import MockMCPToolClient # noqa: E402
15+
16+
def _import_test_modules() -> tuple[type, type, type, type]:
17+
"""Import test modules after mocking is set up."""
18+
from handlers.task_getter_github import TaskGitHubIssue # noqa: PLC0415
19+
from handlers.task_handler import TaskHandler # noqa: PLC0415
20+
from tests.mocks.mock_llm_client import MockLLMClient # noqa: PLC0415
21+
from tests.mocks.mock_mcp_client import MockMCPToolClient # noqa: PLC0415
22+
return TaskGitHubIssue, TaskHandler, MockLLMClient, MockMCPToolClient
23+
24+
25+
# Import the modules we need
26+
TaskGitHubIssue, TaskHandler, MockLLMClient, MockMCPToolClient = _import_test_modules()
1927

2028

2129
class TestSimpleWorkflow(unittest.TestCase):
@@ -72,7 +80,7 @@ def test_basic_integration_workflow(self) -> None:
7280
result = task_handler.handle(github_task)
7381

7482
# Should complete without errors
75-
assert result is None # noqa: S101
83+
assert result is None
7684

7785

7886
if __name__ == "__main__":

tests/integration/test_workflow.py

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -8,20 +8,42 @@
88

99
import pytest
1010

11+
"""Comprehensive workflow integration tests for GitHub and GitLab task handlers."""
12+
13+
1114
# Add parent directory to path for imports
1215
sys.path.insert(0, str(Path(__file__).parent.parent.parent))
1316

1417
# Mock the mcp module before importing TaskHandler
1518
sys.modules["mcp"] = MagicMock()
1619
sys.modules["mcp"].McpError = Exception
1720

18-
from handlers.task_factory import GitHubTaskFactory, GitLabTaskFactory # noqa: E402
19-
from handlers.task_getter_github import TaskGetterFromGitHub, TaskGitHubIssue # noqa: E402
20-
from handlers.task_getter_gitlab import TaskGetterFromGitLab, TaskGitLabIssue # noqa: E402
21-
from handlers.task_handler import TaskHandler # noqa: E402
22-
from handlers.task_key import GitHubIssueTaskKey, GitLabIssueTaskKey # noqa: E402
23-
from tests.mocks.mock_llm_client import MockLLMClient, MockLLMClientWithToolCalls # noqa: E402
24-
from tests.mocks.mock_mcp_client import MockMCPToolClient # noqa: E402
21+
22+
def _import_test_modules() -> tuple[type, ...]:
23+
"""Import test modules after mocking is set up."""
24+
from handlers.task_factory import GitHubTaskFactory, GitLabTaskFactory # noqa: PLC0415
25+
from handlers.task_getter_github import TaskGetterFromGitHub, TaskGitHubIssue # noqa: PLC0415
26+
from handlers.task_getter_gitlab import TaskGetterFromGitLab, TaskGitLabIssue # noqa: PLC0415
27+
from handlers.task_handler import TaskHandler # noqa: PLC0415
28+
from handlers.task_key import GitHubIssueTaskKey, GitLabIssueTaskKey # noqa: PLC0415
29+
from tests.mocks.mock_llm_client import ( # noqa: PLC0415
30+
MockLLMClient,
31+
MockLLMClientWithToolCalls,
32+
)
33+
from tests.mocks.mock_mcp_client import MockMCPToolClient # noqa: PLC0415
34+
35+
return (
36+
GitHubTaskFactory, GitLabTaskFactory, TaskGetterFromGitHub, TaskGitHubIssue,
37+
TaskGetterFromGitLab, TaskGitLabIssue, TaskHandler, GitHubIssueTaskKey,
38+
GitLabIssueTaskKey, MockLLMClient, MockLLMClientWithToolCalls, MockMCPToolClient,
39+
)
40+
41+
42+
# Import the modules we need
43+
(GitHubTaskFactory, GitLabTaskFactory, TaskGetterFromGitHub, TaskGitHubIssue,
44+
TaskGetterFromGitLab, TaskGitLabIssue, TaskHandler, GitHubIssueTaskKey,
45+
GitLabIssueTaskKey, MockLLMClient, MockLLMClientWithToolCalls,
46+
MockMCPToolClient) = _import_test_modules()
2547

2648

2749
class TestGitHubWorkflowIntegration(unittest.TestCase):

0 commit comments

Comments
 (0)