import io import json import unittest from contextlib import redirect_stdout from pathlib import Path from semantic_index.__main__ import main from semantic_index.config import Settings from semantic_index.models import SearchResult class FakeSearchService: def __init__(self): self.queries = [] def search(self, query): self.queries.append(query) if "missing@example.test" in query.text: return [] return [ SearchResult( id="redmine:contact:1890:issue:39779:chunk:0" if "callum" in query.text else "redmine:issue:39779:chunk:0", score=0.58, text="Callum Mackeonis callum@safetagtracking.com SafeTag Tracking", payload={ "source": "redmine", "doc_type": "contact" if "callum" in query.text else "issue", "issue_id": 39779, "project_identifier": "customer-service", "contact_id": 1890, "contact_name": "Callum Mackeonis", "contact_email": "callum@safetagtracking.com", "contact_company": "SafeTag Tracking", "redmine_url": "http://redmine/issues/39779", }, ) ] def get_document(self, document_id): return { "id": document_id, "text": "Full indexed text", "payload": { "source": "redmine", "doc_type": "journal", "issue_id": 39778, "project_identifier": "customer-service", "contact_id": 1890, "contact_name": "Callum Mackeonis", "contact_email": "callum@safetagtracking.com", "redmine_url": "http://redmine/issues/39778", }, } class FakeStore: def __init__(self): self.list_limits = [] def count_documents(self, source=None, project_identifier=None, doc_type=None): return 12 def list_documents(self, limit=10, source=None, project_identifier=None, doc_type=None): self.list_limits.append(limit) return [ { "id": "redmine:issue:39779:chunk:0", "text": "Issue #39779: Goods return\nPlease return our goods.", "payload": { "source": "redmine", "doc_type": "issue", "issue_id": 39779, "project_identifier": "customer-service", "project_name": "Customer Service", "has_helpdesk_ticket": True, "contact_id": 1890, "contact_name": "Callum Mackeonis", "contact_email": "callum@safetagtracking.com", "contact_company": "SafeTag Tracking", "source_hash": "issue-hash", "redmine_url": "http://redmine/issues/39779", }, }, { "id": "redmine:issue:39779:journal:71570:chunk:0", "text": "Hello, we can arrange this today.", "payload": { "source": "redmine", "doc_type": "journal", "issue_id": 39779, "project_identifier": "customer-service", "project_name": "Customer Service", "has_helpdesk_ticket": True, "contact_id": 1890, "contact_name": "Callum Mackeonis", "contact_email": "callum@safetagtracking.com", "contact_company": "SafeTag Tracking", "source_hash": "journal-hash", "redmine_url": "http://redmine/issues/39779", }, }, { "id": "redmine:contact:1890:issue:39779:chunk:0", "text": "Callum Mackeonis callum@safetagtracking.com SafeTag Tracking", "payload": { "source": "redmine", "doc_type": "contact", "issue_id": 39779, "project_identifier": "customer-service", "project_name": "Customer Service", "has_helpdesk_ticket": True, "contact_id": 1890, "contact_name": "Callum Mackeonis", "contact_email": "callum@safetagtracking.com", "contact_company": "SafeTag Tracking", "source_hash": "contact-hash", "redmine_url": "http://redmine/issues/39779", }, }, { "id": "redmine:issue:39800:chunk:0", "text": "Ordinary issue with no helpdesk contact.", "payload": { "source": "redmine", "doc_type": "issue", "issue_id": 39800, "project_identifier": "hiring", "project_name": "Hiring", "has_helpdesk_ticket": False, "source_hash": "ordinary-hash", "redmine_url": "http://redmine/issues/39800", }, }, ] class FakeRedmineSource: def recent_helpdesk_issues(self, limit): return [ { "id": 39779, "subject": "Goods return", "description": "Please return our goods.", "project": {"id": 1, "identifier": "customer-service"}, "helpdesk_ticket": { "id": 35159, "contact_id": 1890, "contact": { "id": 1890, "name": "Callum Mackeonis", "email": "callum@safetagtracking.com", "company": "SafeTag Tracking", }, }, } ][:limit] def fake_services(store=None, search=None): settings = Settings( openai_api_key="", qdrant_url="http://qdrant", qdrant_api_key=None, qdrant_collection="semantic", redmine_url="http://redmine", redmine_api_key="", redmine_project_identifier="customer-service", sample_limit=50, bind_host="127.0.0.1", bind_port=8787, service_api_key=None, refresh_state_path=Path(".cache/semantic_index/refresh_state.json"), ) return { "settings": settings, "search": search or FakeSearchService(), "store": store or FakeStore(), "redmine_source": FakeRedmineSource(), "backfill": FakeBackfillService(), } class FakeBackfillService: def __init__(self): self.calls = [] def backfill_redmine_sample(self, limit): self.calls.append(("sample", limit)) return {"source": "redmine", "issues": limit, "documents": limit} def backfill_redmine_projects(self, projects, per_project_limit): self.calls.append(("projects", projects, per_project_limit)) return { "source": "redmine", "projects": len(projects), "issues": len(projects) * per_project_limit, "documents": len(projects) * per_project_limit, "project_results": [ {"project_identifier": project, "issues": per_project_limit, "documents": per_project_limit} for project in projects ], } def backfill_redmine_project_limits(self, project_limits): self.calls.append(("project_limits", project_limits)) return { "source": "redmine", "projects": len(project_limits), "issues": sum(project_limits.values()), "documents": sum(project_limits.values()), "project_results": [ {"project_identifier": project, "issues": limit, "documents": limit} for project, limit in project_limits.items() ], } class InspectCliTest(unittest.TestCase): def run_cli(self, args): out = io.StringIO() with redirect_stdout(out): main(args, service_builder=fake_services) return out.getvalue() def test_no_args_prints_help_without_building_services(self): def broken_services(): raise AssertionError("help should not build live services") out = io.StringIO() with redirect_stdout(out): main([], service_builder=broken_services) self.assertIn("inspect", out.getvalue()) def test_count_lists_matching_document_count(self): output = self.run_cli(["inspect", "count", "--source", "redmine", "--project", "customer-service"]) self.assertIn("12", output) def test_list_shows_snippet_and_metadata_by_default(self): output = self.run_cli(["inspect", "list", "--limit", "5", "--source", "redmine", "--project", "customer-service"]) self.assertIn("redmine:issue:39779:chunk:0", output) self.assertIn("issue #39779", output.lower()) self.assertIn("customer-service", output) self.assertIn("contact=#1890", output) self.assertIn("Callum Mackeonis", output) self.assertIn("callum@safetagtracking.com", output) self.assertNotIn("Full indexed text", output) def test_search_runs_query_and_prints_citation(self): output = self.run_cli(["inspect", "search", "order status", "--limit", "3", "--project", "customer-service"]) self.assertIn("score=0.5800", output) self.assertIn("http://redmine/issues/39779", output) def test_show_prints_full_document_text(self): output = self.run_cli(["inspect", "show", "redmine:issue:39778:chunk:0"]) self.assertIn("Full indexed text", output) self.assertIn("doc_type=journal", output) def test_preview_redmine_maps_documents_without_writing(self): output = self.run_cli(["inspect", "preview-redmine", "--limit", "1", "--project", "customer-service"]) self.assertIn("redmine:issue:39779:chunk:0", output) self.assertIn("project=customer-service", output) self.assertIn("Please return our goods", output) def test_preview_redmine_uses_minimal_service_builder(self): services = [] def minimal_builder(settings): services.append(settings.redmine_project_identifier) return {"settings": settings, "redmine_source": FakeRedmineSource()} out = io.StringIO() with redirect_stdout(out): main( ["inspect", "preview-redmine", "--limit", "1", "--project", "customer-service"], service_builder=lambda: (_ for _ in ()).throw(AssertionError("full services should not be built")), preview_service_builder=minimal_builder, settings_loader=lambda: fake_services()["settings"], ) self.assertEqual(["customer-service"], services) self.assertIn("redmine:issue:39779:chunk:0", out.getvalue()) def test_audit_prints_doc_type_counts_contact_coverage_and_attachment_check(self): output = self.run_cli(["inspect", "audit", "--limit", "10", "--source", "redmine", "--project", "customer-service"]) self.assertIn("documents=4", output) self.assertIn("doc_type issue=2", output) self.assertIn("doc_type journal=1", output) self.assertIn("doc_type contact=1", output) self.assertIn("contact_metadata 3/4", output) self.assertIn("helpdesk_contact_metadata 3/3", output) self.assertIn("project customer-service=3", output) self.assertIn("project hiring=1", output) self.assertIn("attachments=0", output) self.assertNotIn("missing_contact redmine:issue:39800:chunk:0", output) def test_audit_json_returns_machine_readable_summary(self): output = self.run_cli(["inspect", "audit", "--limit", "10", "--project", "customer-service", "--json"]) payload = json.loads(output) self.assertEqual(4, payload["total_documents"]) self.assertEqual(2, payload["doc_type_counts"]["issue"]) self.assertEqual(3, payload["project_counts"]["customer-service"]) self.assertEqual(1, payload["project_counts"]["hiring"]) self.assertEqual([], payload["missing_helpdesk_contact_metadata"]) def test_compare_redmine_reports_missing_stale_and_contact_mismatches(self): output = self.run_cli(["inspect", "compare-redmine", "--limit", "1", "--project", "customer-service"]) self.assertIn("preview_documents=2", output) self.assertIn("indexed_documents=4", output) self.assertIn("stale", output) self.assertIn("redmine:issue:39779:chunk:0", output) def test_compare_redmine_fetches_a_large_index_window_to_avoid_false_missing_results(self): store = FakeStore() out = io.StringIO() with redirect_stdout(out): main(["inspect", "compare-redmine", "--limit", "3", "--project", "customer-service"], service_builder=lambda: fake_services(store=store)) self.assertEqual(5000, store.list_limits[0]) def test_smoke_search_prints_pass_fail_for_known_queries(self): output = self.run_cli(["inspect", "smoke-search", "--project", "customer-service", "--email", "callum@safetagtracking.com", "--issue-id", "39779"]) self.assertIn("PASS email callum@safetagtracking.com", output) self.assertIn("PASS issue 39779", output) self.assertIn("redmine:contact:1890:issue:39779:chunk:0", output) def test_smoke_search_uses_issue_id_filter_for_issue_checks(self): search = FakeSearchService() out = io.StringIO() with redirect_stdout(out): main(["inspect", "smoke-search", "--project", "customer-service", "--issue-id", "39779"], service_builder=lambda: fake_services(search=search)) issue_queries = [query for query in search.queries if query.text == "39779"] self.assertEqual(39779, issue_queries[0].issue_id) def test_smoke_search_json_returns_check_results(self): output = self.run_cli(["inspect", "smoke-search", "--project", "customer-service", "--email", "missing@example.test", "--json"]) payload = json.loads(output) self.assertFalse(payload["checks"][0]["passed"]) self.assertEqual("email", payload["checks"][0]["kind"]) def test_backfill_redmine_projects_cli_parses_comma_separated_projects(self): backfill = FakeBackfillService() services = fake_services() services["backfill"] = backfill out = io.StringIO() with redirect_stdout(out): main( [ "--backfill-redmine-projects", "--projects", "customer-service,hiring", "--per-project-limit", "25", ], service_builder=lambda: services, ) self.assertEqual(("projects", ["customer-service", "hiring"], 25), backfill.calls[0]) self.assertIn("'projects': 2", out.getvalue()) def test_backfill_redmine_projects_cli_parses_project_specific_limits(self): backfill = FakeBackfillService() services = fake_services() services["backfill"] = backfill out = io.StringIO() with redirect_stdout(out): main( [ "--backfill-redmine-projects", "--project-limits", "customer-service=500,hiring=200", ], service_builder=lambda: services, ) self.assertEqual(("project_limits", {"customer-service": 500, "hiring": 200}), backfill.calls[0]) self.assertIn("'issues': 700", out.getvalue()) if __name__ == "__main__": unittest.main()