import unittest from semantic_index.models import IndexDocument from semantic_index.redmine import RedmineMapper class RedmineMapperTest(unittest.TestCase): def test_issue_chunks_have_stable_ids_and_metadata(self): issue = { "id": 42, "subject": "Widget order ORD-12345 cannot ship", "description": "Customer reports that widget order ORD-12345 is blocked.", "project": {"id": 7, "identifier": "fud-helpdesk"}, "contact": {"id": 9, "email": "ada@example.com", "name": "Ada Lovelace"}, "created_on": "2026-04-01T10:00:00Z", "updated_on": "2026-04-02T10:00:00Z", "url": "http://redmine.local/issues/42", } first = RedmineMapper(redmine_url="http://redmine.local").issue_to_documents(issue) second = RedmineMapper(redmine_url="http://redmine.local").issue_to_documents(issue) self.assertEqual([doc.id for doc in first], [doc.id for doc in second]) self.assertEqual("redmine:issue:42:chunk:0", first[0].id) self.assertEqual("issue", first[0].payload["doc_type"]) self.assertEqual(42, first[0].payload["issue_id"]) self.assertEqual("fud-helpdesk", first[0].payload["project_identifier"]) self.assertIsNone(first[0].payload["project_name"]) self.assertFalse(first[0].payload["has_helpdesk_ticket"]) self.assertEqual("ada@example.com", first[0].payload["contact_email"]) self.assertEqual("Ada Lovelace", first[0].payload["contact_name"]) self.assertEqual("http://redmine.local/issues/42", first[0].payload["redmine_url"]) self.assertIn("source_hash", first[0].payload) def test_helpdesk_ticket_contact_is_mapped_to_all_issue_chunks(self): issue = { "id": 39779, "subject": "Goods return", "description": "Please arrange to return these goods.", "project": {"id": 1, "identifier": "customer-service"}, "helpdesk_ticket": { "id": 35159, "contact_id": 1890, "from_address": "callum@safetagtracking.com", "contact": { "id": 1890, "name": "Callum Mackeonis", "company": "SafeTag Tracking", "email": "callum@safetagtracking.com", }, }, "journals": [ {"id": 71570, "notes": "Hello, yes we can arrange this today.", "created_on": "2026-04-14T14:29:49Z"} ], } docs = RedmineMapper(redmine_url="http://redmine.local").issue_to_documents(issue) issue_doc = next(doc for doc in docs if doc.payload["doc_type"] == "issue") journal_doc = next(doc for doc in docs if doc.payload["doc_type"] == "journal") contact_doc = next(doc for doc in docs if doc.payload["doc_type"] == "contact") for doc in (issue_doc, journal_doc, contact_doc): self.assertEqual(35159, doc.payload["helpdesk_ticket_id"]) self.assertTrue(doc.payload["has_helpdesk_ticket"]) self.assertEqual(1890, doc.payload["contact_id"]) self.assertEqual("Callum Mackeonis", doc.payload["contact_name"]) self.assertEqual("SafeTag Tracking", doc.payload["contact_company"]) self.assertEqual("callum@safetagtracking.com", doc.payload["contact_email"]) self.assertIn("Callum Mackeonis", issue_doc.text) self.assertIn("callum@safetagtracking.com", contact_doc.text) def test_configured_project_identifier_is_used_when_issue_payload_omits_identifier(self): issue = { "id": 42, "subject": "Widget order", "description": "Body", "project": {"id": 1, "name": "Customer Service"}, } docs = RedmineMapper( redmine_url="http://redmine.local", project_identifier="customer-service", ).issue_to_documents(issue) self.assertEqual("customer-service", docs[0].payload["project_identifier"]) self.assertEqual("Customer Service", docs[0].payload["project_name"]) def test_internal_non_helpdesk_issue_keeps_project_metadata_without_contact(self): issue = { "id": 55, "subject": "Internal hiring task", "description": "Follow up with candidate.", "project": {"id": 68, "identifier": "hiring", "name": "Hiring"}, } docs = RedmineMapper(redmine_url="http://redmine.local").issue_to_documents(issue) self.assertEqual(1, len(docs)) self.assertEqual("hiring", docs[0].payload["project_identifier"]) self.assertEqual("Hiring", docs[0].payload["project_name"]) self.assertFalse(docs[0].payload["has_helpdesk_ticket"]) self.assertIsNone(docs[0].payload["contact_id"]) def test_issue_journals_messages_and_contact_are_mapped(self): issue = { "id": 42, "subject": "Widget order", "description": "Ticket envelope", "project": {"id": 7, "identifier": "fud-helpdesk"}, "contact": {"id": 9, "email": "ada@example.com", "name": "Ada Lovelace"}, "journals": [ {"id": 5, "notes": "Private escalation note", "private_notes": True, "created_on": "2026-04-03T10:00:00Z"} ], "messages": [ {"id": 6, "body": "Customer reply body", "direction": "incoming", "created_on": "2026-04-03T11:00:00Z"} ], } docs = RedmineMapper(redmine_url="http://redmine.local").issue_to_documents(issue) ids = {doc.id for doc in docs} types = {doc.payload["doc_type"] for doc in docs} self.assertIn("redmine:issue:42:journal:5:chunk:0", ids) self.assertIn("redmine:issue:42:message:6:chunk:0", ids) self.assertIn("redmine:contact:9:issue:42:chunk:0", ids) self.assertEqual({"issue", "journal", "message", "contact"}, types) journal = next(doc for doc in docs if doc.payload["doc_type"] == "journal") message = next(doc for doc in docs if doc.payload["doc_type"] == "message") self.assertEqual("private", journal.payload["visibility"]) self.assertEqual("incoming", message.payload["direction"]) def test_empty_documents_are_rejected(self): with self.assertRaises(ValueError): IndexDocument(id="x", text=" ", payload={}) if __name__ == "__main__": unittest.main()