diff --git a/fusion_accounting_assets/__manifest__.py b/fusion_accounting_assets/__manifest__.py index f3dc8849..20bc21a1 100644 --- a/fusion_accounting_assets/__manifest__.py +++ b/fusion_accounting_assets/__manifest__.py @@ -1,6 +1,6 @@ { 'name': 'Fusion Accounting Assets', - 'version': '19.0.1.0.35', + 'version': '19.0.1.0.36', 'category': 'Accounting/Accounting', 'summary': 'AI-augmented asset management with depreciation schedules.', 'description': """ diff --git a/fusion_accounting_assets/tests/__init__.py b/fusion_accounting_assets/tests/__init__.py index 501c72a1..e1b37dce 100644 --- a/fusion_accounting_assets/tests/__init__.py +++ b/fusion_accounting_assets/tests/__init__.py @@ -28,3 +28,4 @@ from . import test_audit_report from . import test_coexistence from . import test_assets_tours from . import test_perf_controller +from . import test_local_llm_compat diff --git a/fusion_accounting_assets/tests/test_local_llm_compat.py b/fusion_accounting_assets/tests/test_local_llm_compat.py new file mode 100644 index 00000000..eec86053 --- /dev/null +++ b/fusion_accounting_assets/tests/test_local_llm_compat.py @@ -0,0 +1,83 @@ +"""Local LLM compat smoke test for the useful_life_predictor service. + +Auto-detects an LM Studio (port 1234) or Ollama (port 11434) server on +host.docker.internal or localhost. Skips silently when no local LLM is +reachable, so CI runs stay green. + +When a server is present, this exercises the real OpenAI-compatible +adapter end-to-end against a local model — i.e. it catches prompt / +JSON-parsing regressions that only show up with a non-mocked LLM. +""" + +import socket + +from odoo.tests.common import TransactionCase +from odoo.tests import 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(): + 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 TestLocalLLMUsefulLife(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_useful_life_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.asset_useful_life', + ] + 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.asset_useful_life', 'openai') + + try: + from odoo.addons.fusion_accounting_assets.services.useful_life_predictor import ( + predict_useful_life, + ) + result = predict_useful_life( + self.env, + description='Dell laptop', + amount=2500, + partner_name='Dell Canada', + ) + self.assertIn('useful_life_years', result) + self.assertIn('depreciation_method', result) + self.assertIsInstance(result['useful_life_years'], (int, float)) + self.assertIn( + result['depreciation_method'], + ('straight_line', 'declining_balance', 'units_of_production'), + ) + finally: + for k, v in prior.items(): + if v is not None: + params.set_param(k, v)