Deskaid
by ezyang
- e2e
#!/usr/bin/env python3
"""End-to-end tests for InitProject subtool."""
import os
import unittest
from codemcp.testing import MCPEndToEndTestCase
class InitProjectTest(MCPEndToEndTestCase):
"""Test the InitProject subtool functionality."""
async def test_reuse_head_chat_id(self):
"""Test that reuse_head_chat_id=True reuses the chat ID from the HEAD commit."""
# Set up a git repository in the temp dir
from codemcp.git import (
commit_changes,
get_head_commit_chat_id,
get_ref_commit_chat_id,
)
# Create a simple codemcp.toml file
toml_path = os.path.join(self.temp_dir.name, "codemcp.toml")
with open(toml_path, "w") as f:
f.write("""
project_prompt = "Test reuse chat ID"
[commands]
test = ["./run_test.sh"]
""")
# Set up a git repository
await self.git_run(["init"])
await self.git_run(["config", "user.email", "test@example.com"])
await self.git_run(["config", "user.name", "Test User"])
# Create an initial commit to have a HEAD reference
test_file = os.path.join(self.temp_dir.name, "test_file.txt")
with open(test_file, "w") as f:
f.write("Initial content")
await self.git_run(["add", "."])
await self.git_run(["commit", "-m", "Initial commit"])
# First InitProject call to create a reference with a chat ID
async with self.create_client_session() as session:
result_text1 = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": "First initialization",
"subject_line": "feat: first commit",
},
)
# Extract the chat ID from the result
original_chat_id = self.extract_chat_id_from_text(result_text1)
# Verify the reference contains the chat ID
ref_name = f"refs/codemcp/{original_chat_id}"
ref_chat_id = await get_ref_commit_chat_id(self.temp_dir.name, ref_name)
self.assertEqual(
original_chat_id, ref_chat_id, "Chat ID not found in reference"
)
# The HEAD commit doesn't have the chat ID yet since it's only in the reference
head_chat_id = await get_head_commit_chat_id(self.temp_dir.name)
self.assertNotEqual(
original_chat_id, head_chat_id, "HEAD shouldn't have the chat ID yet"
)
# Make a change and commit it to add the chat ID to HEAD
with open(test_file, "a") as f:
f.write("\nAdded some content")
await commit_changes(
self.temp_dir.name,
description="Adding content",
chat_id=original_chat_id,
)
# Now verify the HEAD has the chat ID
head_chat_id = await get_head_commit_chat_id(self.temp_dir.name)
self.assertEqual(
original_chat_id,
head_chat_id,
"Chat ID should be in HEAD commit after changes",
)
# Second InitProject call with reuse_head_chat_id=True
async with self.create_client_session() as session:
result_text2 = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": "Continue working on the same feature",
"subject_line": "feat: continue working",
"reuse_head_chat_id": True,
},
)
# Extract the chat ID from the result
reused_chat_id = self.extract_chat_id_from_text(result_text2)
# Verify the chat ID is the same as the original
self.assertEqual(original_chat_id, reused_chat_id, "Chat ID not reused")
async def test_init_project_basic(self):
"""Test basic InitProject functionality with simple TOML file."""
# The basic codemcp.toml file is already created in the base test setup
# We'll test that InitProject can read it correctly
async with self.create_client_session() as session:
# Call the InitProject tool with our new helper method
result_text = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": "Test initialization",
"subject_line": "feat: test basic functionality",
"reuse_head_chat_id": False,
},
)
# Verify the result contains expected system prompt elements
self.assertIn("You are an AI assistant", result_text)
async def test_init_project_complex_toml(self):
"""Test InitProject with a more complex TOML file that exercises all parsing features."""
# Create a more complex codemcp.toml file with various data types
complex_toml_path = os.path.join(self.temp_dir.name, "codemcp.toml")
with open(complex_toml_path, "w") as f:
f.write("""# Complex TOML file with various data types
project_prompt = \"\"\"
This is a multiline string
with multiple lines
of text.
\"\"\"
[commands]
format = ["./run_format.sh"]
lint = ["./run_lint.sh"]
[commands.test]
command = ["./run_test.sh"]
doc = "Run tests with optional arguments"
""")
async with self.create_client_session() as session:
# Call the InitProject tool with our new helper method
result_text = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": "Test with complex TOML",
"subject_line": "feat: test complex TOML parsing",
"reuse_head_chat_id": False,
},
)
# Verify the result contains expected elements from the complex TOML
self.assertIn("This is a multiline string", result_text)
self.assertIn("format", result_text)
self.assertIn("lint", result_text)
self.assertIn("Run tests with optional arguments", result_text)
async def test_init_project_with_binary_characters(self):
"""Test InitProject with TOML containing binary/non-ASCII characters to ensure proper handling."""
# Create a TOML file with non-ASCII characters and binary data
binary_toml_path = os.path.join(self.temp_dir.name, "codemcp.toml")
with open(binary_toml_path, "wb") as f:
f.write(b"""project_prompt = "Testing binary data handling \xc2\xa9\xe2\x84\xa2\xf0\x9f\x98\x8a"
[commands]
format = ["./run_format.sh"]
""")
async with self.create_client_session() as session:
# Call the InitProject tool with our new helper method
result_text = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": "Test with binary characters",
"subject_line": "feat: test binary character handling",
"reuse_head_chat_id": False,
},
)
# Verify the result contains expected elements, ensuring binary data was handled properly
self.assertIn("format", result_text)
async def test_chat_id_from_subject_line(self):
"""Test that the chat ID uses the subject line for the human-readable part."""
# Create a simple codemcp.toml file
toml_path = os.path.join(self.temp_dir.name, "codemcp.toml")
with open(toml_path, "w") as f:
f.write("""
project_prompt = "Test project"
[commands]
test = ["./run_test.sh"]
""")
# Set up a git repository
from codemcp.git import get_head_commit_hash, get_ref_commit_chat_id
await self.git_run(["init"])
await self.git_run(["config", "user.email", "test@example.com"])
await self.git_run(["config", "user.name", "Test User"])
# Create an initial commit to have a HEAD reference
test_file = os.path.join(self.temp_dir.name, "test_file.txt")
with open(test_file, "w") as f:
f.write("Initial content")
await self.git_run(["add", "."])
await self.git_run(["commit", "-m", "Initial commit"])
# Get the hash of the initial commit
initial_hash = await get_head_commit_hash(self.temp_dir.name, short=False)
async with self.create_client_session() as session:
# Call InitProject with a conventional commit style subject line
result_text = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": "Test initialize with subject line",
"subject_line": "feat: add new feature with spaces!",
},
)
# Verify that the chat ID contains the slugified subject line
# The chat ID should be something like "1-feat-add-new-feature-with-spaces"
# Check that it's not using "untitled"
self.assertNotIn("untitled", result_text)
# Check that a slugified version of the subject line is in the chat ID
self.assertIn("feat-add-new-feature-with-spaces", result_text)
# Extract the chat ID from the result
chat_id = self.extract_chat_id_from_text(result_text)
# Verify that HEAD hasn't changed - it should still point to the initial commit
head_hash_after = await get_head_commit_hash(
self.temp_dir.name, short=False
)
self.assertEqual(
initial_hash,
head_hash_after,
"HEAD should not change after InitProject",
)
# Verify the reference was created with the chat ID
ref_name = f"refs/codemcp/{chat_id}"
ref_chat_id = await get_ref_commit_chat_id(self.temp_dir.name, ref_name)
self.assertEqual(
chat_id,
ref_chat_id,
f"Chat ID {chat_id} should be in reference {ref_name}",
)
async def test_cherry_pick_reference_commit(self):
"""Test that commit_changes cherry-picks the reference commit when needed."""
from codemcp.git import (
commit_changes,
get_head_commit_chat_id,
get_head_commit_hash,
get_head_commit_message,
)
# Set up a git repository
await self.git_run(["init"])
await self.git_run(["config", "user.email", "test@example.com"])
await self.git_run(["config", "user.name", "Test User"])
# Create an initial commit
initial_file = os.path.join(self.temp_dir.name, "initial.txt")
with open(initial_file, "w") as f:
f.write("Initial content")
await self.git_run(["add", "."])
await self.git_run(["commit", "-m", "Initial commit"])
# Get the hash of the initial commit
initial_hash = await get_head_commit_hash(self.temp_dir.name, short=False)
# Define the test subject and body for InitProject
test_subject = "feat: test reference cherry-pick"
test_body = "Test initialize for cherry-pick test"
# Call InitProject which should create a reference without changing HEAD
async with self.create_client_session() as session:
result_text = await self.call_tool_assert_success(
session,
"codemcp",
{
"subtool": "InitProject",
"path": self.temp_dir.name,
"user_prompt": test_body,
"subject_line": test_subject,
},
)
# Extract the chat ID from the result
chat_id = self.extract_chat_id_from_text(result_text)
# Verify HEAD is unchanged
head_hash_after_init = await get_head_commit_hash(
self.temp_dir.name, short=False
)
self.assertEqual(
initial_hash,
head_hash_after_init,
"HEAD should not change after InitProject",
)
# Now make a change and commit it
test_file = os.path.join(self.temp_dir.name, "test_file.txt")
with open(test_file, "w") as f:
f.write("Test content for cherry-pick test")
# Commit the changes - this should cherry-pick the reference commit first
success, message = await commit_changes(
path=self.temp_dir.name,
description="Testing cherry-pick",
chat_id=chat_id,
)
self.assertTrue(success, f"Commit failed: {message}")
# Verify HEAD has a new commit with the right chat ID
head_chat_id = await get_head_commit_chat_id(self.temp_dir.name)
self.assertEqual(
chat_id,
head_chat_id,
"HEAD commit should have the correct chat ID after cherry-pick",
)
# Verify the commit message contains the original subject and body from InitProject
head_commit_msg = await get_head_commit_message(self.temp_dir.name)
self.assertIsNotNone(head_commit_msg, "Commit message should not be None")
# Check that both the subject and body are in the commit message
self.assertIn(
test_subject,
head_commit_msg,
"Commit message should contain the original subject line",
)
self.assertIn(
test_body,
head_commit_msg,
"Commit message should contain the original body",
)
# Check that the new description is also included since this is an amended commit
self.assertIn(
"Testing cherry-pick",
head_commit_msg,
"Commit message should contain the new change description",
)
# Get commit count to verify we have more than just the initial commit
commit_count_output = await self.git_run(
["rev-list", "--count", "HEAD"], capture_output=True, text=True
)
commit_count = int(commit_count_output.strip())
self.assertGreater(
commit_count, 1, "Should have more than one commit after changes"
)
if __name__ == "__main__":
unittest.main()