Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
122 changes: 122 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: CI

on:
push:
pull_request:
workflow_dispatch:

jobs:
smoke:
runs-on: ubuntu-latest

services:
mongodb:
image: mongo:latest
options: >-
--health-cmd mongosh
--health-interval 10s
--health-timeout 5s
--health-retries 5
ports:
- 27017:27017
env:
MONGO_INITDB_ROOT_USERNAME: admin
MONGO_INITDB_ROOT_PASSWORD: mongodb

steps:
- name: Checkout
uses: actions/checkout@v4

- name: Set up Python
uses: actions/setup-python@v5
with:
python-version: "3.12"

- name: Set up Node
uses: actions/setup-node@v4
with:
node-version: "20"

- name: Repo smoke checks
shell: bash
run: |
set -euo pipefail

echo "Validating repository structure and basic runnable signals"

has_signal=0

if find . -maxdepth 4 -type f -name "package.json" | grep -q .; then
has_signal=1
while IFS= read -r pkg; do
[ -z "$pkg" ] && continue
node -e "const fs=require('fs'); JSON.parse(fs.readFileSync(process.argv[1],'utf8'));" "$pkg"
done < <(find . -maxdepth 4 -type f -name "package.json")
fi

if find . -maxdepth 4 -type f \( -name "pyproject.toml" -o -name "requirements.txt" -o -name "setup.py" -o -name "manage.py" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f \( -name "app.py" -o -name "main.py" -o -name "wsgi.py" -o -name "asgi.py" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f \( -name "pom.xml" -o -name "build.gradle" -o -name "build.gradle.kts" -o -name "gradlew" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f -name "go.mod" | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f -name "Cargo.toml" | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f \( -name "*.csproj" -o -name "*.sln" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f \( -name "Dockerfile" -o -name "docker-compose.yml" -o -name "docker-compose.yaml" \) | grep -q .; then
has_signal=1
fi

if find . -maxdepth 4 -type f -name "Makefile" | grep -q .; then
has_signal=1
fi

if [ "$has_signal" -ne 1 ]; then
echo "No runnable/build signals found in repository"
exit 1
fi

echo "Running Python syntax smoke check"
python_files="$(find . -type f -name '*.py' -not -path './.git/*' 2>/dev/null || true)"
if [ -n "$python_files" ]; then
while IFS= read -r f; do
[ -z "$f" ] && continue
python -m py_compile "$f"
done <<< "$python_files"
fi

echo "Smoke checks passed"

- name: Run repository runtime smoke test
shell: bash
run: |
set -euo pipefail
if [ -f tests/test_runtime.py ]; then
python tests/test_runtime.py
else
echo "No runtime smoke test file found"
exit 1
fi

- name: Install integration test dependencies
run: pip install pytest pymongo

- name: Run integration tests
env:
MONGODB_URI: mongodb://admin:mongodb@localhost:27017/
run: pytest tests/test_integration.py -v
4 changes: 2 additions & 2 deletions app.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
from graph import create_workflow, AgentState
from mongodb.connect import get_mongo_client
from mongodb import checkpointer
from motor.motor_asyncio import AsyncIOMotorClient
from pymongo import AsyncMongoClient
from langchain_core.messages import HumanMessage, AIMessage
from utilities import sanitize_name
from langchain.schema.runnable import Runnable
Expand Down Expand Up @@ -41,7 +41,7 @@ async def on_chat_start():

workflow = create_workflow(chatbot_agent, tools)

mongo_client = AsyncIOMotorClient(MONGO_URI)
mongo_client = AsyncMongoClient(MONGO_URI)
mongodb_checkpointer = checkpointer.MongoDBSaver(mongo_client, DATABASE_NAME, "checkpoints_collection")

graph = workflow.compile(checkpointer=mongodb_checkpointer)
Expand Down
6 changes: 3 additions & 3 deletions mongodb/checkpointer.py
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@
SerializerProtocol,
)
from langgraph.serde.jsonplus import JsonPlusSerializer
from motor.motor_asyncio import AsyncIOMotorClient
from pymongo import AsyncMongoClient

class JsonPlusSerializerCompat(JsonPlusSerializer):
def loads(self, data: bytes) -> Any:
Expand All @@ -25,13 +25,13 @@ def loads(self, data: bytes) -> Any:
class MongoDBSaver(AbstractContextManager, BaseCheckpointSaver):
serde = JsonPlusSerializerCompat()

client: AsyncIOMotorClient
client: AsyncMongoClient
db_name: str
collection_name: str

def __init__(
self,
client: AsyncIOMotorClient,
client: AsyncMongoClient,
db_name: str,
collection_name: str,
*,
Expand Down
3 changes: 1 addition & 2 deletions requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ langsmith==0.1.84
Lazify==0.4.0
literalai==0.0.607
marshmallow==3.21.3
motor==3.5.1
multidict==6.0.5
mypy-extensions==1.0.0
nest-asyncio==1.6.0
Expand All @@ -77,7 +76,7 @@ pyasn1_modules==0.4.0
pydantic==2.8.2
pydantic_core==2.20.1
PyJWT==2.8.0
pymongo==4.8.0
pymongo==4.13.1
pyparsing==3.1.2
python-dateutil==2.9.0.post0
python-dotenv==1.0.1
Expand Down
Empty file added tests/__init__.py
Empty file.
141 changes: 141 additions & 0 deletions tests/test_integration.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
"""Integration tests for hr_agentic_chatbot.

Tests real MongoDB connectivity and session history storage
used by the HR agentic chatbot.

Requires a running MongoDB instance. Set MONGODB_URI (default:
mongodb://admin:mongodb@localhost:27017/) or the tests will be skipped.
"""

import os
import sys
import pytest
from pathlib import Path
from pymongo import MongoClient
from bson import ObjectId

MONGODB_URI = os.environ.get("MONGODB_URI", "mongodb://admin:mongodb@localhost:27017/")
TEST_DB = "hr_chatbot_integration_test"


@pytest.fixture(scope="module")
def db():
client = MongoClient(MONGODB_URI, serverSelectionTimeoutMS=2000)
try:
client.admin.command("ping")
except Exception:
client.close()
pytest.skip(f"MongoDB not reachable at {MONGODB_URI}")
database = client[TEST_DB]
yield database
client.drop_database(TEST_DB)
client.close()


def test_mongodb_ping():
client = MongoClient(MONGODB_URI, serverSelectionTimeoutMS=2000)
try:
result = client.admin.command("ping")
assert result.get("ok") == 1.0
except Exception:
pytest.skip(f"MongoDB not reachable at {MONGODB_URI}")
finally:
client.close()


def test_get_mongo_client_with_real_uri():
"""get_mongo_client() returns a connected client when given a real URI."""
try:
connect_mod_path = Path(__file__).resolve().parents[1] / "mongodb" / "connect.py"

import types
import importlib.util

# Stub langchain_mongodb if not available
if "langchain_mongodb" not in sys.modules:
stub = types.ModuleType("langchain_mongodb")
chm_stub = types.ModuleType("langchain_mongodb.chat_message_histories")
chm_stub.MongoDBChatMessageHistory = type(
"MongoDBChatMessageHistory",
(),
{"__init__": lambda self, *a, **kw: None},
)
stub.chat_message_histories = chm_stub
sys.modules["langchain_mongodb"] = stub
sys.modules["langchain_mongodb.chat_message_histories"] = chm_stub

if "dotenv" not in sys.modules:
dotenv_stub = types.ModuleType("dotenv")
dotenv_stub.load_dotenv = lambda *a, **kw: None
sys.modules["dotenv"] = dotenv_stub

spec = importlib.util.spec_from_file_location("hr_connect_int", connect_mod_path)
mod = importlib.util.module_from_spec(spec)
os.environ["MONGO_URI"] = MONGODB_URI
spec.loader.exec_module(mod)

client = mod.get_mongo_client(MONGODB_URI)
assert client is not None

result = client.admin.command("ping")
assert result.get("ok") == 1.0
client.close()
except Exception as exc:
pytest.skip(f"App-level test skipped: {exc}")


def test_chat_history_collection_crud(db):
"""history collection: store and retrieve chat messages."""
history = db["history"]

session_id = f"test_session_{ObjectId()}"
messages = [
{
"_id": ObjectId(),
"SessionId": session_id,
"History": "Human: Hello\nAI: Hi there!",
},
{
"_id": ObjectId(),
"SessionId": session_id,
"History": "Human: What is MongoDB?\nAI: A NoSQL database.",
},
]
history.insert_many(messages)

session_messages = list(history.find({"SessionId": session_id}))
assert len(session_messages) == 2
assert all(m["SessionId"] == session_id for m in session_messages)

# Cleanup
history.delete_many({"SessionId": session_id})


def test_employee_record_crud(db):
"""employee records collection: store and retrieve HR data."""
employees = db["employees"]

emp_id = ObjectId()
employee = {
"_id": emp_id,
"name": "Test Employee",
"department": "Engineering",
"role": "Developer",
"salary": 90000,
"start_date": "2023-01-15",
}

employees.insert_one(employee)

found = employees.find_one({"_id": emp_id})
assert found["name"] == "Test Employee"
assert found["department"] == "Engineering"

# Update
employees.update_one({"_id": emp_id}, {"$set": {"salary": 95000}})
updated = employees.find_one({"_id": emp_id})
assert updated["salary"] == 95000

# Delete
employees.delete_one({"_id": emp_id})
assert employees.find_one({"_id": emp_id}) is None
Loading
Loading