import importlib.util from pathlib import Path import subprocess import sys import json import pytest def updater(): path=Path(__file__).resolve().parents[1]/'deploy/update.py' spec=importlib.util.spec_from_file_location('linux_updater',path) module=importlib.util.module_from_spec(spec);spec.loader.exec_module(module) return module class Commands: def __init__(self, *, dirty=False, same=False, diverged=False, failed=None, states=None): self.calls=[];self.dirty=dirty;self.same=same;self.diverged=diverged self.failed=failed;self.states=states or ['active','active'] def __call__(self,*args): self.calls.append(args) if self.failed and self.failed(args):raise RuntimeError('injected failure') if args[0]=='git': cmd=args[3:] if cmd[0]=='status':return ' M edited.py' if self.dirty else '' if cmd==('rev-parse','HEAD'):return 'a'*40 if cmd[0]=='rev-parse':return 'origin/main' if '--abbrev-ref' in cmd else ('a' if self.same else 'b')*40 if cmd[0]=='merge-base' and self.diverged:raise RuntimeError('not fast forward') if args[:2]==('systemctl','show'): return self.states[0 if 'worker' in args[-1] else 1] return '' def test_plan_has_no_mutations(): module=updater() result=subprocess.run([sys.executable,module.__file__,'--dry-run'],capture_output=True,text=True,check=True) assert json.loads(result.stdout)['status']=='PLAN' @pytest.mark.parametrize('kwargs',[{'dirty':True},{'diverged':True}]) def test_refuses_local_changes_or_divergence_before_stopping(kwargs): module=updater();commands=Commands(**kwargs) with pytest.raises(RuntimeError):module.update(execute=commands) assert not any(c[0]=='systemctl' for c in commands.calls) def test_same_revision_is_installed_after_bootstrap_pull(monkeypatch): module=updater();commands=Commands(same=True) monkeypatch.setattr(module,'trusted',lambda root:None) assert module.update(execute=commands)['status']=='UPDATED' assert any('pip' in c for c in commands.calls) @pytest.mark.parametrize('states,started',[(['active','active'],['api','worker']),(['active','inactive'],['worker']),(['inactive','inactive'],[])]) def test_update_order_and_preserves_inactive_services(monkeypatch,states,started): module=updater();commands=Commands(states=states) monkeypatch.setattr(module,'trusted',lambda root:None) result=module.update(execute=commands) assert result['status']=='UPDATED' calls=commands.calls stops=[c for c in calls if c[:2]==('systemctl','stop')] assert stops==[('systemctl','stop',s) for s in module.SERVICES] merged=next(i for i,c in enumerate(calls) if c[0]=='git' and c[3]=='merge') installed=next(i for i,c in enumerate(calls) if 'pip' in c) assert calls.index(stops[-1])