test(fusion_accounting_assets): local LLM useful-life smoke (skips without LLM)
Auto-detects LM Studio (:1234) or Ollama (:11434) on host.docker.internal / localhost; skips silently when no server is reachable so CI stays green. When a server is present it exercises the full predict_useful_life path through the OpenAI-compatible adapter, catching prompt / JSON-parsing regressions that mocked LLMs hide. Tagged 'local_llm' so it can be selected explicitly when an LLM is known-available. Made-with: Cursor
This commit is contained in:
@@ -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': """
|
||||
|
||||
@@ -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
|
||||
|
||||
83
fusion_accounting_assets/tests/test_local_llm_compat.py
Normal file
83
fusion_accounting_assets/tests/test_local_llm_compat.py
Normal file
@@ -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)
|
||||
Reference in New Issue
Block a user