diff --git a/scripts/lib/qmd.py b/scripts/lib/qmd.py index 22afc36..2501ee5 100644 --- a/scripts/lib/qmd.py +++ b/scripts/lib/qmd.py @@ -87,6 +87,17 @@ class Setup: if not node_version_ok(self.run(['node', '--version'])): raise ValueError('QMD local search needs Node.js 22 or newer.') + def stop_legacy_daemon(self, legacy): + """Ask the old Python daemon to exit so its files can be deleted (Windows keeps open files locked).""" + python = legacy / '.venv' / ('Scripts/python.exe' if WINDOWS else 'bin/python') + server = legacy / 'server.py' + if python.is_file() and server.is_file(): + try: + self.run([str(python), str(server), 'stop', '--data-dir', str(legacy / 'data'), + '--model-cache', str(legacy / 'model-cache')], timeout=60, check=False) + except (ValueError, OSError, subprocess.SubprocessError): + pass + def migrate(self, state=None): state = self.load() if state is None else state legacy_state = self.base / 'semantic-retrieval.json' @@ -95,6 +106,7 @@ class Setup: if legacy.is_symlink(): raise ValueError('Legacy semantic retrieval directory must not be a symbolic link.') if legacy.exists(): + self.stop_legacy_daemon(legacy) shutil.rmtree(legacy) legacy_state.unlink(missing_ok=True) self.say('Removed the previous Python semantic retrieval installation.') diff --git a/scripts/tests/test-qmd-setup.py b/scripts/tests/test-qmd-setup.py index 9a61207..cbfbac4 100644 --- a/scripts/tests/test-qmd-setup.py +++ b/scripts/tests/test-qmd-setup.py @@ -267,6 +267,29 @@ class SetupTest(unittest.TestCase): self.setup(FakeRunner(fail=('--detect',))).enable(['claude']) self.assertEqual(self.state()['device'], 'cpu') + def legacy_install(self): + legacy = self.home / '.my-ai-configuration/semantic-retrieval' + python = legacy / '.venv' / ('Scripts/python.exe' if os.name == 'nt' else 'bin/python') + python.parent.mkdir(parents=True) + python.write_text('') + (legacy / 'server.py').write_text('') + return legacy, python + + def test_migration_stops_the_legacy_daemon_before_removal(self): + legacy, python = self.legacy_install() + seen = [] + runner = FakeRunner(installed=True, on_call=lambda a: seen.append(legacy.exists()) if 'server.py' in ' '.join(a) else None) + self.setup(runner).enable(['claude']) + self.assertIn([str(python), str(legacy / 'server.py'), 'stop', '--data-dir', str(legacy / 'data'), + '--model-cache', str(legacy / 'model-cache')], runner.calls) + self.assertEqual(seen, [True], 'stop runs while the legacy directory still exists') + self.assertFalse(legacy.exists()) + + def test_failing_legacy_stop_does_not_block_migration(self): + legacy, _ = self.legacy_install() + self.setup(FakeRunner(installed=True, fail=('server.py',))).enable(['claude']) + self.assertFalse(legacy.exists()) + if __name__ == '__main__': unittest.main()