27 KiB
Pre-Deployment Testing Implementation Plan
For Claude: REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task.
Goal: Create a pre-deployment test pipeline with Makefile orchestration, mock Grist server, and MCP protocol integration tests.
Architecture: Makefile orchestrates unit tests, Docker builds, and integration tests. Integration tests use the MCP Python SDK to connect to the containerized grist-mcp server, which talks to a mock Grist API server. Both run in docker-compose on an isolated network.
Tech Stack: Python 3.14, pytest, MCP SDK, Starlette (mock server), Docker Compose, Make
Task 1: Add Health Endpoint to grist-mcp
The integration tests need to poll for service readiness. Add a /health endpoint.
Files:
- Modify:
src/grist_mcp/main.py:42-47
Step 1: Add health endpoint to main.py
In src/grist_mcp/main.py, add a health route to the Starlette app:
from starlette.responses import JSONResponse
async def handle_health(request):
return JSONResponse({"status": "ok"})
And add the route:
return Starlette(
routes=[
Route("/health", endpoint=handle_health),
Route("/sse", endpoint=handle_sse),
Route("/messages", endpoint=handle_messages, methods=["POST"]),
]
)
Step 2: Run existing tests
Run: uv run pytest tests/test_server.py -v
Expected: PASS (health endpoint doesn't break existing tests)
Step 3: Commit
git add src/grist_mcp/main.py
git commit -m "feat: add /health endpoint for service readiness checks"
Task 2: Create Mock Grist Server
Files:
- Create:
tests/integration/mock_grist/__init__.py - Create:
tests/integration/mock_grist/server.py - Create:
tests/integration/mock_grist/Dockerfile - Create:
tests/integration/mock_grist/requirements.txt
Step 1: Create directory structure
mkdir -p tests/integration/mock_grist
Step 2: Create requirements.txt
Create tests/integration/mock_grist/requirements.txt:
starlette>=0.41.0
uvicorn>=0.32.0
Step 3: Create init.py
Create empty tests/integration/mock_grist/__init__.py:
Step 4: Create server.py
Create tests/integration/mock_grist/server.py:
"""Mock Grist API server for integration testing."""
import json
import logging
import os
from datetime import datetime
from starlette.applications import Starlette
from starlette.responses import JSONResponse
from starlette.routing import Route
logging.basicConfig(level=logging.INFO, format="%(asctime)s [MOCK-GRIST] %(message)s")
logger = logging.getLogger(__name__)
# Mock data
MOCK_TABLES = {
"People": {
"columns": [
{"id": "Name", "fields": {"type": "Text"}},
{"id": "Age", "fields": {"type": "Int"}},
{"id": "Email", "fields": {"type": "Text"}},
],
"records": [
{"id": 1, "fields": {"Name": "Alice", "Age": 30, "Email": "alice@example.com"}},
{"id": 2, "fields": {"Name": "Bob", "Age": 25, "Email": "bob@example.com"}},
],
},
"Tasks": {
"columns": [
{"id": "Title", "fields": {"type": "Text"}},
{"id": "Done", "fields": {"type": "Bool"}},
],
"records": [
{"id": 1, "fields": {"Title": "Write tests", "Done": False}},
{"id": 2, "fields": {"Title": "Deploy", "Done": False}},
],
},
}
# Track requests for test assertions
request_log: list[dict] = []
def log_request(method: str, path: str, body: dict | None = None):
"""Log a request for later inspection."""
entry = {
"timestamp": datetime.utcnow().isoformat(),
"method": method,
"path": path,
"body": body,
}
request_log.append(entry)
logger.info(f"{method} {path}" + (f" body={json.dumps(body)}" if body else ""))
async def health(request):
"""Health check endpoint."""
return JSONResponse({"status": "ok"})
async def get_request_log(request):
"""Return the request log for test assertions."""
return JSONResponse(request_log)
async def clear_request_log(request):
"""Clear the request log."""
request_log.clear()
return JSONResponse({"status": "cleared"})
async def list_tables(request):
"""GET /api/docs/{doc_id}/tables"""
doc_id = request.path_params["doc_id"]
log_request("GET", f"/api/docs/{doc_id}/tables")
tables = [{"id": name} for name in MOCK_TABLES.keys()]
return JSONResponse({"tables": tables})
async def get_table_columns(request):
"""GET /api/docs/{doc_id}/tables/{table_id}/columns"""
doc_id = request.path_params["doc_id"]
table_id = request.path_params["table_id"]
log_request("GET", f"/api/docs/{doc_id}/tables/{table_id}/columns")
if table_id not in MOCK_TABLES:
return JSONResponse({"error": "Table not found"}, status_code=404)
return JSONResponse({"columns": MOCK_TABLES[table_id]["columns"]})
async def get_records(request):
"""GET /api/docs/{doc_id}/tables/{table_id}/records"""
doc_id = request.path_params["doc_id"]
table_id = request.path_params["table_id"]
log_request("GET", f"/api/docs/{doc_id}/tables/{table_id}/records")
if table_id not in MOCK_TABLES:
return JSONResponse({"error": "Table not found"}, status_code=404)
return JSONResponse({"records": MOCK_TABLES[table_id]["records"]})
async def add_records(request):
"""POST /api/docs/{doc_id}/tables/{table_id}/records"""
doc_id = request.path_params["doc_id"]
table_id = request.path_params["table_id"]
body = await request.json()
log_request("POST", f"/api/docs/{doc_id}/tables/{table_id}/records", body)
# Return mock IDs for new records
new_ids = [{"id": 100 + i} for i in range(len(body.get("records", [])))]
return JSONResponse({"records": new_ids})
async def update_records(request):
"""PATCH /api/docs/{doc_id}/tables/{table_id}/records"""
doc_id = request.path_params["doc_id"]
table_id = request.path_params["table_id"]
body = await request.json()
log_request("PATCH", f"/api/docs/{doc_id}/tables/{table_id}/records", body)
return JSONResponse({})
async def delete_records(request):
"""POST /api/docs/{doc_id}/tables/{table_id}/data/delete"""
doc_id = request.path_params["doc_id"]
table_id = request.path_params["table_id"]
body = await request.json()
log_request("POST", f"/api/docs/{doc_id}/tables/{table_id}/data/delete", body)
return JSONResponse({})
async def sql_query(request):
"""GET /api/docs/{doc_id}/sql"""
doc_id = request.path_params["doc_id"]
query = request.query_params.get("q", "")
log_request("GET", f"/api/docs/{doc_id}/sql?q={query}")
# Return mock SQL results
return JSONResponse({
"records": [
{"fields": {"Name": "Alice", "Age": 30}},
{"fields": {"Name": "Bob", "Age": 25}},
]
})
async def create_tables(request):
"""POST /api/docs/{doc_id}/tables"""
doc_id = request.path_params["doc_id"]
body = await request.json()
log_request("POST", f"/api/docs/{doc_id}/tables", body)
# Return the created tables with their IDs
tables = [{"id": t["id"]} for t in body.get("tables", [])]
return JSONResponse({"tables": tables})
async def add_column(request):
"""POST /api/docs/{doc_id}/tables/{table_id}/columns"""
doc_id = request.path_params["doc_id"]
table_id = request.path_params["table_id"]
body = await request.json()
log_request("POST", f"/api/docs/{doc_id}/tables/{table_id}/columns", body)
columns = [{"id": c["id"]} for c in body.get("columns", [])]
return JSONResponse({"columns": columns})
async def modify_column(request):
"""PATCH /api/docs/{doc_id}/tables/{table_id}/columns/{col_id}"""
doc_id = request.path_params["doc_id"]
table_id = request.path_params["table_id"]
col_id = request.path_params["col_id"]
body = await request.json()
log_request("PATCH", f"/api/docs/{doc_id}/tables/{table_id}/columns/{col_id}", body)
return JSONResponse({})
async def delete_column(request):
"""DELETE /api/docs/{doc_id}/tables/{table_id}/columns/{col_id}"""
doc_id = request.path_params["doc_id"]
table_id = request.path_params["table_id"]
col_id = request.path_params["col_id"]
log_request("DELETE", f"/api/docs/{doc_id}/tables/{table_id}/columns/{col_id}")
return JSONResponse({})
app = Starlette(
routes=[
# Test control endpoints
Route("/health", endpoint=health),
Route("/_test/requests", endpoint=get_request_log),
Route("/_test/requests/clear", endpoint=clear_request_log, methods=["POST"]),
# Grist API endpoints
Route("/api/docs/{doc_id}/tables", endpoint=list_tables),
Route("/api/docs/{doc_id}/tables", endpoint=create_tables, methods=["POST"]),
Route("/api/docs/{doc_id}/tables/{table_id}/columns", endpoint=get_table_columns),
Route("/api/docs/{doc_id}/tables/{table_id}/columns", endpoint=add_column, methods=["POST"]),
Route("/api/docs/{doc_id}/tables/{table_id}/columns/{col_id}", endpoint=modify_column, methods=["PATCH"]),
Route("/api/docs/{doc_id}/tables/{table_id}/columns/{col_id}", endpoint=delete_column, methods=["DELETE"]),
Route("/api/docs/{doc_id}/tables/{table_id}/records", endpoint=get_records),
Route("/api/docs/{doc_id}/tables/{table_id}/records", endpoint=add_records, methods=["POST"]),
Route("/api/docs/{doc_id}/tables/{table_id}/records", endpoint=update_records, methods=["PATCH"]),
Route("/api/docs/{doc_id}/tables/{table_id}/data/delete", endpoint=delete_records, methods=["POST"]),
Route("/api/docs/{doc_id}/sql", endpoint=sql_query),
]
)
if __name__ == "__main__":
import uvicorn
port = int(os.environ.get("PORT", "8484"))
logger.info(f"Starting mock Grist server on port {port}")
uvicorn.run(app, host="0.0.0.0", port=port)
Step 5: Create Dockerfile
Create tests/integration/mock_grist/Dockerfile:
FROM python:3.14-slim
WORKDIR /app
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
COPY server.py .
ENV PORT=8484
EXPOSE 8484
CMD ["python", "server.py"]
Step 6: Commit
git add tests/integration/mock_grist/
git commit -m "feat: add mock Grist server for integration testing"
Task 3: Create Integration Test Configuration
Files:
- Create:
tests/integration/__init__.py - Create:
tests/integration/config.test.yaml
Step 1: Create init.py
Create empty tests/integration/__init__.py:
Step 2: Create config.test.yaml
Create tests/integration/config.test.yaml:
documents:
test-doc:
url: http://mock-grist:8484
doc_id: test-doc-id
api_key: test-api-key
tokens:
- token: test-token
name: test-agent
scope:
- document: test-doc
permissions: [read, write, schema]
Step 3: Commit
git add tests/integration/__init__.py tests/integration/config.test.yaml
git commit -m "feat: add integration test configuration"
Task 4: Create Docker Compose Test Configuration
Files:
- Create:
docker-compose.test.yaml
Step 1: Create docker-compose.test.yaml
Create docker-compose.test.yaml:
services:
grist-mcp:
build: .
ports:
- "3000:3000"
environment:
- CONFIG_PATH=/app/config.yaml
- GRIST_MCP_TOKEN=test-token
- PORT=3000
volumes:
- ./tests/integration/config.test.yaml:/app/config.yaml:ro
depends_on:
mock-grist:
condition: service_started
networks:
- test-net
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:3000/health')"]
interval: 5s
timeout: 5s
retries: 5
mock-grist:
build: tests/integration/mock_grist
ports:
- "8484:8484"
environment:
- PORT=8484
networks:
- test-net
healthcheck:
test: ["CMD", "python", "-c", "import urllib.request; urllib.request.urlopen('http://localhost:8484/health')"]
interval: 5s
timeout: 5s
retries: 5
networks:
test-net:
driver: bridge
Step 2: Commit
git add docker-compose.test.yaml
git commit -m "feat: add docker-compose for integration testing"
Task 5: Create Integration Test Fixtures
Files:
- Create:
tests/integration/conftest.py
Step 1: Create conftest.py
Create tests/integration/conftest.py:
"""Fixtures for integration tests."""
import asyncio
import time
import httpx
import pytest
from mcp import ClientSession
from mcp.client.sse import sse_client
GRIST_MCP_URL = "http://localhost:3000"
MOCK_GRIST_URL = "http://localhost:8484"
MAX_WAIT_SECONDS = 30
def wait_for_service(url: str, timeout: int = MAX_WAIT_SECONDS) -> bool:
"""Wait for a service to become healthy."""
start = time.time()
while time.time() - start < timeout:
try:
response = httpx.get(f"{url}/health", timeout=2.0)
if response.status_code == 200:
return True
except httpx.RequestError:
pass
time.sleep(0.5)
return False
@pytest.fixture(scope="session")
def services_ready():
"""Ensure both services are healthy before running tests."""
if not wait_for_service(MOCK_GRIST_URL):
pytest.fail(f"Mock Grist server not ready at {MOCK_GRIST_URL}")
if not wait_for_service(GRIST_MCP_URL):
pytest.fail(f"grist-mcp server not ready at {GRIST_MCP_URL}")
return True
@pytest.fixture
async def mcp_client(services_ready):
"""Create an MCP client connected to grist-mcp via SSE."""
async with sse_client(f"{GRIST_MCP_URL}/sse") as (read_stream, write_stream):
async with ClientSession(read_stream, write_stream) as session:
await session.initialize()
yield session
@pytest.fixture
def mock_grist_client(services_ready):
"""HTTP client for interacting with mock Grist test endpoints."""
with httpx.Client(base_url=MOCK_GRIST_URL, timeout=10.0) as client:
yield client
@pytest.fixture(autouse=True)
def clear_mock_grist_log(mock_grist_client):
"""Clear the mock Grist request log before each test."""
mock_grist_client.post("/_test/requests/clear")
yield
Step 2: Commit
git add tests/integration/conftest.py
git commit -m "feat: add integration test fixtures with MCP client"
Task 6: Create MCP Protocol Tests
Files:
- Create:
tests/integration/test_mcp_protocol.py
Step 1: Create test_mcp_protocol.py
Create tests/integration/test_mcp_protocol.py:
"""Test MCP protocol compliance over SSE transport."""
import pytest
@pytest.mark.asyncio
async def test_mcp_connection_initializes(mcp_client):
"""Test that MCP client can connect and initialize."""
# If we get here, connection and initialization succeeded
assert mcp_client is not None
@pytest.mark.asyncio
async def test_list_tools_returns_all_tools(mcp_client):
"""Test that list_tools returns all expected tools."""
result = await mcp_client.list_tools()
tool_names = [tool.name for tool in result.tools]
expected_tools = [
"list_documents",
"list_tables",
"describe_table",
"get_records",
"sql_query",
"add_records",
"update_records",
"delete_records",
"create_table",
"add_column",
"modify_column",
"delete_column",
]
for expected in expected_tools:
assert expected in tool_names, f"Missing tool: {expected}"
assert len(result.tools) == 12
@pytest.mark.asyncio
async def test_list_tools_has_descriptions(mcp_client):
"""Test that all tools have descriptions."""
result = await mcp_client.list_tools()
for tool in result.tools:
assert tool.description, f"Tool {tool.name} has no description"
assert len(tool.description) > 10, f"Tool {tool.name} description too short"
@pytest.mark.asyncio
async def test_list_tools_has_input_schemas(mcp_client):
"""Test that all tools have input schemas."""
result = await mcp_client.list_tools()
for tool in result.tools:
assert tool.inputSchema is not None, f"Tool {tool.name} has no inputSchema"
assert "type" in tool.inputSchema, f"Tool {tool.name} schema missing type"
Step 2: Commit
git add tests/integration/test_mcp_protocol.py
git commit -m "feat: add MCP protocol compliance tests"
Task 7: Create Tool Integration Tests
Files:
- Create:
tests/integration/test_tools_integration.py
Step 1: Create test_tools_integration.py
Create tests/integration/test_tools_integration.py:
"""Test tool calls through MCP client to verify Grist API interactions."""
import json
import pytest
@pytest.mark.asyncio
async def test_list_documents(mcp_client):
"""Test list_documents returns accessible documents."""
result = await mcp_client.call_tool("list_documents", {})
assert len(result.content) == 1
data = json.loads(result.content[0].text)
assert "documents" in data
assert len(data["documents"]) == 1
assert data["documents"][0]["name"] == "test-doc"
assert "read" in data["documents"][0]["permissions"]
@pytest.mark.asyncio
async def test_list_tables(mcp_client, mock_grist_client):
"""Test list_tables calls correct Grist API endpoint."""
result = await mcp_client.call_tool("list_tables", {"document": "test-doc"})
# Check response
data = json.loads(result.content[0].text)
assert "tables" in data
assert "People" in data["tables"]
assert "Tasks" in data["tables"]
# Verify mock received correct request
log = mock_grist_client.get("/_test/requests").json()
assert len(log) >= 1
assert log[-1]["method"] == "GET"
assert "/tables" in log[-1]["path"]
@pytest.mark.asyncio
async def test_describe_table(mcp_client, mock_grist_client):
"""Test describe_table returns column information."""
result = await mcp_client.call_tool(
"describe_table",
{"document": "test-doc", "table": "People"}
)
data = json.loads(result.content[0].text)
assert "columns" in data
column_ids = [c["id"] for c in data["columns"]]
assert "Name" in column_ids
assert "Age" in column_ids
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
assert any("/columns" in entry["path"] for entry in log)
@pytest.mark.asyncio
async def test_get_records(mcp_client, mock_grist_client):
"""Test get_records fetches records from table."""
result = await mcp_client.call_tool(
"get_records",
{"document": "test-doc", "table": "People"}
)
data = json.loads(result.content[0].text)
assert "records" in data
assert len(data["records"]) == 2
assert data["records"][0]["Name"] == "Alice"
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
assert any("/records" in entry["path"] and entry["method"] == "GET" for entry in log)
@pytest.mark.asyncio
async def test_sql_query(mcp_client, mock_grist_client):
"""Test sql_query executes SQL and returns results."""
result = await mcp_client.call_tool(
"sql_query",
{"document": "test-doc", "query": "SELECT Name, Age FROM People"}
)
data = json.loads(result.content[0].text)
assert "records" in data
assert len(data["records"]) >= 1
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
assert any("/sql" in entry["path"] for entry in log)
@pytest.mark.asyncio
async def test_add_records(mcp_client, mock_grist_client):
"""Test add_records sends correct payload to Grist."""
new_records = [
{"Name": "Charlie", "Age": 35, "Email": "charlie@example.com"}
]
result = await mcp_client.call_tool(
"add_records",
{"document": "test-doc", "table": "People", "records": new_records}
)
data = json.loads(result.content[0].text)
assert "record_ids" in data
assert len(data["record_ids"]) == 1
# Verify API call body
log = mock_grist_client.get("/_test/requests").json()
post_requests = [e for e in log if e["method"] == "POST" and "/records" in e["path"]]
assert len(post_requests) >= 1
assert post_requests[-1]["body"]["records"][0]["fields"]["Name"] == "Charlie"
@pytest.mark.asyncio
async def test_update_records(mcp_client, mock_grist_client):
"""Test update_records sends correct payload to Grist."""
updates = [
{"id": 1, "fields": {"Age": 31}}
]
result = await mcp_client.call_tool(
"update_records",
{"document": "test-doc", "table": "People", "records": updates}
)
data = json.loads(result.content[0].text)
assert "updated" in data
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
patch_requests = [e for e in log if e["method"] == "PATCH" and "/records" in e["path"]]
assert len(patch_requests) >= 1
@pytest.mark.asyncio
async def test_delete_records(mcp_client, mock_grist_client):
"""Test delete_records sends correct IDs to Grist."""
result = await mcp_client.call_tool(
"delete_records",
{"document": "test-doc", "table": "People", "record_ids": [1, 2]}
)
data = json.loads(result.content[0].text)
assert "deleted" in data
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
delete_requests = [e for e in log if "/data/delete" in e["path"]]
assert len(delete_requests) >= 1
assert delete_requests[-1]["body"] == [1, 2]
@pytest.mark.asyncio
async def test_create_table(mcp_client, mock_grist_client):
"""Test create_table sends correct schema to Grist."""
columns = [
{"id": "Title", "type": "Text"},
{"id": "Count", "type": "Int"},
]
result = await mcp_client.call_tool(
"create_table",
{"document": "test-doc", "table_id": "NewTable", "columns": columns}
)
data = json.loads(result.content[0].text)
assert "table_id" in data
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
post_tables = [e for e in log if e["method"] == "POST" and e["path"].endswith("/tables")]
assert len(post_tables) >= 1
@pytest.mark.asyncio
async def test_add_column(mcp_client, mock_grist_client):
"""Test add_column sends correct column definition."""
result = await mcp_client.call_tool(
"add_column",
{
"document": "test-doc",
"table": "People",
"column_id": "Phone",
"column_type": "Text",
}
)
data = json.loads(result.content[0].text)
assert "column_id" in data
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
post_cols = [e for e in log if e["method"] == "POST" and "/columns" in e["path"]]
assert len(post_cols) >= 1
@pytest.mark.asyncio
async def test_modify_column(mcp_client, mock_grist_client):
"""Test modify_column sends correct update."""
result = await mcp_client.call_tool(
"modify_column",
{
"document": "test-doc",
"table": "People",
"column_id": "Age",
"type": "Numeric",
}
)
data = json.loads(result.content[0].text)
assert "modified" in data
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
patch_cols = [e for e in log if e["method"] == "PATCH" and "/columns/" in e["path"]]
assert len(patch_cols) >= 1
@pytest.mark.asyncio
async def test_delete_column(mcp_client, mock_grist_client):
"""Test delete_column calls correct endpoint."""
result = await mcp_client.call_tool(
"delete_column",
{
"document": "test-doc",
"table": "People",
"column_id": "Email",
}
)
data = json.loads(result.content[0].text)
assert "deleted" in data
# Verify API call
log = mock_grist_client.get("/_test/requests").json()
delete_cols = [e for e in log if e["method"] == "DELETE" and "/columns/" in e["path"]]
assert len(delete_cols) >= 1
@pytest.mark.asyncio
async def test_unauthorized_document_fails(mcp_client):
"""Test that accessing unauthorized document returns error."""
result = await mcp_client.call_tool(
"list_tables",
{"document": "unauthorized-doc"}
)
assert "error" in result.content[0].text.lower() or "authorization" in result.content[0].text.lower()
Step 2: Commit
git add tests/integration/test_tools_integration.py
git commit -m "feat: add tool integration tests with Grist API validation"
Task 8: Create Makefile
Files:
- Create:
Makefile
Step 1: Create Makefile
Create Makefile:
.PHONY: help test build integration-up integration-test integration-down integration pre-deploy clean
# Default target
help: ## Show this help
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | sort | awk 'BEGIN {FS = ":.*?## "}; {printf "\033[36m%-20s\033[0m %s\n", $$1, $$2}'
test: ## Run unit tests
uv run pytest tests/ -v --ignore=tests/integration
build: ## Build Docker images for testing
docker compose -f docker-compose.test.yaml build
integration-up: ## Start integration test containers
docker compose -f docker-compose.test.yaml up -d
@echo "Waiting for services to be ready..."
@sleep 5
integration-test: ## Run integration tests (containers must be up)
uv run pytest tests/integration/ -v
integration-down: ## Stop and remove test containers
docker compose -f docker-compose.test.yaml down -v
integration: build integration-up ## Full integration cycle (build, up, test, down)
@$(MAKE) integration-test || ($(MAKE) integration-down && exit 1)
@$(MAKE) integration-down
pre-deploy: test integration ## Full pre-deployment pipeline (unit tests + integration)
@echo "Pre-deployment checks passed!"
clean: ## Remove all test artifacts and containers
docker compose -f docker-compose.test.yaml down -v --rmi local 2>/dev/null || true
find . -type d -name __pycache__ -exec rm -rf {} + 2>/dev/null || true
find . -type d -name .pytest_cache -exec rm -rf {} + 2>/dev/null || true
Step 2: Verify Makefile syntax
Run: make help
Expected: List of available targets with descriptions
Step 3: Commit
git add Makefile
git commit -m "feat: add Makefile for test orchestration"
Task 9: Run Full Pre-Deploy Pipeline
Step 1: Run unit tests
Run: make test
Expected: All unit tests pass
Step 2: Run full pre-deploy
Run: make pre-deploy
Expected: Unit tests pass, Docker builds succeed, integration tests pass, containers cleaned up
Step 3: Commit any fixes needed
If any tests fail, fix them and commit:
git add -A
git commit -m "fix: resolve integration test issues"
Summary
Files created:
src/grist_mcp/main.py- Modified with /health endpointtests/integration/mock_grist/__init__.pytests/integration/mock_grist/server.pytests/integration/mock_grist/Dockerfiletests/integration/mock_grist/requirements.txttests/integration/__init__.pytests/integration/config.test.yamltests/integration/conftest.pytests/integration/test_mcp_protocol.pytests/integration/test_tools_integration.pydocker-compose.test.yamlMakefile
Usage:
make help # Show all targets
make test # Unit tests only
make integration # Integration tests only
make pre-deploy # Full pipeline
make clean # Cleanup