Files
Odoo-Modules/fusion_accounting_assets/tests/test_local_llm_compat.py
gsinghpal 7e2c31e371 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
2026-04-19 20:23:30 -04:00

84 lines
3.0 KiB
Python

"""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)