From 0618ca777316b543a027e50f97efdaf89e0459ca Mon Sep 17 00:00:00 2001 From: gsinghpal Date: Sun, 19 Apr 2026 16:30:05 -0400 Subject: [PATCH] test(fusion_accounting_reports): local LLM commentary smoke (skips without LLM) Made-with: Cursor --- fusion_accounting_reports/__manifest__.py | 2 +- fusion_accounting_reports/tests/__init__.py | 1 + .../tests/test_local_llm_compat.py | 86 +++++++++++++++++++ 3 files changed, 88 insertions(+), 1 deletion(-) create mode 100644 fusion_accounting_reports/tests/test_local_llm_compat.py diff --git a/fusion_accounting_reports/__manifest__.py b/fusion_accounting_reports/__manifest__.py index 4605c9d7..eeb10a65 100644 --- a/fusion_accounting_reports/__manifest__.py +++ b/fusion_accounting_reports/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Reports', - 'version': '19.0.1.0.37', + 'version': '19.0.1.0.38', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented financial reports (P&L, balance sheet, trial balance, GL).', 'description': """ diff --git a/fusion_accounting_reports/tests/__init__.py b/fusion_accounting_reports/tests/__init__.py index 1d14bf1d..d8e465e2 100644 --- a/fusion_accounting_reports/tests/__init__.py +++ b/fusion_accounting_reports/tests/__init__.py @@ -25,3 +25,4 @@ from . import test_migration_round_trip from . import test_coexistence from . import test_reports_tours from . import test_performance_benchmarks +from . import test_local_llm_compat diff --git a/fusion_accounting_reports/tests/test_local_llm_compat.py b/fusion_accounting_reports/tests/test_local_llm_compat.py new file mode 100644 index 00000000..80b0b415 --- /dev/null +++ b/fusion_accounting_reports/tests/test_local_llm_compat.py @@ -0,0 +1,86 @@ +"""Local LLM compat smoke for the commentary generator. + +Auto-detects an LM Studio (:1234) or Ollama (:11434) server on either +`host.docker.internal` or `localhost`. If none is reachable the test +self-skips so CI without a local LLM stays green. + +Tagged 'local_llm' so it's never part of the default run. +""" + +import socket +from datetime import date + +from odoo.tests.common import TransactionCase, tagged + + +def _server_reachable(host, port, timeout=1.0): + try: + with socket.create_connection((host, port), timeout=timeout): + return True + except (OSError, socket.timeout): + return False + + +def _detect_local_llm(): + """Return (base_url, default_model) for the first reachable server, or + (None, None) if none of the common dev endpoints respond.""" + candidates = [ + ('host.docker.internal', 1234, 'local-model'), + ('host.docker.internal', 11434, 'llama3.1:8b'), + ('localhost', 1234, 'local-model'), + ('localhost', 11434, 'llama3.1:8b'), + ] + for host, port, default_model in candidates: + if _server_reachable(host, port, timeout=0.5): + return (f'http://{host}:{port}/v1', default_model) + return (None, None) + + +@tagged('post_install', '-at_install', 'local_llm') +class TestLocalLLMCommentary(TransactionCase): + + def setUp(self): + super().setUp() + self.base_url, self.model = _detect_local_llm() + if not self.base_url: + self.skipTest( + "No local LLM server detected " + "(LM Studio :1234 / Ollama :11434)" + ) + + def test_commentary_with_local_llm(self): + params = self.env['ir.config_parameter'].sudo() + keys = [ + 'fusion_accounting.openai_base_url', + 'fusion_accounting.openai_model', + 'fusion_accounting.openai_api_key', + 'fusion_accounting.provider.reports_commentary', + ] + prior = {k: params.get_param(k) for k in keys} + + params.set_param('fusion_accounting.openai_base_url', self.base_url) + params.set_param('fusion_accounting.openai_model', self.model) + params.set_param('fusion_accounting.openai_api_key', 'lm-studio') + params.set_param( + 'fusion_accounting.provider.reports_commentary', 'openai', + ) + + try: + from odoo.addons.fusion_accounting_reports.services.commentary_generator import ( + generate_commentary, + ) + from odoo.addons.fusion_accounting_reports.services.date_periods import ( + Period, + ) + + period = Period(date(2026, 1, 1), date(2026, 12, 31), '2026') + result = self.env['fusion.report.engine'].compute_pnl( + period, company_id=self.env.company.id, + ) + commentary = generate_commentary(self.env, report_result=result) + self.assertIn('summary', commentary) + # Don't assert specific content - just that it returned a dict + finally: + for k, v in prior.items(): + if v is not None: + params.set_param(k, v)