test_memory_service.py 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566
  1. from pathlib import Path
  2. from tempfile import TemporaryDirectory
  3. import unittest
  4. from unittest.mock import Mock, patch
  5. from scripts.memory_service import render, unit_name
  6. from scripts.deploy import Deployment, MODULE
  7. class MemoryServiceTests(unittest.TestCase):
  8. def test_render_limits_children_swap_and_restart_storms(self):
  9. content = render('/opt/business-assistant', 'assistant', 4096)
  10. for expected in ('MemoryMax=4096M', 'MemoryHigh=3584M', 'MemorySwapMax=0',
  11. 'OOMPolicy=kill', 'KillMode=control-group', 'Restart=on-failure',
  12. 'StartLimitBurst=3', 'API_REQUIRE_MEMORY_CGROUP=1',
  13. 'API_MEMORY_HARD_LIMIT_MB=4096', 'API_MEMORY_BUDGET_MB=3072',
  14. 'ExecStart=/bin/sh /opt/business-assistant/deploy.sh serve'):
  15. self.assertIn(expected, content)
  16. self.assertNotIn('EnvironmentFile', content)
  17. self.assertEqual(unit_name('/opt/a'), unit_name('/opt/a'))
  18. self.assertNotEqual(unit_name('/opt/a'), unit_name('/opt/b'))
  19. def test_render_rejects_unsafe_parameters(self):
  20. for args in [('/opt/a\nMemoryMax=infinity', 'assistant', 4096),
  21. ('/opt/%a', 'assistant', 4096), ('/opt/a', 'root', 4096),
  22. ('/opt/a', 'assistant', 100), ('/opt/a', 'a\nUser=root', 4096)]:
  23. with self.assertRaises(ValueError):
  24. render(*args)
  25. def test_render_uses_offline_entry_for_extracted_release(self):
  26. content = render('/opt/business-assistant', 'assistant', 4096, offline=True)
  27. self.assertIn('ExecStart=/bin/sh /opt/business-assistant/deploy-offline.sh serve', content)
  28. def test_installed_unit_routes_start_stop_without_background_fallback(self):
  29. with TemporaryDirectory() as directory:
  30. deployment = Deployment(Path(directory))
  31. deployment.systemd_managed = lambda: True
  32. deployment.systemd_pid = lambda: 1
  33. deployment.settings = Mock(return_value=Mock(host='127.0.0.1', port=8000))
  34. deployment.processes = Mock(return_value=[{'pid': 1}])
  35. deployment.healthy = Mock(return_value=True)
  36. deployment.run = Mock()
  37. with patch('subprocess.Popen') as popen:
  38. deployment.start()
  39. popen.assert_not_called()
  40. deployment.run.assert_called_once_with('systemctl', 'start', unit_name(deployment.root))
  41. deployment.run.reset_mock()
  42. deployment.processes.return_value = []
  43. deployment.stop()
  44. deployment.run.assert_called_once_with('systemctl', 'stop', unit_name(deployment.root))
  45. deployment.run.side_effect = RuntimeError('synthetic permission denied')
  46. with patch('subprocess.Popen') as popen:
  47. with self.assertRaises(RuntimeError):
  48. deployment.start()
  49. popen.assert_not_called()
  50. def test_foreground_resolves_selected_runtime_and_requires_guard(self):
  51. with TemporaryDirectory() as directory:
  52. deployment = Deployment(Path(directory))
  53. deployment.processes = Mock(return_value=[])
  54. with patch.dict('os.environ', {}, clear=True):
  55. with self.assertRaises(RuntimeError):
  56. deployment.serve()
  57. with patch.dict('os.environ', {'API_REQUIRE_MEMORY_CGROUP': '1'}), patch('os.execv') as execute:
  58. deployment.serve()
  59. execute.assert_called_once_with(str(deployment.python), [str(deployment.python), '-m', MODULE])